欢迎访问

FVG结构流动性模板_2

author emer | 69 人阅读 | 0 人评论 |

//+------------------------------------------------------------------+
//| MT4 EA Template|
//| Strategy: Market Structure + Liquidity Sweep + FVG Confirm |
//| Author: Codex (auto-generated template)|
//+------------------------------------------------------------------+

property strict

//--- inputs
input int SwingDepth = 3;
input int LiquidityLookback = 5;
input double FVGMinPips = 3.0;
input int FVGExpiryMinutes = 90;
input int MaxActiveFVG = 20;
input double BufferPips = 1.0;
input double RiskPercent = 1.0;
input double PartialTPRatio = 1.0;
input double FinalTPRatio = 2.5;
input bool EnableTrading = false;
input bool AllowPartialTP = true;
input bool DrawObjects = true;
input int HistoryBars = 200;
input int MaxLiquidityLevels = 8;
input int FVGProjectionBars = 6;

enum SessionModeEnum
{
SessionAll = 0,
SessionLondon,
SessionNY,
SessionCustom
};

input SessionModeEnum SessionMode = SessionLondon;
input string LondonSession = "08:00-11:30";
input string NYSession = "13:30-16:30";
input string CustomSession1 = "08:00-11:30";
input string CustomSession2 = "13:30-16:30";
input int SessionGMTOffset = 0; // offset in hours applied to broker time

//--- data structures
struct SwingPoint
{
datetime time;
double price;
bool isHigh;
};

struct LiquidityEvent
{
datetime time;
double level;
int direction; // +1 swept highs, -1 swept lows
};

struct FVGSlot
{
datetime created;
double upper;
double lower;
int direction; // +1 bullish (buy), -1 bearish (sell)
bool isActive;
datetime lastTouch;
double score;
};

struct SimTrade
{
datetime entryTime;
double entryPrice;
double stopPrice;
double partialTP;
double finalTP;
int direction;
double lots;
bool partialTaken;
bool closed;
datetime closeTime;
double closePrice;
string closeReason;
double rrAchieved;
};

//--- buffers
SwingPoint SwingDeque[];
LiquidityEvent LiquidityEvents[];
FVGSlot ActiveFVGs[];
SimTrade SimTrades[];
SimTrade LastClosedTrade;
bool HasLastClosedTrade = false;

//--- global state
datetime lastBarTime = 0;
int structureBias = 0;
bool replayMode = false;
datetime replayTime = 0;
double replayBid = 0.0;
double replayAsk = 0.0;

//--- forward declarations
void RenderLiquidityLine(const datetime time, const double level, const int direction);
void RefreshLiquidityObjects();
void RemoveFVGObject(const FVGSlot &slot);

//+------------------------------------------------------------------+
//| Utility: active context helpers |
//+------------------------------------------------------------------+
datetime ActiveTime()
{
return(replayMode ? replayTime : TimeCurrent());
}

double ActiveBid()
{
return(replayMode ? replayBid : SymbolInfoDouble(_Symbol, SYMBOL_BID));
}

double ActiveAsk()
{
return(replayMode ? replayAsk : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
}

//+------------------------------------------------------------------+
//| Utility: pip size |
//+------------------------------------------------------------------+
double Pip()
{
if(_Digits == 3 || _Digits == 5)
return(10 * _Point);
return(_Point);
}

//+------------------------------------------------------------------+
//| Utility: trim dynamic array |
//+------------------------------------------------------------------+
template
void RemoveFirstElements(T &arr[], int remove)
{
int count = ArraySize(arr);
if(remove <= 0 || count == 0)
return;
if(remove >= count)
{
ArrayResize(arr, 0);
return;
}
for(int i = remove; i < count; i++)
arr[i - remove] = arr[i];
ArrayResize(arr, count - remove);
}

template
void LimitArraySize(T &arr[], int max_size)
{
int count = ArraySize(arr);
if(max_size > 0 && count > max_size)
RemoveFirstElements(arr, count - max_size);
}

string Trim(const string text)
{
string tmp = text;
StringTrimLeft(tmp);
StringTrimRight(tmp);
return(tmp);
}

//+------------------------------------------------------------------+
//| Utility: session boundaries |
//+------------------------------------------------------------------+
bool ParseSessionString(const string session, int &startMinutes, int &endMinutes)
{
int sep = StringFind(session, "-");
if(sep < 0)
return(false);
string startStr = Trim(StringSubstr(session, 0, sep));
string endStr = Trim(StringSubstr(session, sep + 1));

int startHour, startMin, endHour, endMin;
if(StringLen(startStr) < 4 || StringLen(endStr) < 4)
return(false);

startHour = (int)StringToInteger(StringSubstr(startStr, 0, 2));
startMin = (int)StringToInteger(StringSubstr(startStr, 3, 2));
endHour = (int)StringToInteger(StringSubstr(endStr, 0, 2));
endMin = (int)StringToInteger(StringSubstr(endStr, 3, 2));

startMinutes = startHour 60 + startMin;
endMinutes = endHour
60 + endMin;
return(true);
}

bool IsWithinSessions(const datetime time)
{
if(SessionMode == SessionAll)
return(true);

MqlDateTime tm;
TimeToStruct(time + SessionGMTOffset 3600, tm);
int minutesToday = tm.hour
60 + tm.min;

int start1, end1;
switch(SessionMode)
{
case SessionLondon:
if(ParseSessionString(LondonSession, start1, end1))
return(minutesToday >= start1 && minutesToday <= end1);
return(true);
case SessionNY:
if(ParseSessionString(NYSession, start1, end1))
return(minutesToday >= start1 && minutesToday <= end1);
return(true);
case SessionCustom:
{
bool within = false;
int s1, e1;
if(ParseSessionString(CustomSession1, s1, e1))
within |= (minutesToday >= s1 && minutesToday <= e1);
int s2, e2;
if(ParseSessionString(CustomSession2, s2, e2))
within |= (minutesToday >= s2 && minutesToday <= e2);
return(within);
}
default:
break;
}
return(false);
}

//+------------------------------------------------------------------+
//| Swing detection |
//+------------------------------------------------------------------+
bool DetectSwing(int index, bool &isHigh, double &price)
{
isHigh = false;
price = 0.0;
if(index < SwingDepth || index >= Bars - SwingDepth)
return(false);

double highVal = High[index];
double lowVal = Low[index];
bool highCandidate = true;
bool lowCandidate = true;

for(int i = 1; i <= SwingDepth; i++)
{
if(High[index + i] >= highVal || High[index - i] >= highVal)
highCandidate = false;
if(Low[index + i] <= lowVal || Low[index - i] <= lowVal)
lowCandidate = false;
}

if(highCandidate)
{
isHigh = true;
price = highVal;
return(true);
}
if(lowCandidate)
{
isHigh = false;
price = lowVal;
return(true);
}
return(false);
}

void PushSwing(const datetime time, const double price, const bool isHigh)
{
int size = ArraySize(SwingDeque);
ArrayResize(SwingDeque, size + 1);
SwingDeque[size].time = time;
SwingDeque[size].price = price;
SwingDeque[size].isHigh = isHigh;
LimitArraySize(SwingDeque, 50);
}

//+------------------------------------------------------------------+
//| Structure bias update |
//+------------------------------------------------------------------+
int UpdateStructureBias()
{
int highsFound = 0;
int lowsFound = 0;
double lastHigh = 0.0, prevHigh = 0.0;
double lastLow = 0.0, prevLow = 0.0;

for(int i = ArraySize(SwingDeque) - 1; i >= 0; i--)
{
if(SwingDeque[i].isHigh)
{
if(highsFound == 0)
lastHigh = SwingDeque[i].price;
else if(highsFound == 1)
{
prevHigh = SwingDeque[i].price;
highsFound = 2;
}
highsFound++;
}
else
{
if(lowsFound == 0)
lastLow = SwingDeque[i].price;
else if(lowsFound == 1)
{
prevLow = SwingDeque[i].price;
lowsFound = 2;
}
lowsFound++;
}

  if(highsFound >= 2 && lowsFound >= 2)
     break;
 }

if(highsFound >= 2 && lowsFound >= 2)
{
bool bullishStructure = lastHigh > prevHigh && lastLow > prevLow;
bool bearishStructure = lastHigh < prevHigh && lastLow < prevLow;
if(bullishStructure)
return(1);
if(bearishStructure)
return(-1);
}
return(0);
}

//+------------------------------------------------------------------+
//| Liquidity sweep detection |
//+------------------------------------------------------------------+
bool DetectLiquiditySweep(int index, int &direction, double &level)
{
direction = 0;
level = 0.0;
int size = ArraySize(SwingDeque);
if(size < 2)
return(false);

SwingPoint last = SwingDeque[size - 1];
SwingPoint prev = SwingDeque[size - 2];

if(last.isHigh && !prev.isHigh && last.price > prev.price && Close[index] < last.price)
{
direction = 1;
level = last.price;
return(true);
}

if(!last.isHigh && prev.isHigh && last.price < prev.price && Close[index] > last.price)
{
direction = -1;
level = last.price;
return(true);
}

return(false);
}

void PushLiquidityEvent(const datetime time, const double level, const int direction)
{
int size = ArraySize(LiquidityEvents);
ArrayResize(LiquidityEvents, size + 1);
LiquidityEvents[size].time = time;
LiquidityEvents[size].level = level;
LiquidityEvents[size].direction = direction;
LimitArraySize(LiquidityEvents, 50);
RefreshLiquidityObjects();
}

bool HasRecentLiquidityEvent(const datetime fromTime, const int direction)
{
for(int i = ArraySize(LiquidityEvents) - 1; i >= 0; i--)
{
if(LiquidityEvents[i].direction != direction)
continue;
if(LiquidityEvents[i].time >= fromTime)
return(true);
}
return(false);
}

//+------------------------------------------------------------------+
//| FVG detection and management |
//+------------------------------------------------------------------+
double PipsBetween(const double price1, const double price2)
{
return(MathAbs(price1 - price2) / Pip());
}

void PushFVG(const FVGSlot &slot)
{
if(MaxActiveFVG > 0 && ArraySize(ActiveFVGs) >= MaxActiveFVG)
{
RemoveFVGObject(ActiveFVGs[0]);
RemoveFirstElements(ActiveFVGs, 1);
}
int size = ArraySize(ActiveFVGs);
ArrayResize(ActiveFVGs, size + 1);
ActiveFVGs[size] = slot;
}

void ScanFVG(const int index)
{
if(index + 2 >= Bars)
return;

double high0 = High[index];
double low0 = Low[index];
double high1 = High[index + 1];
double low1 = Low[index + 1];
double high2 = High[index + 2];
double low2 = Low[index + 2];

double minGapPips = FVGMinPips;
datetime barTime = Time[index + 1];

if(low1 > high2 && low1 > high0)
{
double lower = MathMax(high2, high0);
double upper = low1;
if(PipsBetween(upper, lower) >= minGapPips)
{
FVGSlot slot;
slot.created = barTime;
slot.upper = upper;
slot.lower = lower;
slot.direction = 1;
slot.isActive = true;
slot.lastTouch = 0;
slot.score = 1.0;
PushFVG(slot);
}
}

if(high1 < low2 && high1 < low0)
{
double upper = MathMin(low2, low0);
double lower = high1;
if(PipsBetween(upper, lower) >= minGapPips)
{
FVGSlot slot;
slot.created = barTime;
slot.upper = upper;
slot.lower = lower;
slot.direction = -1;
slot.isActive = true;
slot.lastTouch = 0;
slot.score = 1.0;
PushFVG(slot);
}
}
}

void UpdateFVGStates()
{
datetime now = ActiveTime();
double bid = ActiveBid();
double ask = ActiveAsk();

for(int i = 0; i < ArraySize(ActiveFVGs); i++)
{
if(!ActiveFVGs[i].isActive)
continue;

  datetime expiry = ActiveFVGs[i].created + FVGExpiryMinutes * 60;
  if(FVGExpiryMinutes > 0 && now > expiry)
    {
     ActiveFVGs[i].isActive = false;
     RemoveFVGObject(ActiveFVGs[i]);
     continue;
    }

  double priceToCheck = ActiveFVGs[i].direction > 0 ? bid : ask;
  if(priceToCheck <= ActiveFVGs[i].lower || priceToCheck >= ActiveFVGs[i].upper)
    {
     ActiveFVGs[i].lastTouch = now;
     // mark inactive if fully filled beyond opposite boundary
     if(ActiveFVGs[i].direction > 0 && priceToCheck < ActiveFVGs[i].lower)
       {
        ActiveFVGs[i].isActive = false;
        RemoveFVGObject(ActiveFVGs[i]);
       }
     if(ActiveFVGs[i].direction < 0 && priceToCheck > ActiveFVGs[i].upper)
       {
        ActiveFVGs[i].isActive = false;
        RemoveFVGObject(ActiveFVGs[i]);
       }
    }
 }

}

//+------------------------------------------------------------------+
//| Entry validation |
//+------------------------------------------------------------------+
bool ConfirmEntry(const FVGSlot &slot, double &entryPrice, double &stopPrice, double &partialTP, double &finalTP)
{
if(!slot.isActive)
return(false);

if(structureBias != slot.direction)
return(false);

if(!IsWithinSessions(ActiveTime()))
return(false);

datetime lookbackTime = ActiveTime() - LiquidityLookback * PeriodSeconds();
if(!HasRecentLiquidityEvent(lookbackTime, slot.direction))
return(false);

double bid = ActiveBid();
double ask = ActiveAsk();

if(slot.direction > 0)
{
if(bid < slot.lower || bid > slot.upper)
return(false);
entryPrice = bid;
stopPrice = slot.lower - BufferPips Pip();
}
else
{
if(ask > slot.upper || ask < slot.lower)
return(false);
entryPrice = ask;
stopPrice = slot.upper + BufferPips
Pip();
}

double risk = MathAbs(entryPrice - stopPrice);
partialTP = slot.direction > 0 ? entryPrice + PartialTPRatio risk : entryPrice - PartialTPRatio risk;
finalTP = slot.direction > 0 ? entryPrice + FinalTPRatio risk : entryPrice - FinalTPRatio risk;

return(true);
}

//+------------------------------------------------------------------+
//| Risk calculations |
//+------------------------------------------------------------------+
double CalculatePositionSize(const double stopPrice, const double entryPrice)
{
double risk = MathAbs(entryPrice - stopPrice);
if(risk <= 0)
return(0.0);

double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmount = accountBalance * (RiskPercent / 100.0);

double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);

if(tickValue <= 0 || tickSize <= 0)
return(0.0);

double pointValue = tickValue / tickSize;
double lots = riskAmount / (risk / _Point * pointValue);

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(NormalizeDouble(lots, 2));
}

//+------------------------------------------------------------------+
//| Plotting |
//+------------------------------------------------------------------+
void RenderFVG(const FVGSlot &slot)
{
if(!DrawObjects)
return;

string name = StringFormat("FVG%d%I64d", slot.direction, slot.created);
color slotColor = slot.direction > 0 ? clrPaleGreen : clrMistyRose;
color labelColor = slot.direction > 0 ? clrForestGreen : clrFireBrick;
int projectionBars = FVGProjectionBars > 0 ? FVGProjectionBars : 1;
int secondsForward = projectionBars * PeriodSeconds();
datetime projected = Time[0] + secondsForward;
if(projected <= slot.created)
projected = slot.created + PeriodSeconds();
datetime rightTime = projected;

string fillName = name + "_bg";

ifdef OBJPROP_FILL

if(ObjectFind(0, fillName) == -1)
ObjectCreate(0, fillName, OBJ_RECTANGLE, 0, slot.created, slot.upper, rightTime, slot.lower);
ObjectSetInteger(0, fillName, OBJPROP_TIME1, slot.created);
ObjectSetDouble(0, fillName, OBJPROP_PRICE1, slot.upper);
ObjectSetInteger(0, fillName, OBJPROP_TIME2, rightTime);
ObjectSetDouble(0, fillName, OBJPROP_PRICE2, slot.lower);
ObjectSetInteger(0, fillName, OBJPROP_COLOR, slotColor);
ObjectSetInteger(0, fillName, OBJPROP_BACK, true);
ObjectSetInteger(0, fillName, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, fillName, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, fillName, OBJPROP_FILL, true);

ifdef OBJPROP_ALPHA

ObjectSetInteger(0, fillName, OBJPROP_ALPHA, 60);

endif

else

if(ObjectFind(0, fillName) == -1)
ObjectCreate(0, fillName, OBJ_RECTANGLE_LABEL, 0, slot.created, slot.upper, rightTime, slot.lower);
ObjectSetInteger(0, fillName, OBJPROP_TIME1, slot.created);
ObjectSetDouble(0, fillName, OBJPROP_PRICE1, slot.upper);
ObjectSetInteger(0, fillName, OBJPROP_TIME2, rightTime);
ObjectSetDouble(0, fillName, OBJPROP_PRICE2, slot.lower);
ObjectSetInteger(0, fillName, OBJPROP_COLOR, slotColor);
ObjectSetInteger(0, fillName, OBJPROP_BACK, true);

ifdef OBJPROP_ALPHA

ObjectSetInteger(0, fillName, OBJPROP_ALPHA, 60);

endif

endif

if(ObjectFind(0, name) == -1)
ObjectCreate(0, name, OBJ_RECTANGLE, 0, slot.created, slot.upper, rightTime, slot.lower);

ObjectSetInteger(0, name, OBJPROP_TIME1, slot.created);
ObjectSetDouble(0, name, OBJPROP_PRICE1, slot.upper);
ObjectSetInteger(0, name, OBJPROP_TIME2, rightTime);
ObjectSetDouble(0, name, OBJPROP_PRICE2, slot.lower);
ObjectSetInteger(0, name, OBJPROP_COLOR, labelColor);
ObjectSetInteger(0, name, OBJPROP_BACK, true);
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DASH);
ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);

string labelName = name + "_lbl";
if(ObjectFind(0, labelName) == -1)
ObjectCreate(0, labelName, OBJ_TEXT, 0, slot.created, slot.upper);
else
ObjectMove(0, labelName, 0, slot.created, slot.upper);
ObjectSetText(labelName, slot.direction > 0 ? "多头缺口" : "空头缺口", 8, "Arial", labelColor);
ObjectSetInteger(0, labelName, OBJPROP_COLOR, labelColor);
ObjectSetInteger(0, labelName, OBJPROP_BACK, true);
}

void RenderEntryMarker(const datetime time, const double price, const int direction, const double stop, const double finalTP)
{
if(!DrawObjects)
return;

string arrowName = StringFormat("Entry%I64d%d", time, direction);
ObjectCreate(0, arrowName, OBJ_ARROW, 0, time, price);
ObjectSetInteger(0, arrowName, OBJPROP_COLOR, direction > 0 ? clrGreen : clrRed);
ObjectSetInteger(0, arrowName, OBJPROP_ARROWCODE, direction > 0 ? 233 : 234);
ObjectSetInteger(0, arrowName, OBJPROP_WIDTH, 2);

string stopName = arrowName + "_SL";
ObjectCreate(0, stopName, OBJ_HLINE, 0, time, stop);
ObjectSetInteger(0, stopName, OBJPROP_COLOR, clrTomato);
ObjectSetInteger(0, stopName, OBJPROP_STYLE, STYLE_DASH);

string tpName = arrowName + "_TP";
ObjectCreate(0, tpName, OBJ_HLINE, 0, time, finalTP);
ObjectSetInteger(0, tpName, OBJPROP_COLOR, clrLimeGreen);
ObjectSetInteger(0, tpName, OBJPROP_STYLE, STYLE_DASH);
}

void RenderLiquidityLine(const datetime time, const double level, const int direction)
{
if(!DrawObjects)
return;

string name = StringFormat("LIQ%I64d%d", time, direction);
color lineColor = direction > 0 ? clrDodgerBlue : clrOrangeRed;

if(ObjectFind(0, name) == -1)
ObjectCreate(0, name, OBJ_HLINE, 0, 0, level);

ObjectSetDouble(0, name, OBJPROP_PRICE, level);
ObjectSetInteger(0, name, OBJPROP_COLOR, lineColor);
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DASHDOT);
ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);

string labelName = name + "_lbl";
double offset = 2.0 * Pip();
double textPrice = level + (direction > 0 ? offset : -offset);
datetime textTime = Time[0];

if(ObjectFind(0, labelName) == -1)
ObjectCreate(0, labelName, OBJ_TEXT, 0, textTime, textPrice);
else
ObjectMove(0, labelName, 0, textTime, textPrice);

ObjectSetText(labelName, direction > 0 ? "扫上流动性" : "扫下流动性", 8, "Arial", lineColor);
ObjectSetInteger(0, labelName, OBJPROP_COLOR, lineColor);
ObjectSetInteger(0, labelName, OBJPROP_ANCHOR, direction > 0 ? ANCHOR_LOWER : ANCHOR_UPPER);
ObjectSetInteger(0, labelName, OBJPROP_BACK, true);
}

void RefreshLiquidityObjects()
{
if(!DrawObjects)
return;

int totalObjects = ObjectsTotal(0, 0);
for(int idx = totalObjects - 1; idx >= 0; idx--)
{
string objName = ObjectName(0, idx);
if(StringFind(objName, "LIQ_") == 0)
ObjectDelete(0, objName);
}

int totalEvents = ArraySize(LiquidityEvents);
int limit = MaxLiquidityLevels > 0 ? MaxLiquidityLevels : totalEvents;
int start = MathMax(0, totalEvents - limit);
for(int i = start; i < totalEvents; i++)
RenderLiquidityLine(LiquidityEvents[i].time, LiquidityEvents[i].level, LiquidityEvents[i].direction);
}

void RemoveFVGObject(const FVGSlot &slot)
{
if(!DrawObjects)
return;

string name = StringFormat("FVG%d%I64d", slot.direction, slot.created);
ObjectDelete(0, name);
ObjectDelete(0, name + "_bg");
ObjectDelete(0, name + "_lbl");
}

void CleanupObjects()
{
int total = ObjectsTotal(0, 0);
for(int i = total - 1; i >= 0; i--)
{
string name = ObjectName(0, i);
if(StringFind(name, "FVG") == 0 || StringFind(name, "Entry") == 0 || StringFind(name, "LIQ_") == 0)
ObjectDelete(0, name);
}
ObjectDelete(0, "FVG_SL_HUD");
}

string DirectionToText(const int direction)
{
if(direction > 0)
return("多头");
if(direction < 0)
return("空头");
return("中性");
}

void RegisterSimulatedTrade(const int direction, const double lots, const double entryPrice, const double stopPrice, const double partialTP, const double finalTP)
{
if(!EnableTrading)
return;

if(lots <= 0.0)
{
Print("手数为零,忽略模拟交易。");
return;
}

SimTrade trade;
trade.entryTime = ActiveTime();
trade.entryPrice = entryPrice;
trade.stopPrice = stopPrice;
trade.partialTP = partialTP;
trade.finalTP = finalTP;
trade.direction = direction;
trade.lots = lots;
trade.partialTaken = (!AllowPartialTP || PartialTPRatio <= 0.0);
trade.closed = false;
trade.closeTime = 0;
trade.closePrice = 0.0;
trade.closeReason = "Open";
trade.rrAchieved = 0.0;

int size = ArraySize(SimTrades);
ArrayResize(SimTrades, size + 1);
SimTrades[size] = trade;
PrintFormat("模拟%s建仓,入场价 %.5f (止损 %.5f, 止盈 %.5f)",
direction > 0 ? "做多" : "做空",
entryPrice,
stopPrice,
finalTP);
}

void UpdateSimulatedTrades()
{
if(ArraySize(SimTrades) == 0)
return;

double bid = ActiveBid();
double ask = ActiveAsk();
datetime now = ActiveTime();

for(int i = 0; i < ArraySize(SimTrades); i++)
{
if(SimTrades[i].closed)
continue;

  double price = SimTrades[i].direction > 0 ? bid : ask;
  double risk  = MathAbs(SimTrades[i].entryPrice - SimTrades[i].stopPrice);
  if(risk <= 0)
     risk = Pip();

  if(AllowPartialTP && !SimTrades[i].partialTaken)
    {
     bool hitPartial = (SimTrades[i].direction > 0 && price >= SimTrades[i].partialTP) ||
                       (SimTrades[i].direction < 0 && price <= SimTrades[i].partialTP);
     if(hitPartial)
       {
        SimTrades[i].partialTaken = true;
        PrintFormat("模拟持仓已触发部分止盈,RR=%.2f。", PartialTPRatio);
       }
    }

  bool hitStop = (SimTrades[i].direction > 0 && price <= SimTrades[i].stopPrice) ||
                 (SimTrades[i].direction < 0 && price >= SimTrades[i].stopPrice);
  bool hitFinal = (SimTrades[i].direction > 0 && price >= SimTrades[i].finalTP) ||
                  (SimTrades[i].direction < 0 && price <= SimTrades[i].finalTP);

  if(hitStop || hitFinal)
    {
     SimTrades[i].closed      = true;
     SimTrades[i].closeTime   = now;
     SimTrades[i].closePrice  = price;
     SimTrades[i].closeReason = hitFinal ? "TP" : "SL";
     double reward = (SimTrades[i].direction > 0) ? (price - SimTrades[i].entryPrice) : (SimTrades[i].entryPrice - price);
     SimTrades[i].rrAchieved  = reward / risk;
     LastClosedTrade          = SimTrades[i];
     HasLastClosedTrade       = true;
     PrintFormat("模拟持仓因%s结束,价格 %.5f,RR=%.2f",
                 hitFinal ? "止盈" : "止损",
                 price,
                 SimTrades[i].rrAchieved);
    }
 }

}

void UpdateHUD()
{
string name = "FVG_SL_HUD";
if(ObjectFind(0, name) == -1)
{
ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, 10);
ObjectSetInteger(0, name, OBJPROP_COLOR, clrWhite);
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
ObjectSetString(0, name, OBJPROP_FONT, "Arial");
}

int activeFVGs = 0;
for(int i = 0; i < ArraySize(ActiveFVGs); i++)
if(ActiveFVGs[i].isActive)
activeFVGs++;

int openTrades = 0;
for(int j = 0; j < ArraySize(SimTrades); j++)
if(!SimTrades[j].closed)
openTrades++;

string sessionState = IsWithinSessions(TimeCurrent()) ? "交易时段" : "非交易";

string lastTradeLine = "最近交易: 无";
if(HasLastClosedTrade)
{
string dir = LastClosedTrade.direction > 0 ? "买入" : "卖出";
string reason = LastClosedTrade.closeReason == "TP" ? "止盈" : "止损";
string when = TimeToString(LastClosedTrade.closeTime, TIME_MINUTES);
lastTradeLine = StringFormat("最近交易: %s %s %s RR %.2f",
dir,
reason,
when,
LastClosedTrade.rrAchieved);
}

string text;
text = StringFormat("FVG结构流动性 (模拟)\n趋势倾向: %s\n时段状态: %s\n有效缺口: %d\n持仓数量: %d\n%s",
DirectionToText(structureBias),
sessionState,
activeFVGs,
openTrades,
lastTradeLine);

ObjectSetString(0, name, OBJPROP_TEXT, text);
}

//+------------------------------------------------------------------+
//| Trading |
//+------------------------------------------------------------------+
void ExecuteTrade(const int direction, const double lots, const double entryPrice, const double stopPrice, const double partialTP, const double finalTP)
{
RegisterSimulatedTrade(direction, lots, entryPrice, stopPrice, partialTP, finalTP);
}

void ReplayHistory()
{
if(HistoryBars <= 0)
return;
int available = Bars - 3;
if(available < 1)
return;
int limit = MathMin(HistoryBars, available);

for(int idx = limit; idx >= 1; idx--)
{
double price = Close[idx];
datetime evalTime = (idx > 0) ? Time[idx - 1] : Time[idx];
replayMode = true;
replayTime = evalTime;
replayBid = price;
replayAsk = price;

  ProcessBar(idx);
  UpdateSimulatedTrades();
 }

replayMode = false;
UpdateFVGStates();
UpdateSimulatedTrades();
RefreshLiquidityObjects();
}

//+------------------------------------------------------------------+
//| Core processing |
//+------------------------------------------------------------------+
void ProcessBar(const int index)
{
if(index < 1 || index >= Bars - 2)
return;
datetime barTime = Time[index];

bool isHigh;
double price;
if(DetectSwing(index, isHigh, price))
{
PushSwing(barTime, price, isHigh);
structureBias = UpdateStructureBias();
}

int direction;
double level;
if(DetectLiquiditySweep(index, direction, level))
PushLiquidityEvent(barTime, level, direction);

ScanFVG(index);
UpdateFVGStates();

for(int i = ArraySize(ActiveFVGs) - 1; i >= 0; i--)
{
if(!ActiveFVGs[i].isActive)
continue;

  double entryPrice, stopPrice, partialTP, finalTP;
 if(ConfirmEntry(ActiveFVGs[i], entryPrice, stopPrice, partialTP, finalTP))
   {
    double lots = CalculatePositionSize(stopPrice, entryPrice);
     RenderEntryMarker(barTime, entryPrice, ActiveFVGs[i].direction, stopPrice, finalTP);
     ExecuteTrade(ActiveFVGs[i].direction, lots, entryPrice, stopPrice, partialTP, finalTP);
     RemoveFVGObject(ActiveFVGs[i]);
     ActiveFVGs[i].isActive = false;
   }
  else
    {
     RenderFVG(ActiveFVGs[i]);
    }
 }

}

//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
CleanupObjects();
ArrayResize(SwingDeque, 0);
ArrayResize(LiquidityEvents, 0);
ArrayResize(ActiveFVGs, 0);
ArrayResize(SimTrades, 0);
HasLastClosedTrade = false;
structureBias = 0;
lastBarTime = 0;
ReplayHistory();
lastBarTime = Time[0];
UpdateHUD();
return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
CleanupObjects();
ObjectDelete(0, "FVG_SL_HUD");
}

//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
if(Bars < 100)
return;

replayMode = false;
datetime currentBarTime = Time[0];
if(lastBarTime != currentBarTime)
{
ProcessBar(1);
lastBarTime = currentBarTime;
}
UpdateFVGStates();
UpdateSimulatedTrades();
UpdateHUD();
}

//+------------------------------------------------------------------+

还没有人打赏,支持一下