English Русский Español Deutsch 日本語
preview
价格行为分析工具包开发(第三十九部分):在MQL5中实现BOS与ChoCH的自动识别

价格行为分析工具包开发(第三十九部分):在MQL5中实现BOS与ChoCH的自动识别

MetaTrader 5 — 示例 |
46 4
Christian Benjamin
Christian Benjamin

引言

欢迎来到价格行为分析工具包开发系列的第三十九部分。在本文中,我们将搭建一套实用的MQL5系统,帮助价格行为交易者清晰解读图表,不必紧盯K线而错过市场结构的转变。目标很简单:把分形拐点与结构判定规则转化为稳定、无重绘的信号,无论是实盘交易还是历史回测都可以放心使用。

我们将分形拐点作为可靠的局部锚点,并识别两类互补信号: ChoCH(行情性质转变),用于标记市场原有趋势偏向正在衰竭 —— 例如上涨趋势无法创出更高的高点,或是下跌趋势无法创出更低的低点,以及 BOS(结构突破),代表价格收盘有效突破前一波高低点,确认趋势偏向已经发生切换。可以将ChoCH理解为早期预警,BOS则是确认信号。

CHoCH与BOS

将分形指标与ChoCH/BOS相结合,能够得到更清晰、无重绘的分析锚点,更早提示潜在反转信号,同时支持多周期视角,有助于过滤低周期图表上的噪声。这套规则逻辑简单,便于自动化、日志记录与回测,非常适合封装为EA。

本文会完整讲解算法设计与MQL5实现方案:基于K线收盘的分形扫描、内存安全的分形数据存储、持久化绘图对象的安全绘制,以及事件处理模块,对每一个确认后的BOS/ChoCH信号生成日志并推送告警(桌面、移动端、声音提醒)。读完本文,您将得到一套可投入实盘的信号检测器,可以自行编译、测试和部署。

我们会先讲解策略逻辑,再介绍MQL5代码实现、告警、日志与通知配置,随后展示测试与结果,最后总结。目录如下。


策略逻辑

在本节中,我们会讲解这套系统背后的核心策略逻辑。该方法以分形指标以及由分形衍生出的结构信号为核心。如果您之前错过了相关文章,我们在之前的文章中介绍过分形突破趋势策略,分形的用途十分广泛:除了突破交易,它还能提供稳定的锚点,适用于各类价格行为分析。本文我们借助分形来识别行情性质转变(ChoCH)与结构突破(BOS)。

分形拐点是一个局部转折点,中间K线的高点(或低点)高于(或低于)对称数量的相邻K线。实际定义中,窗口长度记为g_length = 2*p + 1:如果中间K线的高点大于或等于窗口内所有K线高点,则构成上分形;如果中间K线的低点小于或等于窗口内所有K线低点,则构成下分形。分形信号具备稳定、无重绘的特性,因为识别判定必须等整个确认窗口的K线全部走完。

BOS是对近期市场结构的明确突破:价格收盘站上前期波段高点(看涨BOS),或是收盘跌破前期波段低点(看跌BOS)。BOS意味着动能以及中短期市场偏向沿着突破方向发生切换,交易者通常依靠该确认信号顺势入场。

ChoCH属于更早、偏预警性质的信号,代表市场原有趋势偏向正在发生变化。典型示例包括上涨趋势中无法创出更高的高点,或是下跌趋势中无法创出更低的低点。ChoCH应当视作预警信号:如果后续价格走势确认力度,ChoCH往往会领先BOS出现,交易者可以借此提前准备(收紧止损、降低仓位或寻找反转入场机会)。

重点在于为了避免信号重绘,所有信号仅使用已收盘K线进行判定。在实际操作中,也就是将已经走完的K线收盘价(例如前一根收盘价、当前收盘价)和已存储的分形价位进行对比,而绝对不在尚未收盘、仍在形成中的K线上判定BOS或ChoCH。采用收盘K线逻辑,可以保证信号可复现和支持回测。

这套算法设计简洁、判定规则确定:在收盘K线上识别可靠的分形锚点,监控收盘价对锚点的穿越;每一次突破只标记、绘图一次,每个确认事件仅输出一条日志与告警。这套收盘K线机制杜绝重绘,生成可复现、可回测的信号。

  • 高层流程

逻辑

  • 等待新K线收盘(以收盘K线的时间戳作为触发信号)。
  • 在长度为g_length = 2*p + 1的对称窗口中心位置检测分形。
  • 如果识别到分形,则存储该分形(时间、价格),并将标记位marked设置为false。
  • 每出现一根新收盘K线,将前收盘价、当前收盘价与每一条已存储分形的价位对比,判断是否发生穿越。
  • 一旦出现穿越(收盘确认突破),则标记该分形,绘制水平线/趋势线与锚定标签,同时输出一条日志与告警。
  • 定期清理过期分形,保证数组容量可控。

核心伪代码(精简版):

if new_closed_bar():
  ScanForFractals()            // detect & append new fractal anchors
  PruneFractals(maxKeepBars)   // remove very old anchors
  for each fractal in stored_fractals:
    if not fractal.marked and crossed(prevClose, curClose, fractal.price):
      DrawBreak(fractal)
      LabelAndLog(fractal)
      fractal.marked = true


MQL5实现

文件头与元数据

在文件开头,简洁注释包含关键元信息:文件名、作者、版权、参考链接。这些信息便于版本管理,后续复用或回看代码时也有据可查。 #property预处理指令控制编译行为;其中#property strict开启严格类型与API校验,便于在开发早期发现隐蔽错误。#include <stdlib.mqh>引入标准辅助库,封装常用功能,使得主代码更加整洁、易于维护。如果读者缺少该库文件,需要自行补充或者注释掉这条引入语句,避免编译报错。

//+------------------------------------------------------------------+
//|                                       Fractal Reaction System.mq5|
//|                               Copyright 2025, Christian Benjamin.|
//|                           https://www.mql5.com/en/users/lynnchris|
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Christian Benjamin."
#property link      "https://www.mql5.com/en/users/lynnchris"
#property version   "1.0"
#property strict

#include <stdlib.mqh>

用户输入

输入区域提供可配置参数,修改参数无需重新编译EA。AutoDetectLength 会根据图表周期自动选择合适的分形窗口长度;LengthInput 允许手动指定窗口大小。请注意,分形识别要求窗口长度为奇数(例如3、5或7),保证存在一根中心K线,两侧K线数量对称。ShowBull 和ShowBear 等显示开关以及颜色参数(BullColor、BearColor)优化可视化效果,便于在同时查看多张图表时快速识别信号。

HorizontalRightBars和HorizontalLeftExtend 控制线向左右延伸的K线数量,帮助判断关键价位的有效性。DebugMode开启调试日志,用于开发测试;MaxFractalHistoryBars限制保留的历史分形范围(以K线数量计),防止长时间运行时内存无限增长。

// User-configurable inputs
input bool   AutoDetectLength = false;
input int    LengthInput      = 5;
input bool   ShowBull         = true;
input color  BullColor        = clrLime;
input bool   ShowBear         = true;
input color  BearColor        = clrRed;
input int    HorizontalRightBars = 0;
input int    HorizontalLeftExtend = 3;
input bool   DebugMode        = false;
input int    MaxFractalHistoryBars = 2000;

全局变量与数据结构

全局变量用于维护运行时状态与持久化数据。g_chart_id保存图表ID,保证所有绘图对象绑定到正确图表。g_length 标识分形窗口总长度,p_half 表示窗口半长;二者在初始化时只计算一次,后续重复使用。ea_digits、ea_point和ea_point_pips用于适配不同经纪商、不同品种的价格精度,保证偏移量与标签位置的一致性。系统使用并行数组分别存储看涨、看跌分形(*_time[]、*_price[]、*_marked[]),时间戳和价格定位分形,marked标记用来防止重复触发信号。最后,os_state 记录市场偏向状态:0为中性,1为看涨,-1为看跌。

// Internal globals
long   g_chart_id;
int    g_length;
int    p_half;
int    ea_digits;
double ea_point, ea_point_pips;

datetime bull_time[];
double bull_price[];
bool   bull_marked[];

datetime bear_time[];
double bear_price[];
bool   bear_marked[];

int    os_state = 0; // 0: none, 1: bullish, -1: bearish

初始化(OnInit)

OnInit()负责初始化与入参合法性校验。该函数获取图表ID、确定分形窗口长度(自动识别或用户指定),设置最小值约束,强制窗口为奇数。计算p_half ,读取品种精度,得到point与pip值,用于精准绘图。将分形相关数组重置为空。开启DebugMode时,会打印初始化信息与关键参数,确认配置无误后再继续运行。

int OnInit()
{
    g_chart_id = ChartID();

    // Determine fractal length
    if(AutoDetectLength)
    {
        if(_Period <= PERIOD_H1)
            g_length = 5;
        else if(_Period <= PERIOD_H4)
            g_length = 7;
        else if(_Period <= PERIOD_D1)
            g_length = 9;
        else
            g_length = 11;
    }
    else
    {
        g_length = LengthInput;
    }

    // Ensure odd length >=3
    if(g_length < 3)
        g_length = 5;
    if((g_length % 2) == 0)
        g_length++;

    p_half = g_length / 2;

    // Get symbol info
    ea_digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
    ea_point = Point();
    ea_point_pips = ea_point;
    if(ea_digits == 3 || ea_digits == 5)
        ea_point_pips = Point() * 10.0;

    // Clear fractal arrays
    ArrayResize(bull_time,0);
    ArrayResize(bull_price,0);
    ArrayResize(bull_marked,0);
    ArrayResize(bear_time,0);
    ArrayResize(bear_price,0);
    ArrayResize(bear_marked,0);

    if(DebugMode)
        PrintFormat("EA INIT: AutoDetect=%s LengthInput=%d g_length=%d p_half=%d chart=%d",
                    AutoDetectLength ? "true" : "false", LengthInput, g_length, p_half, g_chart_id);
    return(INIT_SUCCEEDED);
}

清理(OnDeinit)

OnDeinit()在EA从图表上移除时执行清理工作。它调用CleanupObjectsByPrefix(),按统一的对象名前缀删除由EA创建的全部绘图对象(例如趋势线和标签)。这样可以防止孤立残留对象杂乱堆积在图表上,避免干扰后续分析或其他工具,对于实盘部署和演示场景十分重要。

void OnDeinit(const int reason)
{
    CleanupObjectsByPrefix("CHB_");
}

主循环(OnTick)

OnTick()每根K线收盘只执行一次的运行模型,保证判定逻辑具备确定性。该函数会读取最新已收盘K线的时间戳;如果自上次调用以来没有新K线收盘,则直接跳过后续流程。一旦检测到新收盘K线,OnTick() 会依次触发三个核心函数:ScanForFractals()识别新确认成立的分形;PruneFractals()删除超出设定历史保存上限的旧分形记录;ProcessFractalCrosses判断价格是否穿越任意一条已存储的分形价位。该执行顺序兼顾运行效率,同时保证所有识别与处理都基于已经走完的K线,这是回测结果可复现、实盘信号稳定可靠的关键。

void OnTick()
{
    static datetime last_checked = 0;
    datetime t = iTime(_Symbol, _Period, 1);
    if(t == last_checked)
        return;
    last_checked = t;

    ScanForFractals();
    PruneFractals(MaxFractalHistoryBars);
    ProcessFractalCrosses();
}

分形检测(ScanForFractals)

ScanForFractals()先校验是否有足够的历史K线数据,随后检查偏移量为p_half的中心K线,也就是分形窗口中间那根K线。函数调用IsFractalHighAtShift()和IsFractalLowAtShift(),判断该中心K线是否构成上分形或下分形。这两个辅助函数会将中心K线与两侧相邻K线逐一严格比对。如果识别出一个有效的分形,并且该分形尚未被记录(根据时间戳去重),程序就把它的时间戳和价格追加到对应数组中,并标记为未处理状态,供后续穿越检测使用。

void ScanForFractals()
{
    int bars = iBars(_Symbol, _Period);
    if(bars <= g_length)
        return;

    int centerShift = p_half;
    if(centerShift >= bars)
        return;

    // High fractal detection
    if(IsFractalHighAtShift(centerShift))
    {
        datetime t_fr = (datetime)iTime(_Symbol, _Period, centerShift);
        double p_fr = iHigh(_Symbol, _Period, centerShift);
        // Store if new
        bool exists = false;
        for(int i=0;i<ArraySize(bull_time);i++)
            if(bull_time[i]==t_fr)
                exists = true;
        if(!exists)
        {
            int n = ArraySize(bull_time);
            ArrayResize(bull_time, n+1);
            ArrayResize(bull_price, n+1);
            ArrayResize(bull_marked, n+1);
            bull_time[n] = t_fr;
            bull_price[n] = p_fr;
            bull_marked[n] = false;
            if(DebugMode)
                PrintFormat("FRAC_BULL DETECTED: t=%s price=%G", TimeToString(t_fr, TIME_DATE|TIME_SECONDS), p_fr);
        }
    }

    // Low fractal detection
    if(IsFractalLowAtShift(centerShift))
    {
        datetime t_fr = (datetime)iTime(_Symbol, _Period, centerShift);
        double p_fr = iLow(_Symbol, _Period, centerShift);
        // Store if new
        bool exists = false;
        for(int i=0;i<ArraySize(bear_time);i++)
            if(bear_time[i]==t_fr)
                exists = true;
        if(!exists)
        {
            int n = ArraySize(bear_time);
            ArrayResize(bear_time, n+1);
            ArrayResize(bear_price, n+1);
            ArrayResize(bear_marked, n+1);
            bear_time[n] = t_fr;
            bear_price[n] = p_fr;
            bear_marked[n] = false;
            if(DebugMode)
                PrintFormat("FRAC_BEAR DETECTED: t=%s price=%G", TimeToString(t_fr, TIME_DATE|TIME_SECONDS), p_fr);
        }
    }
}

分形校验辅助函数

IsFractalHighAtShift()与IsFractalLowAtShift()按照分形的定义,遍历中心K线两侧的对称窗口进行判断。只要任意一根相邻K线使得中心K线不再具备极值优势,或是完整窗口不可用,函数就返回false。这套严格校验可以避免提前判定或误识别出虚假分形,同时防止在历史数据起始位置或是切换周期后发生索引越界错误。如果需要处理大批量数据,可以考虑使用CopyHigh/CopyLow批量读取K线序列,减少反复调用单根K线API,以此提升运行性能。

bool IsFractalHighAtShift(int shift)
{
    int bars = iBars(_Symbol,_Period);
    int p = p_half;
    if(shift < 0 || shift >= bars)
        return false;

    double center = iHigh(_Symbol,_Period,shift);
    for(int k=-p; k<=p; k++)
    {
        if(k == 0)
            continue;
        int s = shift + k;
        if(s < 0 || s >= bars)
            return false; // incomplete window
        if(iHigh(_Symbol,_Period,s) > center)
            return false;
    }
    return true;
}

辅助函数:检查下分形

bool IsFractalLowAtShift(int shift)
{
    int bars = iBars(_Symbol,_Period);
    int p = p_half;
    if(shift < 0 || shift >= bars)
        return false;

    double center = iLow(_Symbol,_Period,shift);
    for(int k=-p; k<=p; k++)
    {
        if(k == 0)
            continue;
        int s = shift + k;
        if(s < 0 || s >= bars)
            return false;
        if(iLow(_Symbol,_Period,s) < center)
            return false;
    }
    return true;
}

穿越处理(ProcessFractalCrosses)

ProcessFractalCrosses()会校验最新已确认收盘价是否穿越任意分形价位,将已存储的分形水平转化为可交易信号。该函数采用严谨的收盘K线判定方式:对比prevClose(上一根已收盘K线)和curClose(最新已收盘 K 线),调用CrossedOver()或CrossedUnder()判断价格穿越。当检测到某个未处理过的分形发生穿越时,EA会生成唯一的绘图对象标识,根据os_state将事件归类为BOS或ChoCH,按需绘制突破线与标签,更新os_state状态,将该分形标记为已处理,并触发告警。每个分形只处理一次,保证逻辑具备确定性,非常适合回测。 

void ProcessFractalCrosses()
{
    double prevClose = iClose(_Symbol, _Period, 2);
    double curClose  = iClose(_Symbol, _Period, 1);
    datetime curTime = (datetime)iTime(_Symbol, _Period, 1);

    // Process bullish fractals
    for(int i=0; i<ArraySize(bull_time); i++)
    {
        if(bull_marked[i])
            continue;
        double level = bull_price[i];

        if(CrossedOver(prevClose, curClose, level))
        {
            datetime fr_time = bull_time[i];
            string tag = "CHB_BULL_" + IntegerToString((int)fr_time);
            bool isChoCH = (os_state == -1);
            string niceName = isChoCH ? "Bull ChoCH" : "Bull BOS";

            if(ShowBull)
            {
                DrawBreak(tag, fr_time, level, curTime, true);
                CreateAnchoredLabel(tag + "_lbl", niceName, fr_time, level + 3*ea_point, BullColor);
            }

            os_state = 1;
            bull_marked[i] = true;
            string msg = StringFormat("%s detected: %s %s at %s price=%s",
                niceName, _Symbol, TimeframeToString(_Period), TimeToString(curTime, TIME_DATE|TIME_MINUTES), DoubleToString(level, ea_digits));
            EmitLogAlert(msg);
        }
    }

    // Process bearish fractals
    for(int i=0; i<ArraySize(bear_time); i++)
    {
        if(bear_marked[i])
            continue;
        double level = bear_price[i];

        if(CrossedUnder(prevClose, curClose, level))
        {
            datetime fr_time = bear_time[i];
            string tag = "CHB_BEAR_" + IntegerToString((int)fr_time);
            bool isChoCH = (os_state == 1);
            string niceName = isChoCH ? "Bear ChoCH" : "Bear BOS";

            if(ShowBear)
            {
                DrawBreak(tag, fr_time, level, curTime, false);
                CreateAnchoredLabel(tag + "_lbl", niceName, fr_time, level - 3*ea_point, BearColor);
            }

            os_state = -1;
            bear_marked[i] = true;
            string msg = StringFormat("%s detected: %s %s at %s price=%s",
                niceName, _Symbol, TimeframeToString(_Period), TimeToString(curTime, TIME_DATE|TIME_MINUTES), DoubleToString(level, ea_digits));
            EmitLogAlert(msg);
        }
    }
}

告警输出(EmitLogAlert)

EmitLogAlert()统一管理通知逻辑。该函数会始终把信息打印到“专家”日志中,用于事后核查与复盘。可选择额外弹出桌面弹窗提醒、向已配置的MetaTrader 移动端推送消息,以及播放提示音。这套多渠道告警机制确保交易者无论当前是否盯盘,都能及时收到通知。如果需要推送消息,请先在终端内配置好MetaQuotes ID。

void EmitLogAlert(const string msg)
{
    Print(msg);

    if(EnableAlerts)
        Alert(msg);

    if(EnableNotifications)
        SendNotification(msg);

    if(EnableSound && StringLen(AlertSoundFile) > 0)
        PlaySound(AlertSoundFile);
}

绘图函数

DrawBreak()、CreateTrendLine()、CreateAnchoredLabel()负责图表图形绘制。DrawBreak()通过iBarShift()获取分形位置与突破时间对应的K线索引;线条向左侧(历史方向)延伸长度由HorizontalLeftExtend控制,同时可根据HorizontalRightBars设置,将线条右端向当前K线方向延展。CreateTrendLine()在两个时间点之间创建绑定锚点的趋势线,并设置显示样式。CreateAnchoredLabel()在指定时间和价位放置说明文字,并且做点位偏移,方便阅读。每一个绘图函数在新建对象前,都会调用SafeDelete()删除同名冲突对象,保证图表整洁且显示稳定。

void DrawBreak(const string tag, datetime fract_time, double fract_price, datetime break_time, bool bullish)
{
    int barFr = iBarShift(_Symbol, _Period, fract_time, false);
    int barBreak = iBarShift(_Symbol, _Period, break_time, false);
    int bars = iBars(_Symbol, _Period);
    if(barFr == -1 || barBreak == -1)
        return;

    int older_shift = MathMax(barFr, barBreak);
    int newer_shift = MathMin(barFr, barBreak);

    // Extend left
    older_shift = MathMin(older_shift + HorizontalLeftExtend, bars - 1);

    // Extend right towards current bar
    if(HorizontalRightBars > 0)
        newer_shift = MathMax(newer_shift - HorizontalRightBars, 0);

    // Swap if necessary
    if(older_shift < newer_shift)
    {
        int tmp = older_shift;
        older_shift = newer_shift;
        newer_shift = tmp;
    }

    datetime tLeft = (datetime)iTime(_Symbol, _Period, older_shift);
    datetime tRight = (datetime)iTime(_Symbol, _Period, newer_shift);

    string lineName = tag + "_line";
    CreateTrendLine(lineName, tLeft, fract_price, tRight, (bullish ? BullColor : BearColor), false);
}

void CreateAnchoredLabel(const string name, const string txt, datetime when, double price, color col)
{
    SafeDelete(name);
    if(ObjectCreate(g_chart_id, name, OBJ_TEXT, 0, when, price))
    {
        ObjectSetString(g_chart_id, name, OBJPROP_TEXT, txt);
        ObjectSetInteger(g_chart_id, name, OBJPROP_COLOR, (int)col);
        ObjectSetInteger(g_chart_id, name, OBJPROP_FONTSIZE, 10);
        ObjectSetInteger(g_chart_id, name, OBJPROP_BACK, false);
        ObjectMove(g_chart_id, name, 0, when, price);
    }
}

void CreateTrendLine(const string name, datetime tLeft, double price, datetime tRight, color col, bool dashed=false)
{
    SafeDelete(name);
    if(ObjectCreate(g_chart_id, name, OBJ_TREND, 0, tLeft, price, tRight, price))
    {
        ObjectSetInteger(g_chart_id, name, OBJPROP_COLOR, (int)col);
        ObjectSetInteger(g_chart_id, name, OBJPROP_WIDTH, 2);
        ObjectSetInteger(g_chart_id, name, OBJPROP_STYLE, dashed ? STYLE_DASH : STYLE_SOLID);
        ObjectSetInteger(g_chart_id, name, OBJPROP_BACK, false);
        ObjectSetInteger(g_chart_id, name, OBJPROP_SELECTABLE, false);
    }
}

void SafeDelete(const string name)
{
    if(ObjectFind(g_chart_id, name) >= 0)
        ObjectDelete(g_chart_id, name);
}

工具辅助函数

CrossedOver()和CrossedUnder()封装了收盘K线的穿越判定条件:CrossedOver满足上一根收盘价≤价位水平且当前收盘价>价位水平;CrossedUnder则与之相反。

bool CrossedOver(double prevClose, double curClose, double level)
{
    return (prevClose <= level && curClose > level);
}

bool CrossedUnder(double prevClose, double curClose, double level)
{
    return (prevClose >= level && curClose < level);
}

TimeframeToString()用于将周期常量转换为易于阅读的文本字符串,让日志输出更直观。

string TimeframeToString(int period)
{
    switch(period)
    {
        case PERIOD_M1: return "M1";
        case PERIOD_M5: return "M5";
        case PERIOD_M15: return "M15";
        case PERIOD_M30: return "M30";
        case PERIOD_H1: return "H1";
        case PERIOD_H4: return "H4";
        case PERIOD_D1: return "D1";
        case PERIOD_W1: return "W1";
        case PERIOD_MN1: return "MN1";
        default: return IntegerToString(period);
    }
}

CleanupObjectsByPrefix()会删除所有带有相同前缀的图表对象;它逆序遍历对象列表,避免删除过程中发生索引偏移错误。

void CleanupObjectsByPrefix(const string prefix)
{
    long total = ObjectsTotal(g_chart_id);
    for(int i=total-1; i>=0; i--)
    {
        string name = ObjectName(g_chart_id, i);
        if(StringLen(name) >= StringLen(prefix) && StringSubstr(name, 0, StringLen(prefix)) == prefix)
            ObjectDelete(g_chart_id, name);
    }
}

清理过期分形(PruneFractals)

PruneFractals()用于移除存储时长超过MaxFractalHistoryBars阈值的分形,以此保障程序性能与内存稳定。该函数借助写入指针对数组进行原地压缩,随后调整数组大小,规避不必要的内存分配操作。请牢记,清理逻辑中每一条已保存的分形都会调用一次iBarShift(),因此如果设置过大的存储上限,会增加处理耗时。选用一个合理的默认值(例如2000根K线),可以在历史数据覆盖范围和运行效率之间取得平衡。

void PruneFractals(int keepBars)
{
    if(keepBars <=0) return;

    // Prune bullish fractals
    int nB = ArraySize(bull_time);
    if(nB > 0)
    {
        int write = 0;
        for(int i=0; i<nB; i++)
        {
            int sh = iBarShift(_Symbol, _Period, bull_time[i], false);
            if(sh != -1 && sh <= keepBars)
            {
                bull_time[write] = bull_time[i];
                bull_price[write] = bull_price[i];
                bull_marked[write] = bull_marked[i];
                write++;
            }
        }
        if(write != nB)
        {
            ArrayResize(bull_time, write);
            ArrayResize(bull_price, write);
            ArrayResize(bull_marked, write);
        }
    }

    // Prune bearish fractals
    int nS = ArraySize(bear_time);
    if(nS > 0)
    {
        int write = 0;
        for(int i=0; i<nS; i++)
        {
            int sh = iBarShift(_Symbol, _Period, bear_time[i], false);
            if(sh != -1 && sh <= keepBars)
            {
                bear_time[write] = bear_time[i];
                bear_price[write] = bear_price[i];
                bear_marked[write] = bear_marked[i];
                write++;
            }
        }
        if(write != nS)
        {
            ArrayResize(bear_time, write);
            ArrayResize(bear_price, write);
            ArrayResize(bear_marked, write);
        }
    }
}

这款EA是一套模块化、专业级系统:在收盘K线上识别已确认有效的分形、高效存储分形数据、检测确认的价格穿越并生成BOS/ChoCH信号、在图表上绘制清晰标注,同时提供多渠道告警。各个模块在设计上兼顾可读性、结果可复现性与可维护性。如果用于实盘或者教学场景,建议读者先在模拟环境测试;在校验阶段开启调试模式(DebugMode)。


告警、日志与通知选项

该EA提供三条可配置的输出通道,保证不会错过信号事件:桌面弹窗、移动端推送和声音提示。可以同时开启全部通道,实现全方位提醒;也可以单独开启部分通道,降低提示干扰。

设置:

  • EnableAlerts —— 桌面弹窗,通过Alert()实现(终端运行时即时弹出、直观可见),适合盯盘时使用。
  • EnableNotifications —— 移动端推送,通过SendNotification()实现(需要在【工具 - 选项 - 通知】中填写MetaQuotes ID,并且在MetaTrader 手机端开启通知权限)。
  • EnableSound —— 通过PlaySound(文件名)播放本地提示音;音频文件必须放在终端的Sounds文件夹内,同时终端音量不能静音。

推荐告警消息格式(清晰、可解析):

Bull BOS detected: EURUSD H1 at 2025.08.01 14:00 price=1.12345 fr_time=2025.08.01 12:00

消息包含品种、周期、事件类型、确认时间、价格,可附带分形生成时间,方便事后核对复盘。

如需开启推送通知,请在终端选项里配置MetaQuotes ID,并确保手机端通知已开启。

对于声音告警,将WAV或MP3音频文件复制到终端的Sounds文件夹中,并在【选项 - 事件】中确认PlaySound功能正常。为了避免消息轰炸,程序依靠*_marked[]数组防止同一个分形重复触发告警;在行情剧烈波动时,还可以为每个品种增加冷却时间(例如30-120秒)。除此之外,为了方便审计与排障,测试阶段可以把事件追加写入CSV日志文件中 —— 使用FILE_COMMON标识,或者将文件放在终端的MQL5/Files目录;写入完成后务必及时关闭文件句柄,防止文件被锁定。


测试与结果

本节将展示该系统的表现,包含历史回测与实盘结果,在我看来,结果符合设计目标,运行表现和预期一致。我会展示回测配置与绩效指标、净值曲线以及部分样本交易,直观说明该信号检测器在实际场景下的运行效果;同时简要介绍实盘表现,附上实时截图,并和回测结果做对照校验。最后,我会总结该系统的局限性,以及后续进一步验证的建议方向。

我在EURUSD与Step Index两个品种的H1周期上执行了回测,测试结果展示如下。下方动图对ChoCH、BOS事件做了清晰标注与可视化,标记位置精准,可以很直观地看清“预警 — 确认”这一信号流程。

  • Step Index,H1周期:标注分形拐点(锚点)、ChoCH(未能创出更高的高点)以及看涨BOS(收盘确认)。

Step Index回测

  • EURUSD,H1周期:标注分形拐点(锚点)、ChoCH(未能创出更高的高点)以及看涨 BOS(收盘确认)。

EURUSD回测


我同时开展了实盘测试,验证实时运行效果,工具的表现完全符合预期。下方截图展示EA实时识别、标记、记录ChoCH和BOS事件:一旦K线收盘确认,桌面告警与图表绘图对象会立刻生成。实盘信号方向和回测信号保持一致;成交日志里仅能看到实盘市场固有的影响因素,也就是点差与滑点。

实盘演示 —— Volatility 75(1秒)指数,M1周期:实时ChoCH预警以及看涨BOS确认信号。

实盘Volatility 75(1秒)指数

这张实盘截图展示了系统如何依靠分形拐点提前发出ChoCH预警,再通过无重绘的BOS标记确认结构切换,从视觉上验证检测器的实时运行逻辑。

实盘演示 —— Step Index,M1周期:实时ChoCH预警以及看涨BOS确认信号

EA首先通过看跌ChoCH标记下跌阶段,并连续生成看跌BOS价位;随后在下跌动能衰竭时触发看涨ChoCH,再通过多条看涨BOS信号确认反转。这直观体现了系统的两大核心能力 —— ChoCH提前预警和BOS无重绘确认。

MetaTrader 5“专家”标签页中的日志记录如下。

2025.09.03 
10 :
20 :58.856 Fractal Reaction System (Volatility 75 (1s) Index,M1)   Alert: Bear BOS detected: Volatility 75 (1s) Index M1 at 2025.09.03 13:44 price=3446.40
2025.09.03 
10 :
20 :58.856 Fractal Reaction System (Volatility 75 (1s) Index,M1)   Bear BOS detected: Volatility 75 (1s) Index M1 at 2025.09.03 13:44 price=3446.73
2025.09.03 
10 :
20 :58.856 Fractal Reaction System (Volatility 75 (1s) Index,M1)   Alert: Bear BOS detected: Volatility 75 (1s) Index M1 at 2025.09.03 13:44 price=3446.73
2025.09.03 
10 :
20 :58.894 Fractal Reaction System (Step Index,M1) Bear BOS detected: Step Index M1 at 2025.09.03 13:44 price=8233.6
2025.09.03 
10 :
20 :58.894 Fractal Reaction System (Step Index,M1) Alert: Bear BOS detected: Step Index M1 at 2025.09.03 13:44 price=8233.6
2025.09.03 
10 :
20 :58.896 Fractal Reaction System (Volatility 75 (1s) Index,M1)   Alert: Bear BOS detected: Volatility 75 (1s) Index M1 at 2025.09.03 13:44 price=3447.86

无论是历史回测(EURUSD、Step Index,H1周期)还是实盘测试,这套基于分形的检测器均按设计逻辑稳定运行:分形拐点提供稳定、无重绘的锚点,ChoCH预警可以提前捕捉原有趋势动能衰减,BOS确认信号则可靠标记市场结构发生切换。回测生成的信号序列和动图标注内容一致(预警 — 确认流程);实盘截图也呈现出相同的信号模式,差异仅来自实盘固有因素:点差与滑点。


结论

分形响应系统将简单的分形拐点转化为稳定、无重绘的市场结构信号:ChoCH作为提前预警,BOS作为确认信号。本文介绍的EA具备内存安全特性,仅使用已收盘K线进行判定,保障结果可复现;可在图表上绘制持久化的结构标记,对每一个确认事件输出日志并推送告警,历史回测与实盘表现保持一致。它的核心优势在于可追溯(信号可核验)、可复现(收盘K线判定逻辑)、实用性强(桌面、移动端、声音多渠道通知,日志便于复盘核对)。

该工具仅为信号检测器,并非完整的交易管理系统:点差、滑点、成交情况等实盘执行因素都会影响最终收益,而ChoCH预警只提供行情参考,不构成直接交易指令。在投入实盘资金前,请在您自己的交易品种、经纪商环境下独立验证EA,仔细查阅配套的回测配置文件(.set)与事件日志;您也可以根据自身风险偏好,新增仓位管理规则、高周期过滤条件或者信号冷却机制。

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

附加的文件 |
最近评论 | 前往讨论 (4)
linfo2
linfo2 | 15 9月 2025 在 17:31
再次感谢您讲解清晰且富有创意的想法,stdlib_mq5里都有什么内容呢?如果将那部分注释掉,机器人也能正常运行,我只是对您的调整方法感兴趣。感谢您上传该文件。
Chris
Chris | 25 10月 2025 在 11:44

你好,


找不到文件 'C:\Users\Administrator\AppData\Roaming\MetaQuotes\Terminal\24F345EB9F291441AFE537834F9D8A19\MQL5\Include\stdlib_mq5.mqh' 未找到 Fractal_Reaction_System.mq5

我该去哪里获取该文件?


克里斯


Kevin Onsongo
Kevin Onsongo | 30 12月 2025 在 18:39
Chris #:

你好,


找不到文件 'C:\Users\Administrator\AppData\Roaming\MetaQuotes\Terminal\24F345EB9F291441AFE537834F9D8A19\MQL5\Include\stdlib_mq5.mqh' 未找到 Fractal_Reaction_System.mq5

我该去哪里获取该文件?


克里斯


看起来像是一种AI生成的模式。可能是用来清理代码的。AI代理有时会在此处引入不兼容的C/C++语法。如果删除这一行,代码就能编译通过。
Ryan L Johnson
Ryan L Johnson | 30 12月 2025 在 19:59
Kevin Onsongo #:
看起来像是一种AI生成的模式。可能是用来清理代码的。AI代理有时会在此处引入不兼容的C/C++语法。如果删除这一行,代码就能编译通过。

为明确起见,代码中的实际包含指令是:

#include <stdlib.mqh>

请注意,这里没有下划线分隔符。

交易中的神经网络:分解,而非扩大模型规模(结论) 交易中的神经网络:分解,而非扩大模型规模(结论)
本文将介绍一种算法,可以将时间序列分解为具备明确含义的多层分量,并基于这些分量构建简约模型。我们会系统阐述模型架构、基于MQL5/OpenCL的实际实现,以及基于历史行情数据开展的实际测试。
市场模拟:仓位视图(四) 市场模拟:仓位视图(四)
在这里,我们将开始将之前完全相互隔离的不同组件或应用程序整合在一起。Chart Trade、鼠标指标和 EA 已经相互连接,但仍然无法直接在图表上显示交易服务器上未平仓位,这些持仓通常使用对冲订单机制进行管理。从这一点开始,这成为可能,为各种想法和未来的实施开辟了道路。尽管我们才刚刚开始将这些组件投入运行,但我们已经有了进一步发展的方向。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
交易中的神经网络:以分解替代规模化 —— 模块构建 交易中的神经网络:以分解替代规模化 —— 模块构建
本文我们继续实操讲解SSCNN —— 一款能够处理碎片化时间序列的新一代架构方案。SSCNN不再盲目扩大模型规模,而是依靠精巧的模块化设计、精细化处理和针对性归一化。我们将在MQL5环境中逐步搭建计算单元,为可靠的预测分析打下基础。