FVG结构流动性模板_2
//+------------------------------------------------------------------+
//| 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();
}
//+------------------------------------------------------------------+
