English Русский Español Deutsch 日本語
preview
运用聪明钱概念(SMC)提升交易水平:订单块(OB)、结构突破(BOS)与公允价值缺口(FVG)

运用聪明钱概念(SMC)提升交易水平:订单块(OB)、结构突破(BOS)与公允价值缺口(FVG)

MetaTrader 5 — 示例 |
50 12
Hlomohang John Borotho
Hlomohang John Borotho

引言

有时,交易者会面临策略不一致的挑战,经常在指标、形态和方法之间来回切换,却没有一个坚实的框架来指引他们。这种缺乏结构的状态会导致混乱、错失机会以及不断累积的挫败感,尤其是在市场状况发生变化、传统工具难以跟上节奏的时候。在本文中,我们探讨一款将多种聪明钱概念(SMC)策略统一为一个强大解决方案的单一 EA 所能带来的好处。

聪明钱概念(SMC)通过订单块(OB)、结构突破(BOS)和公允价值缺口(FVG),为理解市场行为提供了一种结构化的方法。通过将这些概念整合到单一 EA 中,交易者可以简化工作流程、自动化决策过程,并专注于最强大的价格行为信号。无论是使用自动模式进行无缝执行,还是选择单一概念,这种方法都能减少主观猜测,使交易更加高效、稳定,并能适应不断变化的市场状况。



EA的逻辑

订单块:

订单块(OB)代表重大市场行情启动前的最后一根看涨或看跌 K 线,通常预示着机构交易者将订单布置在何处。当价格重新回到这些区域时,它们往往会充当强劲的支撑或阻力区域,提供高概率的交易机会。在这款 EA 中,订单块检测还通过整合斐波那契指标进行验证而得到增强,确保交易与关键回撤位或扩展位保持一致。正如前一篇文章所讨论的,将 OB 与斐波那契确认相结合,可以过滤掉较弱的交易形态,为交易者提供一个更可靠的进场与持仓管理框架。

公允价值缺口:

公允价值缺口(FVG)出现在价格走势失衡之时,通常由一波强劲的推动性行情所创造:一根或多根 K 线在前一根与后一根 K 线的影线之间留下一个缺口。这个缺口反映了市场中的低效状态 —— 并非所有订单都得到了匹配,价格往往会回到该区域以 "填补" 这个空间,然后继续朝既定方向运行。通过识别并围绕 FVG 进行交易,交易者可以预判价格可能回撤至这些区域,从而获得风险回报比明确的精准进场机会。

结构突破:

当价格向上突破前一个波段高点或向下跌破前一个波段低点时,便发生结构突破(BOS),预示市场方向可能发生转变。在之前的文章中,我们重点讨论的是在波段高点被突破时卖出,利用看跌动能获利。然而,在本方法中,BOS 逻辑经过优化以顺应市场主流趋势 —— 也就是说,如果波段高点被突破,我们现在会寻找买入机会,以确认看涨力量;而波段低点的跌破则发出卖出信号。这一转变确保交易顺应市场主导流向,而非逆势而为。

系统架构:



让我们开始

//+------------------------------------------------------------------+
//|                                                 SMC_ALL_IN_1.mq5 |
//|                        GIT under Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/en/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "GIT under Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/johnhlomohang/"
#property version   "1.01"
#property description "Unified SMC: FVG + Order Blocks + BOS. Detect + Draw + Trade."

#include <Trade/Trade.mqh>
#include <Trade/PositionInfo.mqh>

CTrade         trade;
CPositionInfo  pos;

enum ENUM_STRATEGY
{
   STRAT_OB,         // Use Order Blocks Only
   STRAT_FVG,        // Use FVGs Only
   STRAT_BOS,        // Use Break of Structure Only
   STRAT_AUTO        // Auto (All SMC Concepts)
};
enum SWING_TYPE{
   SWING_OB,
   SWING_BOS,
};

该代码包含 MetaTrader 的交易库,具体为 CTrade 和 CPositionInfo,它们使 EA 能够下达并管理交易,同时访问有关未平仓头寸的信息。通过在开头实例化这些类,EA 确保其具备订单执行和持仓跟踪所需的核心工具。

下一部分使用枚举来定义策略的灵活性与摆动点的分类。ENUM_STRATEGY 为交易者提供了选择 EA 执行何种方法的选项:仅采用订单块策略、仅采用 FVG 策略、仅采用 BOS 策略,或完全自动化模式(STRAT_AUTO)—— 该模式动态地综合运用这三种概念。同样,SWING_TYPE 枚举用于区分用于订单块分析的摆动点与用于结构突破分析的摆动点。这种结构使代码模块化且易于适配,允许交易者尝试不同的 SMC 方法,或让 EA 根据市场状况自动做出决策。

//----------------------------- Inputs ------------------------------//
input ENUM_STRATEGY TradeStrategy   = STRAT_AUTO;
input double        In_Lot          = 0.02;
input double        StopLoss        = 3500;   // points
input double        TakeProfit      = 7500;   // points
input long          MagicNumber     = 76543;

input int           SwingPeriod     = 5;      // bars each side to confirm swing
input int           SwingProbeBar   = 5;      // bar index we test for swings (>=SwingPeriod)
input double        Fib_Trade_lvls  = 61.8;   // OB retrace must reach this %
input bool          DrawBOSLines    = true;

input int           FVG_MinPoints   = 3;      // minimal gap in points
input int           FVG_ScanBars    = 20;     // how many bars to scan for FVGs
input bool          FVG_TradeAtEQ   = true;   // trade at 50% of the gap (EQ)
input bool          OneTradePerBar  = true;

//---------------------------- Colors -------------------------------//
#define BullOB   clrLime
#define BearOB   clrRed
#define BullFVG  clrPaleGreen
#define BearFVG  clrMistyRose
#define BOSBull  clrDodgerBlue
#define BOSBear  clrTomato

本段代码定义了交易者可根据自身交易风格和风险管理偏好进行自定义的输入参数。TradeStrategy 输入参数允许用户选择 EA 的交易方式:仅使用订单块、仅使用公允价值缺口、仅使用结构突破,或自动综合运用这三种方法。风险与资金管理设置通过 In_Lot、StopLoss、TakeProfit 和 MagicNumber 进行控制,以确保合理的下单手数、保护性止损、止盈目标以及唯一的交易标识。此外,SwingPeriod 和 SwingProbeBar 等摆动检测参数指定了识别波段高点和波段低点时纳入计算的 K 线数量,而 Fib_Trade_lvls 则确保订单块交易与斐波那契回撤确认保持一致。DrawBOSLines 选项为图表可视化提供了灵活性,允许交易者直接在图表上查看结构突破(BOS)水平。

接下来是公允价值缺口(FVG)设置,它为缺口的检测与交易方式提供了更精细的控制。FVG_MinPoints 确保只考虑显著的失衡缺口,而 FVG_ScanBars 则决定 EA 在历史数据中向后搜索有效缺口的范围。FFVG_TradeAtEQ 选项允许在缺口的 50% 位置(均衡点)进行交易;在 SMC 理论中,该位置通常被视为较为平衡的入场点。最后,BullOB、BearOB、BullFVG、BearFVG、BOSBull 和 BOSBear 等颜色定义使图表对象在视觉上直观易辨,让交易者能够一眼快速区分看涨与看跌的交易形态。

//---------------------------- Globals ------------------------------//
double   Bid, Ask;
datetime g_lastBarTime = 0;

// OB state
class COrderBlock : public CObject
{
public:
   int      direction;   // +1 bullish, -1 bearish
   datetime time;        // OB candle time
   double   high;        // OB candle high
   double   low;         // OB candle low

   string Key() const { return TimeToString(time, TIME_DATE|TIME_MINUTES); }

   void draw(datetime tmS, datetime tmE, color clr){
      string objOB = " OB REC" + TimeToString(time);
      ObjectCreate( 0, objOB, OBJ_RECTANGLE, 0, time, low, tmS, high);
      ObjectSetInteger( 0, objOB, OBJPROP_FILL, true);
      ObjectSetInteger( 0, objOB, OBJPROP_COLOR, clr);
      
      string objtrade = " OB trade" + TimeToString(time);
      ObjectCreate( 0, objtrade, OBJ_RECTANGLE, 0, tmS, high, tmE, low); // trnary operator
      ObjectSetInteger( 0, objtrade, OBJPROP_FILL, true);
      ObjectSetInteger( 0, objtrade, OBJPROP_COLOR, clr);
   }
};
COrderBlock* OB = NULL;

// OB fib state
// Track if an OB has already been traded
datetime lastTradedOBTime = 0;
bool tradedOB = false;
double fib_low, fib_high;
datetime fib_t1, fib_t2;
bool isBullishOB = false; 
bool isBearishOB = false;
datetime T1;
datetime T2;
color OBClr;
#define FIB_OB_BULL "FIB_OB_BULL"
#define FIB_OB_BEAR "FIB_OB_BEAR"
#define FIBO_OBJ "Fibo Retracement"

// BOS state 
datetime lastBOSTradeTime = 0;
bool Bull_BOS_traded, Bear_BOS_traded;
int lastBOSTradeDirection = 0; // 1 for buy, -1 for sell
double   swng_High = -1.0, swng_Low = -1.0;
datetime bos_tH = 0, bos_tL = 0;

在这里,代码的全局区段确立了跟踪市场状态、交易机会和图表对象的核心变量与结构。它从 Bid、Ask 和 g_lastBarTime 等简单的全局变量开始,这些变量对于实时价格跟踪以及确保专家顾问(EA)每根新 K 线只执行一次逻辑至关重要。随后,创建了一个专门的类 COrderBlock,用于封装订单块的属性,包括其方向(看涨或看跌)、时间以及价格水平(高点 / 低点)。该类还包含一个 draw () 函数,它会在图表上自动创建并着色矩形,方便交易者直观识别活跃的订单块及其交易区域。通过将这些功能集中到一个类中,代码保持了条理性与可复用性。

除了基本的 OB 识别之外,还定义了额外的变量来处理基于斐波那契的订单块验证。例如,fib_low、fib_high 以及时间标记 fib_t1 和 fib_t2 存储斐波那契回撤水平的边界,而 isBullishOB、isBearishOB 和 tradedOB 等标志则确保每个订单块只被验证和交易一次。这些变量使 EA 能够动态引用斐波那契水平,确保只有有效的回撤位(例如输入参数中定义的 61.8% 水平)才会被纳入交易进场考虑。为使可视化更清晰,FIB_OB_BULL、FIB_OB_BEAR 和 FIBO_OBJ 等常量定义了图表对象的名称,确保斐波那契回撤在绘制时易于区分。

最后,引入了结构突破(BOS)的状态管理。lastBOSTradeTime、Bull_BOS_traded 和 Bear_BOS_traded 这些变量用于防止 BOS 信号出现时执行重复交易。lastBOSTradeDirection 用于跟踪上一次 BOS 交易是看涨(+1)还是看跌(–1),而 swng_High 和 swng_Low 则存储用于结构检测的最新摆动水平。时间标记 bos_tH 和 bos_tL 用于识别摆动被确认的精确时刻,确保与市场结构规则保持一致。通过在全局层面管理这些状态,EA 确保了不同策略(订单块、BOS、FVG)之间的一致性,并防止交易重叠,为聪明钱概念(SMC)交易自动化奠定了结构化的基础。

//--------------------------- Helpers -------------------------------//
double  getHigh(int i)   { return iHigh(_Symbol, _Period, i);  }
double  getLow(int i)    { return iLow(_Symbol, _Period, i);   }
double  getOpen(int i)   { return iOpen(_Symbol, _Period, i);  }
double  getClose(int i)  { return iClose(_Symbol, _Period, i); }
datetime getTimeBar(int i){ return iTime(_Symbol, _Period, i); }

bool IsNewBar()
{
   datetime lastbar_time = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
   if(g_lastBarTime == 0) { g_lastBarTime = lastbar_time; return false; }
   if(g_lastBarTime != lastbar_time) { g_lastBarTime = lastbar_time; return true; }
   return false;
}

void ExecuteTrade(ENUM_ORDER_TYPE type)
{
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   double price = (type==ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
                                         : SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double sl = (type==ORDER_TYPE_BUY) ? price - StopLoss*point
                                      : price + StopLoss*point;
   double tp = (type==ORDER_TYPE_BUY) ? price + TakeProfit*point
                                      : price - TakeProfit*point;
   sl = NormalizeDouble(sl, _Digits);
   tp = NormalizeDouble(tp, _Digits);

   trade.SetExpertMagicNumber(MagicNumber);
   trade.PositionOpen(_Symbol, type, In_Lot, price, sl, tp, "SMC");
}

这里的辅助函数提供了一种简洁且可复用的方式来与图表数据交互。与在整个代码中直接调用 iHigh、iLow 或 iClose 等内置函数不同,getHigh ()、getLow () 和 getClose () 等包装函数使访问 K 线信息更加简便,同时提高了代码可读性。IsNewBar () 函数在确保 EA 每根 K 线只处理一次逻辑方面起着关键作用,避免在同一根 K 线内重复执行。它通过将上一次已知的 K 线时间存储在 g_lastBarTime 中,并将其与当前 K 线的时间戳进行比较来实现这一点。这种高效的方法确保只有在新 K 线开盘时才会考虑信号和交易,这对于依赖已确认 K 线收盘的策略至关重要。

ExecuteTrade () 函数封装了完整的交易执行逻辑。它会根据订单类型(买入或卖出)自动计算正确的进场价、止损和止盈水平,同时确保数值经过标准化处理,与交易品种的小数位精度相匹配。通过集中这一逻辑,EA 避免了重复代码,并减少了开仓时的错误。该函数还设置了 EA 唯一的 MagicNumber 用于跟踪,并为每笔交易添加 "SMC" 注释标记,使交易者能够轻松识别由该系统开立的头寸。

//----------------------- Unified Swing Detection -------------------//
// Detects if barIndex is a swing high and/or swing low using len bars on each side.
// If swing high found -> updates fib_high/fib_tH (and swng_High/bos_tH if for BOS).
// If swing low  found -> updates fib_low/fib_tL (and swng_Low/bos_tL if for BOS).
// return: true if at least one swing found.
void DetectSwingForBar(int barIndex, SWING_TYPE type)
{
   const int len = 5;
   bool isSwingH = true, isSwingL = true;   

   for(int i = 1; i <= len; i++){
      int right_bars = barIndex - i;
      int left_bars  = barIndex + i;
      
      if(right_bars < 0) {
         isSwingH = false;
         isSwingL = false;
         break;
      }
      
      if((getHigh(barIndex) <= getHigh(right_bars)) || (left_bars < Bars(_Symbol, _Period) && getHigh(barIndex) < getHigh(left_bars)))
         isSwingH = false;
      
      if((getLow(barIndex) >= getLow(right_bars)) || (left_bars < Bars(_Symbol, _Period) && getLow(barIndex) > getLow(left_bars)))
         isSwingL = false;
   }

   // Assign with ternary operator depending on swing type
   if(isSwingH){
      if(type == SWING_OB) {
         fib_high = getHigh(barIndex);
         fib_t1 = getTimeBar(barIndex);
      } else {
         swng_High = getHigh(barIndex);
         bos_tH = getTimeBar(barIndex);
      }
   }
   if(isSwingL){
      if(type == SWING_OB) {
         fib_low = getLow(barIndex);
         fib_t2 = getTimeBar(barIndex);
      } else {
         swng_Low = getLow(barIndex);
         bos_tL = getTimeBar(barIndex);
      }
   }
}

统一的摆动检测函数旨在识别图表上潜在的波段高点与波段低点,其方法是检查某一根指定的 K 线,并将其与一定数量的相邻 K 线进行比较。该函数使用对称范围(两侧各 len 根 K 线)来检查当前 K 线是否构成局部极值 —— 即其最高价高于左侧和右侧的 K 线(波段高点),或是最低价低于两侧的 K 线(波段低点)。如果左侧或右侧的任何一根 K 线使这些条件失效,则该摆动标记被否决。这种方法确保摆动不会因小幅波动而被误触发,而是聚焦于市场结构中具有意义的枢轴点。

一旦摆动得到确认,该函数会根据摆动类型更新关键变量。对于订单块(SWING_OB)检测,波段高点和波段低点连同其时间戳被赋予斐波那契锚点(fib_high 和 fib_low),这些锚点稍后将用于回撤验证。对于结构突破(BOS)逻辑,相同的摆动水平被赋予 swng_High 或 swng_Low,并与各自的时间戳(bos_tH 或 bos_tL)配对。通过在单个函数中同时处理 OB 和 BOS,EA 保持了摆动检测的精简性并避免了冗余逻辑,确保结构验证与斐波那契回撤形态共享同一套一致的摆动识别流程。

void DetectAndDrawOrderBlocks()
{
   static datetime lastDetect = 0;
   datetime lastBar = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
   
   // Reset OB detection on new bar
   if(lastDetect != lastBar)
   {
      if(OB != NULL) 
      { 
         delete OB; 
         OB = NULL; 
      }
      lastDetect = lastBar;
   }
   
   // Only detect new OB if we don't have one already
   if(OB == NULL)
   {
      for(int i = 1; i < 100; i++)
      {
         // Bullish OB candidate
         if(getOpen(i) < getClose(i) && 
            getOpen(i+2) < getClose(i+2) &&
            getOpen(i+3) > getClose(i+3) && 
            getOpen(i+3) < getClose(i+2))
         {
            OB = new COrderBlock();
            OB.direction = 1;
            OB.time = getTimeBar(i+3);
            OB.high = getHigh(i+3);
            OB.low = getLow(i+3);
            OBClr = BullOB;
            T1 = OB.time;
            Print("Bullish Order Block detected at: ", TimeToString(OB.time));
            break;
         }
         
         // Bearish OB candidate
         if(getOpen(i) > getClose(i) && 
            getOpen(i+2) > getClose(i+2) &&
            getOpen(i+3) < getClose(i+3) && 
            getOpen(i+3) > getClose(i+2)) // Fixed condition
         {
            OB = new COrderBlock();
            OB.direction = -1;
            OB.time = getTimeBar(i+3);
            OB.high = getHigh(i+3);
            OB.low = getLow(i+3);
            OBClr = BearOB;
            T1 = OB.time;
            Print("Bearish Order Block detected at: ", TimeToString(OB.time));
            break;
         }
      }
   }

   if(OB == NULL) return;
   
   // Check if we already traded this OB
   if(lastTradedOBTime == OB.time) return;

   // If price retraces inside OB zone
   Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

   bool inBullZone = (OB.direction > 0 && Ask <= OB.high && Ask >= OB.low);
   bool inBearZone = (OB.direction < 0 && Bid >= OB.low && Bid <= OB.high);

   if(!inBullZone && !inBearZone) return;

   // Use your DetectSwing function to find swings
   // We need to call it multiple times to find the most recent swings
   double mostRecentSwingHigh = 0;
   double mostRecentSwingLow = EMPTY_VALUE;
   datetime mostRecentSwingHighTime = 0;
   datetime mostRecentSwingLowTime = 0;
   
   // Scan recent bars to find the most recent swings
   for(int i = 0; i < 20; i++) // Check the last 20 bars
   {
      // Reset swing variables
      fib_high = 0;
      fib_low = 0;
      fib_t1 = 0;
      fib_t2 = 0;
      
      DetectSwingForBar(i, SWING_OB);
      
      if(fib_high > 0 && (mostRecentSwingHighTime == 0 || fib_t1 > mostRecentSwingHighTime))
      {
         mostRecentSwingHigh = fib_high;
         mostRecentSwingHighTime = fib_t1;
      }
      
      if(fib_low < EMPTY_VALUE && (mostRecentSwingLowTime == 0 || fib_t2 > mostRecentSwingLowTime))
      {
         mostRecentSwingLow = fib_low;
         mostRecentSwingLowTime = fib_t2;
      }
   }
   
   // Ensure we found both swing points
   if(mostRecentSwingHighTime == 0 || mostRecentSwingLowTime == 0) return;
   
   // Draw Fibonacci before trading to validate
   if(OB.direction > 0 && inBullZone)
   {
      // Draw Fibonacci from recent swing low to recent swing high
      ObjectDelete(0, "FIB_OB_BULL");
      if(ObjectCreate(0, "FIB_OB_BULL", OBJ_FIBO, 0, mostRecentSwingLowTime, mostRecentSwingLow, 
                     mostRecentSwingHighTime, mostRecentSwingHigh))
      {
         // Format Fibonacci
         ObjectSetInteger(0, "FIB_OB_BULL", OBJPROP_COLOR, clrBlack);
         for(int i = 0; i < ObjectGetInteger(0, "FIB_OB_BULL", OBJPROP_LEVELS); i++)
         {
            ObjectSetInteger(0, "FIB_OB_BULL", OBJPROP_LEVELCOLOR, i, clrBlack);
         }
         
         double entLvlBull = mostRecentSwingHigh - (mostRecentSwingHigh - mostRecentSwingLow) * (Fib_Trade_lvls / 100.0);
         
         if(Ask <= entLvlBull)
         {
            T2 = getTimeBar(0);
            OB.draw(T1, T2, BullOB);
            ExecuteTrade(ORDER_TYPE_BUY);
            lastTradedOBTime = OB.time; // Mark this OB as traded
            delete OB;
            OB = NULL;
         }
      }
   }
   else if(OB.direction < 0 && inBearZone)
   {
      // Draw Fibonacci from recent swing high to recent swing low
      ObjectDelete(0, "FIB_OB_BEAR");
      if(ObjectCreate(0, "FIB_OB_BEAR", OBJ_FIBO, 0, mostRecentSwingHighTime, mostRecentSwingHigh, 
                     mostRecentSwingLowTime, mostRecentSwingLow))
      {
         // Format Fibonacci
         ObjectSetInteger(0, "FIB_OB_BEAR", OBJPROP_COLOR, clrBlack);
         for(int i = 0; i < ObjectGetInteger(0, "FIB_OB_BEAR", OBJPROP_LEVELS); i++)
         {
            ObjectSetInteger(0, "FIB_OB_BEAR", OBJPROP_LEVELCOLOR, i, clrBlack);
         }
         
         double entLvlBear = mostRecentSwingLow + (mostRecentSwingHigh - mostRecentSwingLow) * (Fib_Trade_lvls / 100.0);
         
         if(Bid >= entLvlBear)
         {
            T2 = getTimeBar(0);
            OB.draw(T1, T2, BearOB);
            ExecuteTrade(ORDER_TYPE_SELL);
            lastTradedOBTime = OB.time; // Mark this OB as traded
            delete OB;
            OB = NULL;
         }
      }
   }
}

DetectAndDrawOrderBlocks () 函数负责识别、验证并交易订单块,同时将斐波那契汇聚融入决策流程之中。它首先在每根新 K 线上重置检测状态,然后扫描最近的 K 线,寻找看涨或看跌的订单块形态。一旦找到有效的订单块,它便会检查当前价格是否回撤进入该区域,以此发出潜在交易机会的信号。在执行交易之前,该函数会调用摆动检测来定位最近的波段高点和波段低点,确保可以在两者之间绘制一条斐波那契回撤。

随后使用斐波那契工具来验证进场,要求价格与预设的回撤位对齐之后才确认交易。这样一来,系统避免了过早进场,并确保只有在订单块区域与斐波那契回撤同时对齐时才执行交易,从而增强交易验证的准确性与一致性。

//============================== FVG ================================//
// Definition (ICT-style):
// Let C=i, B=i+1, A=i+2.
// Bullish FVG if Low(A) > High(C) -> gap [High(C), Low(A)]
// Bearish FVG if High(A) < Low(C) -> gap [High(A), Low(C)]
struct SFVG
{
   int      dir;    // +1 bull, -1 bear
   datetime tLeft;  // left time anchor
   double   top;    // zone top price
   double   bot;    // zone bottom price

   string Name() const
   {
      string k = TimeToString(tLeft, TIME_DATE|TIME_MINUTES);
      return (dir>0 ? "FVG_B_" : "FVG_S_") + k + "_" + IntegerToString((int)(top*1000.0));
   }
};

bool FVGExistsAt(const string &name){ return ObjectFind(0, name) != -1; }

void DetectAndDrawFVGs()
{
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int counted = 0;

   for(int i=2; i<MathMin(FVG_ScanBars, Bars(_Symbol, _Period))-2; i++)
   {
      // Build A,B,C
      double lowA  = getLow(i+2);
      double highA = getHigh(i+2);
      double highC = getHigh(i);
      double lowC  = getLow(i);

      // Bullish FVG: Low of A > High of C
      if(lowA > highC && (lowA - highC >= FVG_MinPoints * point))
      {
         SFVG z;
         z.dir   = +1;
         z.tLeft = getTimeBar(i+2);  // Changed from getTimeBar to getTime
         z.top   = lowA;
         z.bot   = highC;
         DrawFVG(z);
         counted++;
      }
      // Bearish FVG: High of A < Low of C
      else if(highA < lowC && (lowC - highA >= FVG_MinPoints * point))
      {
         SFVG z;
         z.dir   = -1;
         z.tLeft = getTimeBar(i+2);  // Changed from getTimeBar to getTime
         z.top   = lowC;          // Fixed: should be lowC for bearish FVG top
         z.bot   = highA;         // Fixed: should be highA for bearish FVG bottom
         DrawFVG(z);
         counted++;
      }
      
      if(counted > 15) break; // avoid clutter
   }

   // --- Simplified trading for FVGs ---
   Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

   // scan drawn objects and trade on first valid touch of EQ (50%)
   int total = ObjectsTotal(0, 0, -1);
   static datetime lastTradeBar = 0;
   
   if(OneTradePerBar)
   {
      datetime barNow = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
      if(lastTradeBar == barNow) return; // already traded this bar
   }

   for(int idx=0; idx<total; idx++)
   {
      string name = ObjectName(0, idx);
      if(StringFind(name, "FVG_", 0) != 0) continue; // only our FVGs

      // Get object coordinates
      datetime t1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0);
      double y1 = ObjectGetDouble(0, name, OBJPROP_PRICE, 0);
      datetime t2 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 1);
      double y2 = ObjectGetDouble(0, name, OBJPROP_PRICE, 1);

      double top = MathMax(y1, y2);
      double bot = MathMin(y1, y2);
      bool isBull = (StringFind(name, "FVG_B_", 0) == 0);
      double mid  = (top + bot) * 0.5;

      if(isBull)
      {
         // trade when Ask is inside the gap and at/under EQ
         if(Ask <= top && Ask >= bot && (!FVG_TradeAtEQ || Ask <= mid))
         {
            ExecuteTrade(ORDER_TYPE_BUY);
            lastTradeBar = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
            break;
         }
      }
      else
      {
         // trade when Bid is inside the gap and at/over EQ
         if(Bid <= top && Bid >= bot && (!FVG_TradeAtEQ || Bid >= mid))
         {
            ExecuteTrade(ORDER_TYPE_SELL);
            lastTradeBar = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
            break;
         }
      }
   }
}

该函数基于 ICT 逻辑检测、绘制并交易公允价值缺口(FVG)。它首先扫描最近的 K 线,以识别看涨缺口 —— 即 A K线的最低价高于 C K线的最高价 —— 以及看跌缺口 —— 即 A K线的最高价低于 C K线的最低价,前提是缺口满足最小点数要求。一旦检测到 FVG,它会被存储到一个结构中,在图表上进行可视化绘制,并被跟踪以用于交易。然后,系统会监控活跃的 FVG 区域,检查价格何时进入缺口并与均衡位(该区域的 50%)对齐(若该功能已启用)。当条件满足时,该函数执行交易:从看涨 FVG 买入、从看跌 FVG 卖出,同时确保每根 K 线只交易一次,以防止过度交易。这种检测、可视化与执行的组合,使 FVG 工具既具备分析性,又可直接用于交易。

//=============================== BOS ===============================//
// Use unified swings (no RSI). Trading logic mirrors your earlier code:
// - Sell when price breaks above last swing high (liquidity run idea)
// - Buy  when price breaks below last swing low
void DetectAndDrawBOS()
{
   // Use DetectSwingForBar to find the most recent swing points
   double mostRecentSwingHigh = 0;
   double mostRecentSwingLow = EMPTY_VALUE;
   datetime mostRecentSwingHighTime = 0;
   datetime mostRecentSwingLowTime = 0;
   
   // Scan recent bars to find the most recent swings for BOS
   for(int i = 0; i < 20; i++) // Check the last 20 bars
   {
      // Reset swing variables
      swng_High = 0;
      swng_Low = 0;
      bos_tH = 0;
      bos_tL = 0;
      
      // Detect swing at this bar for BOS
      DetectSwingForBar(i, SWING_BOS);
      
      if(swng_High > 0 && (mostRecentSwingHighTime == 0 || bos_tH > mostRecentSwingHighTime))
      {
         mostRecentSwingHigh = swng_High;
         mostRecentSwingHighTime = bos_tH;
      }
      
      if(swng_Low < EMPTY_VALUE && (mostRecentSwingLowTime == 0 || bos_tL > mostRecentSwingLowTime))
      {
         mostRecentSwingLow = swng_Low;
         mostRecentSwingLowTime = bos_tL;
      }
   }
   
   // Update the global BOS variables with the most recent swings
   if(mostRecentSwingHighTime > 0)
   {
      if(mostRecentSwingHighTime != bos_tH)
         Bull_BOS_traded = false;
      swng_High = mostRecentSwingHigh;
      bos_tH = mostRecentSwingHighTime;
   }
   
   if(mostRecentSwingLowTime > 0)
   {
      if(mostRecentSwingLowTime != bos_tL)
         Bear_BOS_traded = false;
      swng_Low = mostRecentSwingLow;
      bos_tL = mostRecentSwingLowTime;
   }
   
   // Now check for break of structure
   Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   
   // Get current bar time to prevent multiple trades on same bar
   datetime currentBarTime = iTime(_Symbol, _Period, 0);
   
   // SELL on break above swing high
   if(swng_High > 0 && Ask > swng_High && Bull_BOS_traded == false)
   {
      // Check if we haven't already traded this breakout
      if(lastBOSTradeTime != currentBarTime || lastBOSTradeDirection != -1)
      {
         if(DrawBOSLines)
            DrawBOS("BOS_H_" + TimeToString(bos_tH), bos_tH, swng_High,
                    TimeCurrent(), swng_High, BOSBear, -1);
         
         ExecuteTrade(ORDER_TYPE_BUY);
         
         // Update trade tracking
         lastBOSTradeTime = currentBarTime;
         lastBOSTradeDirection = -1;
         Bull_BOS_traded = true;
         
         // Reset the swing high to prevent immediate re-trading
         swng_High = -1.0;
      }
   }
   
   // BUY on break below swing low
   if(swng_Low > 0 && Bid < swng_Low && Bear_BOS_traded == false)
   {
      // Check if we haven't already traded this breakout
      if(lastBOSTradeTime != currentBarTime || lastBOSTradeDirection != 1)
      {
         if(DrawBOSLines)
            DrawBOS("BOS_L_" + TimeToString(bos_tL), bos_tL, swng_Low,
                    TimeCurrent(), swng_Low, BOSBull, +1);
         
         ExecuteTrade(ORDER_TYPE_SELL);
         
         // Update trade tracking
         Bear_BOS_traded = true;
         lastBOSTradeTime = currentBarTime;
         lastBOSTradeDirection = 1;
         
         // Reset the swing low to prevent immediate re-trading
         swng_Low = -1.0;
      }
   }   
}
BOS 函数使用统一摆动逻辑检测并交易结构突破(BOS)事件。它扫描最近 20 根 K 线以识别最近的波段高点和波段低点,更新全局 BOS 变量,并确保避免在同一根 K 线上重复交易。如果价格向上突破最近的波段高点,它执行买入(高点上方流动性扫荡);如果价格向下跌破最近的波段低点,它执行卖出。可选的 BOS 线条可以绘制出来用于可视化,内部标志则防止在同一突破上立即重复交易。
//---------------------------- BOS UI -------------------------------//
void DrawBOS(const string name, datetime t1, double p1, datetime t2, double p2, color col, int dir)
{
   if(ObjectFind(0, name) == -1)
   {
      ObjectCreate(0, name, OBJ_TREND, 0, t1, p1, t2, p2);
      ObjectSetInteger(0, name, OBJPROP_COLOR, col);
      ObjectSetInteger(0, name, OBJPROP_WIDTH, 2);

      string lbl = name + "_lbl";
      ObjectCreate(0, lbl, OBJ_TEXT, 0, t2, p2);
      ObjectSetInteger(0, lbl, OBJPROP_COLOR, col);
      ObjectSetInteger(0, lbl, OBJPROP_FONTSIZE, 10);
      ObjectSetString(0,  lbl, OBJPROP_TEXT, "Break");
      ObjectSetInteger(0, lbl, OBJPROP_ANCHOR, (dir>0)?ANCHOR_RIGHT_UPPER:ANCHOR_RIGHT_LOWER);
   }
}

DrawBOS 函数负责在图表上可视化标记结构突破(BOS),方法是在两点 (t1, p1) 与 (t2, p2) 之间创建一条具有指定颜色和宽度的趋势线。如果具有给定名称的对象尚不存在,它首先创建趋势线,设置其颜色和粗细,然后在端点添加一个显示 "Break" 的文本标签。标签的位置锚点取决于方向参数 dir:看涨时锚定在右上角,看跌时锚定在右下角,从而直接在图表上提供关于市场结构变化的清晰视觉提示。

void DrawFVG(const SFVG &z)
{
   string name = z.Name();
   datetime tNow = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
   
   // Delete existing object if it exists
   if(ObjectFind(0, name) != -1) 
      ObjectDelete(0, name);
   
   // Create rectangle object for FVG
   if(!ObjectCreate(0, name, OBJ_RECTANGLE, 0, z.tLeft, z.bot, tNow, z.top))
   {
      Print("Error creating FVG object: ", GetLastError());
      return;
   }
   
   // Set object properties
   ObjectSetInteger(0, name, OBJPROP_COLOR, z.dir>0 ? BullFVG : BearFVG);
   ObjectSetInteger(0, name, OBJPROP_FILL, true);
   ObjectSetInteger(0, name, OBJPROP_BACK, true);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
   ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);
   
   // Set Z-order to make sure it's visible
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 0);
}

DrawFVG 函数在图表上可视化公允价值缺口(FVG):它首先检查是否存在与该 FVG 同名的对象,若存在则将其删除以避免重复。然后它创建一个矩形,范围从缺口的左侧时间 tLeft 延伸至当前 K 线,从缺口的底部 bot 延伸至顶部 top。该矩形根据缺口方向设置颜色、进行填充、置于图表底层,并赋予实线边框。通过将其 Z-order 设为 0,FVG 保持在其他图表元素之后可见,为交易者提供关于价格失衡的清晰、实时的视觉参考。

void OnTick()
{
   if(!IsNewBar()) return;

   // Strategy switch
   if(TradeStrategy == STRAT_FVG || TradeStrategy == STRAT_AUTO)
      DetectAndDrawFVGs();

   if(TradeStrategy == STRAT_OB  || TradeStrategy == STRAT_AUTO)
      DetectAndDrawOrderBlocks();

   if(TradeStrategy == STRAT_BOS || TradeStrategy == STRAT_AUTO)
      DetectAndDrawBOS();
}

OnTick 函数在每个市场报价(tick)时执行,但仅在新 K 线形成时才继续运行,确保每根 K 线只执行一次计算。然后它检查所选的交易策略:如果启用了公允价值缺口(FVG)或自动模式,则调用 DetectAndDrawFVGs;如果启用了订单块(OB)或自动模式,则调用 DetectAndDrawOrderBlocks;如果启用了结构突破(BOS)或自动模式,则调用 DetectAndDrawBOS。这种结构使 EA 或指标能够根据所选策略,在实时环境中动态检测并可视化不同的市场结构。


回测结果

回测在 1 小时时间框架上进行评估,测试窗口约为两个月(2025 年 7 月 1 日至 2025 年 9 月 1 日),使用默认设置,以 BOS 作为策略。



结论

总之,我们开发了一个统一的聪明钱概念(SMC)交易框架,整合了三大支柱:订单块(OB)、结构突破(BOS)和公允价值缺口(FVG)。每个概念都编写了相应的检测、绘制和交易逻辑。OB 被识别为带有斐波那契回撤验证的机构资金活动留下的痕迹;BOS 用于捕捉价格扫过波段高点或波段低点时出现的流动性扫荡现象;FVG 则突出价格传递中的低效区域,这些区域往往充当强烈的反应区。通过将这些元素整合到单一系统中,该代码确保直接在图表上动态检测高概率交易形态,并配有可视化辅助与自动执行功能。

总而言之,这种统一的方法赋予交易者一种系统化应用 SMC 原则的方式,免除了手工图表分析中常伴随的主观猜测。该 EA 不仅标记并跟踪市场结构的变化,而且在价格与预设的 SMC 条件对齐时实时执行交易。这为交易者提供了一种纪律严明、基于规则的方法,能够减少情绪偏差、提高一致性,并在不同市场状况下捕捉机构级交易机会时提供专业优势。

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16340

附加的文件 |
SMC_ALL_IN_1.mq5 (21.41 KB)
最近评论 | 前往讨论 (12)
Hlomohang John Borotho
Hlomohang John Borotho | 16 9月 2025 在 15:59
Stanislav Korotky #:

“公允价值缺口”的描述和图示有误,请参阅本文 中的正确图示。

你搞错了,这其实是一回事,我绘制的公允价值缺口图中,价格/K线已经回调至公允价值缺口区域内。
Hlomohang John Borotho
Hlomohang John Borotho | 16 9月 2025 在 16:00
Yata Tema Gea #:

哇,你的文章写得真好。我一眼就看出文章里提到了印尼人。我们的思路是一致的,不同之处在于我使用Python来实现SMC+GPT。


我看到了 :)
Stanislav Korotky
Stanislav Korotky | 16 9月 2025 在 17:37
Hlomohang John Borotho #:
你搞错了,这其实是一回事,我绘制的FVG图中,价格和K线已经在FVG区域内回调了。

你要么搞错了,要么图表制作得不够严谨。由于(从左数起) 第1根和第3根K线显示为红色,说明它们是看跌K线,并且在第2根K线(推测FVG发生的位置)前后 形成了两个较大的跳空缺口。

如果你在网上搜索FVG,你会发现这种形态是指价格出现的大幅单向跳跃/缺口, 而非呈锯齿状的跳动,后者会模糊未来可能回调的整体效果,因为你无法确定(这三个缺口)中的哪一个会首先回补(在你的图表中,是第三根K线处的最后一个缺口,而不是第二个缺口 ——该缺口已被第四根K线填补)。
Bao Thuan Thai
Bao Thuan Thai | 28 9月 2025 在 11:06
我不需要知道它是怎么运作的,但非常感谢你的分享和善意。
ritik pathak
ritik pathak | 4 10月 2025 在 11:01
做得很好,如果止损位设在FVG下方/上方,或者订单区块采用非固定点止损,效果会更好。
交易中的神经网络:时间序列查询模型(TQNet) 交易中的神经网络:时间序列查询模型(TQNet)
时序查询网络(TQNet)框架兼具模块化、灵活性与高性能,为金融时间序列的建模与预测开辟了新路径。本文探讨实现全局相关性复杂处理机制的可行性,其中包含高级参数初始化方法。
从新手到专家:使用MQL5实现动态新闻标题(X)——新闻交易的多品种图表视图 从新手到专家:使用MQL5实现动态新闻标题(X)——新闻交易的多品种图表视图
今天,我们将基于图表对象开发一套多图表视图系统。开发目标是服务于新闻交易,借助MQL5算法,缩短交易者在重大新闻发布等高波动行情下的反应时间。在本示例中,我们为交易者提供一体化方案,可以在同一个新闻交易工具内同时监控多个主流交易品种。我们的开发在新闻标题EA(News Headline EA)的基础上持续迭代,目前该EA的功能还在不断扩充,无论是全自动策略使用者,还是依靠算法辅助手工交易的交易者,都能从中获得实际帮助。欢迎点击阅读、参与讨论,一起学习更多知识、思路与实战方案。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
交易中的神经网络:分解,而非扩大模型规模(结论) 交易中的神经网络:分解,而非扩大模型规模(结论)
本文将介绍一种算法,可以将时间序列分解为具备明确含义的多层分量,并基于这些分量构建简约模型。我们会系统阐述模型架构、基于MQL5/OpenCL的实际实现,以及基于历史行情数据开展的实际测试。