2026-03-11 18:13:27 +07:00
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
//| 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 |
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-05-09 23:16:50 +07:00
|
|
|
|
string FVGTypeToString(ENUM_FVG_TYPE type)
|
|
|
|
|
|
{
|
|
|
|
|
|
return (type == FVG_BULLISH) ? "BULL" : "BEAR";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void LogDecisionTrace(const string stage, const string message)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(!InpDebugLog)
|
|
|
|
|
|
return;
|
|
|
|
|
|
PrintFormat("[TRACE][%s] %s", stage, message);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool IsSpreadAcceptable(string symbol)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(InpMaxSpreadPoints <= 0)
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
|
|
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
|
|
|
|
double spreadPoints = -1.0;
|
|
|
|
|
|
if(ask > 0.0 && bid > 0.0)
|
|
|
|
|
|
spreadPoints = (ask - bid) / _Point;
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// In Strategy Tester, BID/ASK can be unavailable on some bars.
|
|
|
|
|
|
// Fallback to broker/tester spread setting (already in points).
|
|
|
|
|
|
long spreadInt = SymbolInfoInteger(symbol, SYMBOL_SPREAD);
|
|
|
|
|
|
if(spreadInt > 0)
|
|
|
|
|
|
spreadPoints = (double)spreadInt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if(spreadPoints < 0.0)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
return (spreadPoints <= InpMaxSpreadPoints);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
double GetCurrentSpreadPoints(string symbol)
|
|
|
|
|
|
{
|
|
|
|
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
|
|
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
|
|
|
|
if(ask > 0.0 && bid > 0.0)
|
|
|
|
|
|
return (ask - bid) / _Point;
|
|
|
|
|
|
|
|
|
|
|
|
long spreadInt = SymbolInfoInteger(symbol, SYMBOL_SPREAD);
|
|
|
|
|
|
if(spreadInt > 0)
|
|
|
|
|
|
return (double)spreadInt;
|
|
|
|
|
|
|
|
|
|
|
|
return -1.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool IsTradingSessionOpen()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(!InpUseSessionFilter)
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
|
|
datetime now = TimeCurrent();
|
|
|
|
|
|
if(now == 0)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
MqlDateTime dt;
|
|
|
|
|
|
TimeToStruct(now, dt);
|
|
|
|
|
|
int hour = dt.hour;
|
|
|
|
|
|
|
|
|
|
|
|
int startHour = MathMax(0, MathMin(23, InpSessionStartHour));
|
|
|
|
|
|
int endHour = MathMax(0, MathMin(23, InpSessionEndHour));
|
|
|
|
|
|
|
|
|
|
|
|
if(startHour == endHour)
|
|
|
|
|
|
return true;
|
|
|
|
|
|
if(startHour < endHour)
|
|
|
|
|
|
return (hour >= startHour && hour < endHour);
|
|
|
|
|
|
return (hour >= startHour || hour < endHour); // overnight session
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int GetCurrentServerHour()
|
|
|
|
|
|
{
|
|
|
|
|
|
datetime now = TimeCurrent();
|
|
|
|
|
|
if(now == 0)
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
|
|
|
|
MqlDateTime dt;
|
|
|
|
|
|
TimeToStruct(now, dt);
|
|
|
|
|
|
return dt.hour;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GetNormalizedSessionHours(int &outStartHour, int &outEndHour)
|
|
|
|
|
|
{
|
|
|
|
|
|
outStartHour = MathMax(0, MathMin(23, InpSessionStartHour));
|
|
|
|
|
|
outEndHour = MathMax(0, MathMin(23, InpSessionEndHour));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
double GetATRPoints(string symbol, ENUM_TIMEFRAMES tf, int period)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(period <= 1)
|
|
|
|
|
|
return -1.0;
|
|
|
|
|
|
|
|
|
|
|
|
int handle = iATR(symbol, tf, period);
|
|
|
|
|
|
if(handle == INVALID_HANDLE)
|
|
|
|
|
|
return -1.0;
|
|
|
|
|
|
|
|
|
|
|
|
double buffer[1];
|
|
|
|
|
|
int copied = CopyBuffer(handle, 0, 1, 1, buffer);
|
|
|
|
|
|
IndicatorRelease(handle);
|
|
|
|
|
|
if(copied < 1 || buffer[0] <= 0.0)
|
|
|
|
|
|
return -1.0;
|
|
|
|
|
|
|
|
|
|
|
|
return buffer[0] / _Point;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool IsATRRegimeValid(string symbol)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(!InpUseATRFilter)
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
|
|
double atrPoints = GetATRPoints(symbol, InpTimeframe, InpATRPeriod);
|
|
|
|
|
|
if(atrPoints <= 0.0)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
if(InpMinATRPoints > 0.0 && atrPoints < InpMinATRPoints)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
if(InpMaxATRPoints > 0.0 && atrPoints > InpMaxATRPoints)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
double GetCurrentATRPoints(string symbol)
|
|
|
|
|
|
{
|
|
|
|
|
|
return GetATRPoints(symbol, InpTimeframe, InpATRPeriod);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 18:13:27 +07:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 18:25:18 +07:00
|
|
|
|
void GetTradeStats(int &totalTrades, int &tpTrades, int &slTrades)
|
|
|
|
|
|
{
|
|
|
|
|
|
totalTrades = 0;
|
|
|
|
|
|
tpTrades = 0;
|
|
|
|
|
|
slTrades = 0;
|
|
|
|
|
|
|
|
|
|
|
|
datetime now = TimeCurrent();
|
|
|
|
|
|
if(now == 0)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
if(!HistorySelect(0, now))
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
int deals = HistoryDealsTotal();
|
|
|
|
|
|
string symbol = GetTradeSymbol();
|
|
|
|
|
|
|
|
|
|
|
|
for(int i = 0; i < deals; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
ulong dealTicket = HistoryDealGetTicket(i);
|
|
|
|
|
|
if(dealTicket == 0)
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
string dealSymbol = (string)HistoryDealGetString(dealTicket, DEAL_SYMBOL);
|
|
|
|
|
|
long dealMagic = (long)HistoryDealGetInteger(dealTicket, DEAL_MAGIC);
|
|
|
|
|
|
if(dealSymbol != symbol || dealMagic != InpEAMagic)
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(dealTicket, DEAL_ENTRY);
|
|
|
|
|
|
if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_INOUT)
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
totalTrades++;
|
|
|
|
|
|
|
|
|
|
|
|
ENUM_DEAL_REASON reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(dealTicket, DEAL_REASON);
|
|
|
|
|
|
if(reason == DEAL_REASON_TP)
|
|
|
|
|
|
tpTrades++;
|
|
|
|
|
|
else if(reason == DEAL_REASON_SL)
|
|
|
|
|
|
slTrades++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 18:13:27 +07:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 21:22:25 +07:00
|
|
|
|
// Đặt lệnh limit theo entry/SL từ low TF FVG (sau khi có tín hiệu xác nhận)
|
|
|
|
|
|
bool PlaceLimitFromLowTF(string symbol, ENUM_FVG_TYPE type, double entryPrice, double slPrice)
|
|
|
|
|
|
{
|
|
|
|
|
|
double tpPrice;
|
|
|
|
|
|
ENUM_ORDER_TYPE orderType;
|
|
|
|
|
|
|
|
|
|
|
|
if(type == FVG_BULLISH)
|
|
|
|
|
|
{
|
|
|
|
|
|
double risk = entryPrice - slPrice;
|
|
|
|
|
|
if(risk <= 0.0) return false;
|
|
|
|
|
|
tpPrice = entryPrice + risk * InpRRRatio;
|
|
|
|
|
|
orderType = ORDER_TYPE_BUY_LIMIT;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
double risk = slPrice - entryPrice;
|
|
|
|
|
|
if(risk <= 0.0) return false;
|
|
|
|
|
|
tpPrice = entryPrice - risk * InpRRRatio;
|
|
|
|
|
|
orderType = ORDER_TYPE_SELL_LIMIT;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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_LTF";
|
|
|
|
|
|
|
|
|
|
|
|
if(!OrderSend(req, res))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
return (res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 18:13:27 +07:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-03-11 19:59:42 +07:00
|
|
|
|
// Entry: 35% từ đỉnh vùng xuống
|
2026-03-11 18:13:27 +07:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-03-11 19:59:42 +07:00
|
|
|
|
// Entry: 35% từ đáy vùng lên
|
2026-03-11 18:13:27 +07:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-05-09 23:16:50 +07:00
|
|
|
|
string symbol = GetTradeSymbol();
|
|
|
|
|
|
if(!IsSpreadAcceptable(symbol))
|
|
|
|
|
|
{
|
|
|
|
|
|
double spreadPts = GetCurrentSpreadPoints(symbol);
|
|
|
|
|
|
LogDecisionTrace("FILTER", StringFormat("Skip entries: spread %.1f > max %d points",
|
|
|
|
|
|
spreadPts, InpMaxSpreadPoints));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(!IsTradingSessionOpen())
|
|
|
|
|
|
{
|
|
|
|
|
|
int startHour, endHour;
|
|
|
|
|
|
GetNormalizedSessionHours(startHour, endHour);
|
|
|
|
|
|
int currentHour = GetCurrentServerHour();
|
|
|
|
|
|
LogDecisionTrace("FILTER", StringFormat("Skip entries: session closed (hour=%d, window=%02d-%02d, useSession=%s)",
|
|
|
|
|
|
currentHour, startHour, endHour,
|
|
|
|
|
|
InpUseSessionFilter ? "true" : "false"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(!IsATRRegimeValid(symbol))
|
|
|
|
|
|
{
|
|
|
|
|
|
double atrPoints = GetCurrentATRPoints(symbol);
|
|
|
|
|
|
LogDecisionTrace("FILTER", StringFormat("Skip entries: ATR %.1f outside [%.1f..%.1f] points (period=%d, useATR=%s)",
|
|
|
|
|
|
atrPoints, InpMinATRPoints, InpMaxATRPoints,
|
|
|
|
|
|
InpATRPeriod, InpUseATRFilter ? "true" : "false"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 21:12:47 +07:00
|
|
|
|
// Chỉ tìm tín hiệu low TF FVG khi high TF FVG đã TOUCHED (giá lấp đủ %), không trigger khi mới chạm cạnh
|
2026-03-11 18:13:27 +07:00
|
|
|
|
ENUM_TREND_DIRECTION trend = g_CurrentTrend;
|
2026-03-11 21:22:25 +07:00
|
|
|
|
ENUM_TIMEFRAMES lowTF = GetConfirmationTimeframe(InpTimeframe);
|
|
|
|
|
|
const int LOW_TF_FVG_LOOKBACK = 15;
|
|
|
|
|
|
|
2026-03-11 18:13:27 +07:00
|
|
|
|
for(int i = g_FVGCount - 1; i >= 0 && currentLimits < InpMaxLimitOrders; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
FVGZone zone = g_FVGZones[i];
|
2026-03-11 18:25:18 +07:00
|
|
|
|
if(!IsZoneActive(zone) || IsZoneMitigated(zone))
|
2026-05-09 23:16:50 +07:00
|
|
|
|
{
|
|
|
|
|
|
LogDecisionTrace("ZONE", StringFormat("#%d skip: inactive/mitigated", i));
|
2026-03-11 18:13:27 +07:00
|
|
|
|
continue;
|
2026-05-09 23:16:50 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if(zone.tradeLocked)
|
|
|
|
|
|
{
|
|
|
|
|
|
LogDecisionTrace("ZONE", StringFormat("#%d %s skip: locked", i, FVGTypeToString(zone.type)));
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-03-11 18:13:27 +07:00
|
|
|
|
|
|
|
|
|
|
if(zone.type == FVG_BULLISH && trend != TREND_BULLISH)
|
2026-05-09 23:16:50 +07:00
|
|
|
|
{
|
|
|
|
|
|
LogDecisionTrace("ZONE", StringFormat("#%d BULL blocked by EMA trend", i));
|
2026-03-11 18:13:27 +07:00
|
|
|
|
continue;
|
2026-05-09 23:16:50 +07:00
|
|
|
|
}
|
2026-03-11 18:13:27 +07:00
|
|
|
|
if(zone.type == FVG_BEARISH && trend != TREND_BEARISH)
|
2026-05-09 23:16:50 +07:00
|
|
|
|
{
|
|
|
|
|
|
LogDecisionTrace("ZONE", StringFormat("#%d BEAR blocked by EMA trend", i));
|
2026-03-11 18:13:27 +07:00
|
|
|
|
continue;
|
2026-05-09 23:16:50 +07:00
|
|
|
|
}
|
2026-03-11 18:13:27 +07:00
|
|
|
|
|
2026-03-19 21:12:47 +07:00
|
|
|
|
// Điều kiện vào lệnh: FVG high TF phải đã TOUCHED (giá lấp >= InpFVGTouchedPercent), không chỉ chạm cạnh
|
|
|
|
|
|
if(!IsZoneTouched(zone))
|
2026-05-09 23:16:50 +07:00
|
|
|
|
{
|
|
|
|
|
|
LogDecisionTrace("ZONE", StringFormat("#%d %s not touched", i, FVGTypeToString(zone.type)));
|
2026-03-11 18:25:18 +07:00
|
|
|
|
continue;
|
2026-05-09 23:16:50 +07:00
|
|
|
|
}
|
2026-03-11 18:25:18 +07:00
|
|
|
|
|
2026-03-11 21:22:25 +07:00
|
|
|
|
// Chỉ đặt lệnh khi có low TF để xác nhận (H1->M5, H4->M15, M15->M2)
|
|
|
|
|
|
if(lowTF == InpTimeframe)
|
2026-05-09 23:16:50 +07:00
|
|
|
|
{
|
|
|
|
|
|
LogDecisionTrace("ZONE", StringFormat("#%d skip: lowTF mapping unavailable", i));
|
2026-03-11 21:22:25 +07:00
|
|
|
|
continue;
|
2026-05-09 23:16:50 +07:00
|
|
|
|
}
|
2026-03-11 21:22:25 +07:00
|
|
|
|
|
|
|
|
|
|
double ltfUpper, ltfLower, ltfBarALow, ltfBarAHigh;
|
2026-05-09 23:16:50 +07:00
|
|
|
|
if(!GetLatestLowTFFVGInRange(symbol, lowTF, zone.type, LOW_TF_FVG_LOOKBACK,
|
|
|
|
|
|
zone.lowerEdge, zone.upperEdge,
|
|
|
|
|
|
InpLowTFEntryRangeBufferPoints,
|
|
|
|
|
|
ltfUpper, ltfLower, ltfBarALow, ltfBarAHigh))
|
|
|
|
|
|
{
|
|
|
|
|
|
LogDecisionTrace("ZONE", StringFormat("#%d %s no LTF FVG aligned to HTF zone [%.5f..%.5f]",
|
|
|
|
|
|
i, FVGTypeToString(zone.type), zone.lowerEdge, zone.upperEdge));
|
2026-03-11 21:22:25 +07:00
|
|
|
|
continue;
|
2026-05-09 23:16:50 +07:00
|
|
|
|
}
|
2026-03-11 21:22:25 +07:00
|
|
|
|
|
2026-03-11 23:42:37 +07:00
|
|
|
|
// Entry theo low TF FVG; SL = bar B của high TF FVG
|
2026-03-11 21:22:25 +07:00
|
|
|
|
double entryPrice, slPrice;
|
|
|
|
|
|
if(zone.type == FVG_BULLISH)
|
|
|
|
|
|
{
|
|
|
|
|
|
entryPrice = ltfUpper; // Buy limit tại cạnh trên low TF FVG
|
2026-03-11 23:42:37 +07:00
|
|
|
|
slPrice = zone.slReferencePrice; // SL dưới bar B high TF
|
2026-03-11 21:22:25 +07:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
entryPrice = ltfLower; // Sell limit tại cạnh dưới low TF FVG
|
2026-03-11 23:42:37 +07:00
|
|
|
|
slPrice = zone.slReferencePrice; // SL trên bar B high TF
|
2026-03-11 21:22:25 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if(PlaceLimitFromLowTF(symbol, zone.type, entryPrice, slPrice))
|
2026-03-19 21:12:47 +07:00
|
|
|
|
{
|
2026-05-09 23:16:50 +07:00
|
|
|
|
// v2 state: lock zone after first successful placement; keep visible for diagnostics.
|
|
|
|
|
|
g_FVGZones[i].tradeLocked = true;
|
|
|
|
|
|
g_FVGZones[i].tradeLockedTime = TimeCurrent();
|
|
|
|
|
|
g_FVGZones[i].linkedOrderTicket = 0;
|
|
|
|
|
|
LogDecisionTrace("ORDER", StringFormat("#%d %s placed @%.5f SL=%.5f HTF[%.5f..%.5f] LTF[%.5f..%.5f]",
|
|
|
|
|
|
i, FVGTypeToString(zone.type), entryPrice, slPrice,
|
|
|
|
|
|
zone.lowerEdge, zone.upperEdge, ltfLower, ltfUpper));
|
2026-03-11 18:13:27 +07:00
|
|
|
|
currentLimits++;
|
2026-03-19 21:12:47 +07:00
|
|
|
|
}
|
2026-05-09 23:16:50 +07:00
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
LogDecisionTrace("ORDER", StringFormat("#%d %s failed @%.5f", i, FVGTypeToString(zone.type), entryPrice));
|
|
|
|
|
|
}
|
2026-03-11 18:13:27 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|