Đặt lệnh limit khi giá touch fvg

This commit is contained in:
Bell
2026-03-11 18:13:27 +07:00
parent af4b79ade8
commit 61bcedb4f1
5 changed files with 545 additions and 62 deletions
+2
View File
@@ -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();
}
+33 -16
View File
@@ -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 |
//+------------------------------------------------------------------+
+90 -7
View File
@@ -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);
}
+105 -39
View File
@@ -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;
+315
View File
@@ -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