diff --git a/Experts/SimpleFVG.mq5 b/Experts/SimpleFVG.mq5 index fb286cf..ca4ef07 100644 --- a/Experts/SimpleFVG.mq5 +++ b/Experts/SimpleFVG.mq5 @@ -14,6 +14,7 @@ #include "SimpleFVG/Config.mqh" #include "SimpleFVG/Trend.mqh" #include "SimpleFVG/FVG.mqh" +#include "SimpleFVG/Trade.mqh" #include "SimpleFVG/Drawing.mqh" //+------------------------------------------------------------------+ @@ -63,5 +64,6 @@ void OnTick() TrendUpdate(); FVGUpdate(); + ManageFVGTrades(); DrawAll(); } diff --git a/Experts/SimpleFVG/Config.mqh b/Experts/SimpleFVG/Config.mqh index dec8630..772edaa 100644 --- a/Experts/SimpleFVG/Config.mqh +++ b/Experts/SimpleFVG/Config.mqh @@ -6,30 +6,39 @@ #define __SIMPLE_FVG_CONFIG_MQH__ //--- Step 0: Asset & Timeframe --- -input string InpSymbol = ""; // Symbol (empty = current chart) -input ENUM_TIMEFRAMES InpTimeframe = PERIOD_H1; // Analysis Timeframe +input string InpSymbol = ""; +input ENUM_TIMEFRAMES InpTimeframe = PERIOD_H1; //--- Step 1: Trend (EMA) --- -input int InpEMAFastPeriod = 34; // EMA Fast Period -input int InpEMASlowPeriod = 89; // EMA Slow Period +input int InpEMAFastPeriod = 34; +input int InpEMASlowPeriod = 89; //--- Step 2: FVG Detection --- -input int InpFVGLookbackBars = 100; // FVG: Lookback range (bars) -input int InpFVGMaxAgeBars = 50; // FVG: Max age before expiry (bars) -input double InpFVGMinBodyPct = 50.0; // FVG: Min body % of impulse candle -input double InpFVGMinSizePoints = 0; // FVG: Min gap size (points, 0=any) +input int InpFVGLookbackBars = 100; +input int InpFVGMaxAgeBars = 50; +input double InpFVGMinBodyPct = 50.0; +input double InpFVGMinSizePoints = 0; +input double InpFVGTouchedPercent = 40.0; + +//--- Step 3: Trading --- +input bool InpTradeEnabled = true; +input double InpRiskPercentPerR = 1.0; +input double InpRRRatio = 2.2; +input int InpMaxLimitOrders = 3; +input int InpLimitMaxAgeBars = 24; +input long InpEAMagic = 123456; //--- Step 4: Drawing --- -input color InpColorBullFVG = C'30,80,140'; // Color: Bullish FVG zone -input color InpColorBearFVG = C'140,30,20'; // Color: Bearish FVG zone -input color InpColorMitigatedFVG = C'60,60,60'; // Color: Mitigated FVG zone -input color InpColorEMAFast = clrDodgerBlue; // Color: EMA Fast line -input color InpColorEMASlow = clrOrangeRed; // Color: EMA Slow line -input bool InpShowPanel = true; // Draw: Show info panel -input bool InpShowFVGLabels = true; // Draw: Show FVG labels +input color InpColorBullFVG = C'30,80,140'; +input color InpColorBearFVG = C'140,30,20'; +input color InpColorMitigatedFVG = C'60,60,60'; +input color InpColorEMAFast = clrDodgerBlue; +input color InpColorEMASlow = clrOrangeRed; +input bool InpShowPanel = true; +input bool InpShowFVGLabels = true; //--- Debug --- -input bool InpDebugLog = true; // Debug: Enable logging +input bool InpDebugLog = true; //+------------------------------------------------------------------+ //| Enums | @@ -47,6 +56,14 @@ enum ENUM_FVG_TYPE FVG_BEARISH = -1 }; +enum ENUM_FVG_STATUS +{ + ACTIVE = 0, // Giá chưa quay lại test + TOUCHED = 1, // Giá đã retest >= threshold % + MITIGATED = 2, // Giá đâm xuyên qua vùng + EXPIRED = 3 // Hết hạn, không còn vẽ/đếm +}; + //+------------------------------------------------------------------+ //| Constants | //+------------------------------------------------------------------+ diff --git a/Experts/SimpleFVG/Drawing.mqh b/Experts/SimpleFVG/Drawing.mqh index ad970e8..a053eee 100644 --- a/Experts/SimpleFVG/Drawing.mqh +++ b/Experts/SimpleFVG/Drawing.mqh @@ -7,6 +7,7 @@ #include "Config.mqh" #include "Trend.mqh" #include "FVG.mqh" +#include "Trade.mqh" //+------------------------------------------------------------------+ //| Object-creation helpers | @@ -75,6 +76,81 @@ void DrawTextOnChart(string name, datetime t, double price, ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); } +//+------------------------------------------------------------------+ +//| Draw limit orders (TradingView-style lines) | +//+------------------------------------------------------------------+ +void DrawLimitOrders() +{ + ObjectsDeleteAll(0, EA_PREFIX + "ORD_"); + + string symbol = GetTradeSymbol(); + datetime currentBar = iTime(symbol, InpTimeframe, 0); + + for(int i = 0; i < OrdersTotal(); i++) + { + ulong ticket = OrderGetTicket(i); + if(ticket == 0) continue; + if(!OrderSelect(ticket)) continue; + + if((long)OrderGetInteger(ORDER_MAGIC) != InpEAMagic) continue; + if((string)OrderGetString(ORDER_SYMBOL) != symbol) continue; + + ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); + if(type != ORDER_TYPE_BUY_LIMIT && type != ORDER_TYPE_SELL_LIMIT) + continue; + + double entryPrice = OrderGetDouble(ORDER_PRICE_OPEN); + double slPrice = OrderGetDouble(ORDER_SL); + double tpPrice = OrderGetDouble(ORDER_TP); + double volume = OrderGetDouble(ORDER_VOLUME_CURRENT); + datetime setupTime = (datetime)OrderGetInteger(ORDER_TIME_SETUP); + + string side = (type == ORDER_TYPE_BUY_LIMIT) ? "BUY" : "SELL"; + color entryClr = (type == ORDER_TYPE_BUY_LIMIT) ? clrDeepSkyBlue : clrOrangeRed; + color slClr = clrRed; + color tpClr = clrLime; + + string id = IntegerToString((int)ticket); + + // Entry line + DrawTrendLine( + EA_PREFIX + "ORD_ENTRY_" + id, + setupTime, entryPrice, + currentBar, entryPrice, + entryClr, STYLE_SOLID, 2 + ); + + // SL line + DrawTrendLine( + EA_PREFIX + "ORD_SL_" + id, + setupTime, slPrice, + currentBar, slPrice, + slClr, STYLE_DASH, 1 + ); + + // TP line + DrawTrendLine( + EA_PREFIX + "ORD_TP_" + id, + setupTime, tpPrice, + currentBar, tpPrice, + tpClr, STYLE_DASH, 1 + ); + + // Label near entry + string labelText = StringFormat("%s LIMIT %.2f | Vol %.2f | RR %.1f", + side, entryPrice, volume, InpRRRatio); + + DrawTextOnChart( + EA_PREFIX + "ORD_LBL_" + id, + setupTime, + entryPrice, + labelText, + entryClr, + 8, + ANCHOR_LEFT_LOWER + ); + } +} //+------------------------------------------------------------------+ //| Draw all active FVG zones as rectangles on the chart | //+------------------------------------------------------------------+ @@ -87,14 +163,14 @@ void DrawFVGZones() for(int i = 0; i < g_FVGCount; i++) { - if(!g_FVGZones[i].isActive) + if(IsZoneExpired(g_FVGZones[i])) continue; string zoneId = IntegerToString(i); - //--- Zone color depends on type and mitigation status --- + //--- Zone color depends on type and FVG status --- color zoneColor; - if(g_FVGZones[i].isMitigated) + if(IsZoneMitigated(g_FVGZones[i])) zoneColor = InpColorMitigatedFVG; else if(g_FVGZones[i].type == FVG_BULLISH) zoneColor = InpColorBullFVG; @@ -111,7 +187,7 @@ void DrawFVGZones() //--- Draw mid-line (50% of FVG) --- double midPrice = (g_FVGZones[i].upperEdge + g_FVGZones[i].lowerEdge) / 2.0; - color midColor = g_FVGZones[i].isMitigated ? clrDimGray : clrGold; + color midColor = IsZoneMitigated(g_FVGZones[i]) ? clrDimGray : clrGold; DrawTrendLine( EA_PREFIX + "FVG_MID_" + zoneId, @@ -125,11 +201,17 @@ void DrawFVGZones() { string typeStr = (g_FVGZones[i].type == FVG_BULLISH) ? "BULL" : "BEAR"; string arrow = (g_FVGZones[i].type == FVG_BULLISH) ? "▲" : "▼"; - string mitigStr = g_FVGZones[i].isMitigated ? " [MITIGATED]" : ""; + string stateStr = ""; + if(IsZoneTouched(g_FVGZones[i])) + stateStr = " [TOUCHED]"; + else if(IsZoneMitigated(g_FVGZones[i])) + stateStr = " [MITIGATED]"; color labelColor; - if(g_FVGZones[i].isMitigated) + if(IsZoneMitigated(g_FVGZones[i])) labelColor = clrDimGray; + else if(IsZoneTouched(g_FVGZones[i])) + labelColor = clrYellow; else if(g_FVGZones[i].type == FVG_BULLISH) labelColor = clrDeepSkyBlue; else @@ -142,7 +224,7 @@ void DrawFVGZones() StringFormat("%s %s [%.2f–%.2f]%s", arrow, typeStr, g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge, - mitigStr), + stateStr), labelColor, 7, ANCHOR_LEFT_LOWER ); } @@ -190,6 +272,7 @@ void DrawAll() { DrawFVGZones(); DrawInfoPanel(); + DrawLimitOrders(); ChartRedraw(0); } diff --git a/Experts/SimpleFVG/FVG.mqh b/Experts/SimpleFVG/FVG.mqh index 185f14a..62d7469 100644 --- a/Experts/SimpleFVG/FVG.mqh +++ b/Experts/SimpleFVG/FVG.mqh @@ -22,13 +22,13 @@ //+------------------------------------------------------------------+ struct FVGZone { - bool isActive; - ENUM_FVG_TYPE type; - double upperEdge; - double lowerEdge; - datetime createdTime; - int ageInBars; - bool isMitigated; + ENUM_FVG_TYPE type; + ENUM_FVG_STATUS status; + double upperEdge; + double lowerEdge; + double slReferencePrice; + datetime createdTime; + int ageInBars; }; //+------------------------------------------------------------------+ @@ -49,8 +49,34 @@ void FVGInit() } //+------------------------------------------------------------------+ -//| Check if impulse candle has strong body (body/range >= threshold) | +//| Helper: status helpers & impulse strength | //+------------------------------------------------------------------+ +bool IsZoneActive(const FVGZone &z) +{ + return (z.status != EXPIRED); +} + +bool IsZoneLive(const FVGZone &z) +{ + return (z.status == ACTIVE || z.status == TOUCHED); +} + +bool IsZoneMitigated(const FVGZone &z) +{ + return (z.status == MITIGATED); +} + +bool IsZoneTouched(const FVGZone &z) +{ + return (z.status == TOUCHED); +} + +bool IsZoneExpired(const FVGZone &z) +{ + return (z.status == EXPIRED); +} + +// Impulse candle strength: body/range >= threshold bool IsImpulseCandleStrong(string symbol, ENUM_TIMEFRAMES tf, int shift) { double high = iHigh(symbol, tf, shift); @@ -69,13 +95,13 @@ bool IsImpulseCandleStrong(string symbol, ENUM_TIMEFRAMES tf, int shift) } //+------------------------------------------------------------------+ -//| Check if this FVG timestamp already exists in our array | +//| Check if this FVG timestamp already exists in our array | //+------------------------------------------------------------------+ bool FVGAlreadyTracked(datetime fvgTime, ENUM_FVG_TYPE fvgType) { for(int i = 0; i < g_FVGCount; i++) { - if(g_FVGZones[i].isActive + if(IsZoneActive(g_FVGZones[i]) && g_FVGZones[i].createdTime == fvgTime && g_FVGZones[i].type == fvgType) return true; @@ -84,9 +110,13 @@ bool FVGAlreadyTracked(datetime fvgTime, ENUM_FVG_TYPE fvgType) } //+------------------------------------------------------------------+ -//| Add a new FVG zone to the array | +//| Add a new FVG zone to the array | //+------------------------------------------------------------------+ -bool AddFVGZone(ENUM_FVG_TYPE type, double upper, double lower, datetime time) +bool AddFVGZone(ENUM_FVG_TYPE type, + double upper, + double lower, + double slReference, + datetime time) { if(g_FVGCount >= MAX_FVG_SLOTS) return false; @@ -99,13 +129,13 @@ bool AddFVGZone(ENUM_FVG_TYPE type, double upper, double lower, datetime time) } ZeroMemory(g_FVGZones[g_FVGCount]); - g_FVGZones[g_FVGCount].isActive = true; - g_FVGZones[g_FVGCount].type = type; - g_FVGZones[g_FVGCount].upperEdge = upper; - g_FVGZones[g_FVGCount].lowerEdge = lower; - g_FVGZones[g_FVGCount].createdTime = time; - g_FVGZones[g_FVGCount].ageInBars = 0; - g_FVGZones[g_FVGCount].isMitigated = false; + g_FVGZones[g_FVGCount].type = type; + g_FVGZones[g_FVGCount].status = ACTIVE; + g_FVGZones[g_FVGCount].upperEdge = upper; + g_FVGZones[g_FVGCount].lowerEdge = lower; + g_FVGZones[g_FVGCount].slReferencePrice = slReference; + g_FVGZones[g_FVGCount].createdTime = time; + g_FVGZones[g_FVGCount].ageInBars = 0; g_FVGCount++; if(InpDebugLog) @@ -148,7 +178,10 @@ void ScanForNewFVGs() && IsImpulseCandleStrong(symbol, InpTimeframe, shiftB)) { if(!FVGAlreadyTracked(fvgTime, FVG_BULLISH)) - AddFVGZone(FVG_BULLISH, candleC_Low, candleA_High, fvgTime); + { + double slRef = iLow(symbol, InpTimeframe, shiftB); // bar2.low + AddFVGZone(FVG_BULLISH, candleC_Low, candleA_High, slRef, fvgTime); + } } //--- Bearish FVG: gap between C.High and A.Low --- @@ -157,7 +190,10 @@ void ScanForNewFVGs() && IsImpulseCandleStrong(symbol, InpTimeframe, shiftB)) { if(!FVGAlreadyTracked(fvgTime, FVG_BEARISH)) - AddFVGZone(FVG_BEARISH, candleA_Low, candleC_High, fvgTime); + { + double slRef = iHigh(symbol, InpTimeframe, shiftB); // bar2.high + AddFVGZone(FVG_BEARISH, candleA_Low, candleC_High, slRef, fvgTime); + } } } } @@ -171,28 +207,58 @@ void CheckMitigationStatus() for(int i = 0; i < g_FVGCount; i++) { - if(!g_FVGZones[i].isActive || g_FVGZones[i].isMitigated) + if(IsZoneMitigated(g_FVGZones[i]) || !IsZoneActive(g_FVGZones[i])) continue; double lastLow = iLow (symbol, InpTimeframe, 1); double lastHigh = iHigh(symbol, InpTimeframe, 1); - // Bullish FVG mitigated: price drops through lower edge - if(g_FVGZones[i].type == FVG_BULLISH && lastLow <= g_FVGZones[i].lowerEdge) + double zoneHeight = g_FVGZones[i].upperEdge - g_FVGZones[i].lowerEdge; + if(zoneHeight <= 0) + continue; + + //--- Bullish FVG: price comes down into the gap --- + if(g_FVGZones[i].type == FVG_BULLISH) { - g_FVGZones[i].isMitigated = true; - if(InpDebugLog) - PrintFormat("[FVG] MITIGATED BULL [%.5f – %.5f]", - g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge); + // Mitigated: price drops through lower edge + if(lastLow <= g_FVGZones[i].lowerEdge) + { + g_FVGZones[i].status = MITIGATED; + if(InpDebugLog) + PrintFormat("[FVG] MITIGATED BULL [%.5f – %.5f]", + g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge); + continue; + } + + // Touched: price has filled at least X% of gap, but not fully + double deepestPrice = MathMin(lastLow, g_FVGZones[i].upperEdge); + deepestPrice = MathMax(deepestPrice, g_FVGZones[i].lowerEdge); + double fillPct = (g_FVGZones[i].upperEdge - deepestPrice) / zoneHeight * 100.0; + + if(g_FVGZones[i].status == ACTIVE && fillPct >= InpFVGTouchedPercent) + g_FVGZones[i].status = TOUCHED; } - // Bearish FVG mitigated: price rises through upper edge - if(g_FVGZones[i].type == FVG_BEARISH && lastHigh >= g_FVGZones[i].upperEdge) + //--- Bearish FVG: price goes up into the gap --- + if(g_FVGZones[i].type == FVG_BEARISH) { - g_FVGZones[i].isMitigated = true; - if(InpDebugLog) - PrintFormat("[FVG] MITIGATED BEAR [%.5f – %.5f]", - g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge); + // Mitigated: price rises through upper edge + if(lastHigh >= g_FVGZones[i].upperEdge) + { + g_FVGZones[i].status = MITIGATED; + if(InpDebugLog) + PrintFormat("[FVG] MITIGATED BEAR [%.5f – %.5f]", + g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge); + continue; + } + + // Touched: price has filled at least X% of gap, but not fully + double highestPrice = MathMax(lastHigh, g_FVGZones[i].lowerEdge); + highestPrice = MathMin(highestPrice, g_FVGZones[i].upperEdge); + double fillPct = (highestPrice - g_FVGZones[i].lowerEdge) / zoneHeight * 100.0; + + if(g_FVGZones[i].status == ACTIVE && fillPct >= InpFVGTouchedPercent) + g_FVGZones[i].status = TOUCHED; } } } @@ -204,13 +270,13 @@ void ExpireOldFVGs() { for(int i = 0; i < g_FVGCount; i++) { - if(!g_FVGZones[i].isActive) continue; + if(!IsZoneActive(g_FVGZones[i])) continue; g_FVGZones[i].ageInBars++; if(g_FVGZones[i].ageInBars > InpFVGMaxAgeBars) { - g_FVGZones[i].isActive = false; + g_FVGZones[i].status = EXPIRED; if(InpDebugLog) PrintFormat("[FVG] EXPIRED %s [%.5f – %.5f] age=%d bars", (g_FVGZones[i].type == FVG_BULLISH) ? "BULL" : "BEAR", @@ -228,7 +294,7 @@ void CompactFVGArray() int writeIndex = 0; for(int i = 0; i < g_FVGCount; i++) { - if(g_FVGZones[i].isActive) + if(IsZoneActive(g_FVGZones[i])) { if(writeIndex != i) g_FVGZones[writeIndex] = g_FVGZones[i]; @@ -257,7 +323,7 @@ int CountActiveFVGs(ENUM_FVG_TYPE type) int count = 0; for(int i = 0; i < g_FVGCount; i++) { - if(g_FVGZones[i].isActive && g_FVGZones[i].type == type) + if(IsZoneActive(g_FVGZones[i]) && g_FVGZones[i].type == type) count++; } return count; @@ -268,7 +334,7 @@ int CountMitigatedFVGs() int count = 0; for(int i = 0; i < g_FVGCount; i++) { - if(g_FVGZones[i].isActive && g_FVGZones[i].isMitigated) + if(IsZoneMitigated(g_FVGZones[i])) count++; } return count; diff --git a/Experts/SimpleFVG/Trade.mqh b/Experts/SimpleFVG/Trade.mqh new file mode 100644 index 0000000..930aa80 --- /dev/null +++ b/Experts/SimpleFVG/Trade.mqh @@ -0,0 +1,315 @@ +//+------------------------------------------------------------------+ +//| Trade.mqh – Step 3: Trade management for FVG strategy | +//+------------------------------------------------------------------+ +#ifndef __SIMPLE_FVG_TRADE_MQH__ +#define __SIMPLE_FVG_TRADE_MQH__ + +#include "Config.mqh" +#include "Trend.mqh" +#include "FVG.mqh" + +//--- Trading config nằm trong Config.mqh (Step 3) + +//+------------------------------------------------------------------+ +//| Internal helpers | +//+------------------------------------------------------------------+ +int CountOurPositions() +{ + int count = 0; + for(int i = 0; i < PositionsTotal(); i++) + { + ulong ticket = PositionGetTicket(i); + if(ticket == 0) continue; + if(!PositionSelectByTicket(ticket)) continue; + + if((long)PositionGetInteger(POSITION_MAGIC) == InpEAMagic && + (string)PositionGetString(POSITION_SYMBOL) == GetTradeSymbol()) + { + count++; + } + } + return count; +} + +int CountOurLimitOrders() +{ + int count = 0; + for(int i = 0; i < OrdersTotal(); i++) + { + ulong ticket = OrderGetTicket(i); + if(ticket == 0) continue; + if(!OrderSelect(ticket)) continue; + + ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); + long magic = (long)OrderGetInteger(ORDER_MAGIC); + string symbol = (string)OrderGetString(ORDER_SYMBOL); + + if(magic == InpEAMagic && + symbol == GetTradeSymbol() && + (type == ORDER_TYPE_BUY_LIMIT || type == ORDER_TYPE_SELL_LIMIT)) + { + count++; + } + } + return count; +} + +void CancelAllOurLimitOrders() +{ + for(int i = OrdersTotal() - 1; i >= 0; i--) + { + ulong ticket = OrderGetTicket(i); + if(ticket == 0) continue; + if(!OrderSelect(ticket)) continue; + + ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); + long magic = (long)OrderGetInteger(ORDER_MAGIC); + string symbol = (string)OrderGetString(ORDER_SYMBOL); + + if(magic == InpEAMagic && + symbol == GetTradeSymbol() && + (type == ORDER_TYPE_BUY_LIMIT || type == ORDER_TYPE_SELL_LIMIT)) + { + MqlTradeRequest req; + MqlTradeResult res; + ZeroMemory(req); + ZeroMemory(res); + + req.action = TRADE_ACTION_REMOVE; + req.order = ticket; + + if(!OrderSend(req, res) && InpDebugLog) + PrintFormat("[Trade] Failed to cancel order #%I64u, retcode=%d", + ticket, res.retcode); + } + } +} + +bool HasLimitOrderAtPrice(ENUM_ORDER_TYPE orderType, double entryPrice) +{ + double eps = 2 * _Point; + for(int i = 0; i < OrdersTotal(); i++) + { + ulong ticket = OrderGetTicket(i); + if(ticket == 0) continue; + if(!OrderSelect(ticket)) continue; + + if((long)OrderGetInteger(ORDER_MAGIC) != InpEAMagic) continue; + if((string)OrderGetString(ORDER_SYMBOL) != GetTradeSymbol()) continue; + + ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); + if(type != orderType) continue; + + double price = OrderGetDouble(ORDER_PRICE_OPEN); + if(MathAbs(price - entryPrice) <= eps) + return true; + } + return false; +} + +// Hủy các limit order quá "già" (tính theo số bar trên InpTimeframe) +void CancelStaleLimitOrders() +{ + if(InpLimitMaxAgeBars <= 0) + return; + + datetime now = TimeCurrent(); + if(now == 0) + return; + + int tfSeconds = PeriodSeconds(InpTimeframe); + if(tfSeconds <= 0) + return; + + int maxAgeSeconds = InpLimitMaxAgeBars * tfSeconds; + + for(int i = OrdersTotal() - 1; i >= 0; i--) + { + ulong ticket = OrderGetTicket(i); + if(ticket == 0) continue; + if(!OrderSelect(ticket)) continue; + + ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); + long magic = (long)OrderGetInteger(ORDER_MAGIC); + string symbol = (string)OrderGetString(ORDER_SYMBOL); + + if(magic != InpEAMagic) continue; + if(symbol != GetTradeSymbol()) continue; + if(type != ORDER_TYPE_BUY_LIMIT && type != ORDER_TYPE_SELL_LIMIT) + continue; + + datetime setupTime = (datetime)OrderGetInteger(ORDER_TIME_SETUP); + int ageSeconds = int(now - setupTime); + if(ageSeconds < maxAgeSeconds) + continue; + + MqlTradeRequest req; + MqlTradeResult res; + ZeroMemory(req); + ZeroMemory(res); + + req.action = TRADE_ACTION_REMOVE; + req.order = ticket; + + if(!OrderSend(req, res) && InpDebugLog) + PrintFormat("[Trade] Failed to cancel stale order #%I64u, ageBars=%d, retcode=%d", + ticket, ageSeconds / tfSeconds, res.retcode); + } +} + +// Tính khối lượng lot sao cho 1R = InpRiskPercentPerR % balance +double CalculateRiskLotSize(double entryPrice, double slPrice) +{ + string symbol = GetTradeSymbol(); + + double balance = AccountInfoDouble(ACCOUNT_BALANCE); + double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); + double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); + double volMin = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); + double volMax = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); + double volStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); + + double priceDiff = MathAbs(entryPrice - slPrice); + if(balance <= 0 || tickSize <= 0 || tickValue <= 0 || priceDiff <= 0) + return 0.0; + + double riskMoney = balance * InpRiskPercentPerR / 100.0; + if(riskMoney <= 0.0) + return 0.0; + + double ticks = priceDiff / tickSize; + double costPerLot = ticks * tickValue; // tiền lỗ nếu 1 lot hit SL + if(costPerLot <= 0.0) + return 0.0; + + double rawVolume = riskMoney / costPerLot; + + // Làm tròn đến 2 chữ số thập phân + double rounded2 = MathFloor(rawVolume * 100.0 + 0.5) / 100.0; + + // Canh theo step volume + if(volStep > 0.0) + rounded2 = MathFloor(rounded2 / volStep) * volStep; + + // Giới hạn theo min/max + if(rounded2 < volMin) + rounded2 = volMin; + if(rounded2 > volMax) + rounded2 = volMax; + + return rounded2; +} + +bool PlaceLimitForZone(const FVGZone &zone) +{ + string symbol = GetTradeSymbol(); + + double entryPrice; + double slPrice; + double tpPrice; + ENUM_ORDER_TYPE orderType; + + double zoneHeight = zone.upperEdge - zone.lowerEdge; + if(zoneHeight <= 0.0) + return false; + + double touchRatio = InpFVGTouchedPercent / 100.0; + + if(zone.type == FVG_BULLISH) + { + // Entry: 40% từ đỉnh vùng xuống + entryPrice = zone.upperEdge - zoneHeight * touchRatio; + slPrice = zone.slReferencePrice; + + double risk = entryPrice - slPrice; + if(risk <= 0) return false; + tpPrice = entryPrice + risk * InpRRRatio; + orderType = ORDER_TYPE_BUY_LIMIT; + } + else // FVG_BEARISH + { + // Entry: 40% từ đáy vùng lên + entryPrice = zone.lowerEdge + zoneHeight * touchRatio; + slPrice = zone.slReferencePrice; + + double risk = slPrice - entryPrice; + if(risk <= 0) return false; + tpPrice = entryPrice - risk * InpRRRatio; + orderType = ORDER_TYPE_SELL_LIMIT; + } + + // Không tạo lệnh nếu đã có limit trùng giá/type + if(HasLimitOrderAtPrice(orderType, entryPrice)) + return false; + + double volume = CalculateRiskLotSize(entryPrice, slPrice); + if(volume <= 0.0) + return false; + + MqlTradeRequest req; + MqlTradeResult res; + ZeroMemory(req); + ZeroMemory(res); + + req.action = TRADE_ACTION_PENDING; + req.symbol = symbol; + req.magic = InpEAMagic; + req.type = orderType; + req.volume = volume; + req.price = entryPrice; + req.sl = slPrice; + req.tp = tpPrice; + req.type_filling = ORDER_FILLING_RETURN; + + req.deviation = 10; + req.comment = "SimpleFVG"; + + if(!OrderSend(req, res)) + return false; + + return (res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED); +} + +//+------------------------------------------------------------------+ +//| Public: main trade manager | +//+------------------------------------------------------------------+ +void ManageFVGTrades() +{ + if(!InpTradeEnabled) + return; + + // Hủy các lệnh limit đã quá số bar cho phép + CancelStaleLimitOrders(); + + // Nếu đã có vị thế, hủy toàn bộ limit còn lại + if(CountOurPositions() > 0) + { + CancelAllOurLimitOrders(); + return; + } + + int currentLimits = CountOurLimitOrders(); + if(currentLimits >= InpMaxLimitOrders) + return; + + // Chỉ đặt lệnh limit khi giá đã chạm FVG (TOUCHED), không đặt khi mới ACTIVE + ENUM_TREND_DIRECTION trend = g_CurrentTrend; + + for(int i = g_FVGCount - 1; i >= 0 && currentLimits < InpMaxLimitOrders; i--) + { + FVGZone zone = g_FVGZones[i]; + if(!IsZoneTouched(zone)) + continue; + + if(zone.type == FVG_BULLISH && trend != TREND_BULLISH) + continue; + if(zone.type == FVG_BEARISH && trend != TREND_BEARISH) + continue; + + if(PlaceLimitForZone(zone)) + currentLimits++; + } +} + +#endif +