价格行为分析工具包开发(第三十九部分):在MQL5中实现BOS与ChoCH的自动识别
引言
欢迎来到价格行为分析工具包开发系列的第三十九部分。在本文中,我们将搭建一套实用的MQL5系统,帮助价格行为交易者清晰解读图表,不必紧盯K线而错过市场结构的转变。目标很简单:把分形拐点与结构判定规则转化为稳定、无重绘的信号,无论是实盘交易还是历史回测都可以放心使用。
我们将分形拐点作为可靠的局部锚点,并识别两类互补信号: 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(收盘确认)。

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

我同时开展了实盘测试,验证实时运行效果,工具的表现完全符合预期。下方截图展示EA实时识别、标记、记录ChoCH和BOS事件:一旦K线收盘确认,桌面告警与图表绘图对象会立刻生成。实盘信号方向和回测信号保持一致;成交日志里仅能看到实盘市场固有的影响因素,也就是点差与滑点。
实盘演示 —— Volatility 75(1秒)指数,M1周期:实时ChoCH预警以及看涨BOS确认信号。
这张实盘截图展示了系统如何依靠分形拐点提前发出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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
交易中的神经网络:分解,而非扩大模型规模(结论)
市场模拟:仓位视图(四)
新手在交易中的10个基本错误
交易中的神经网络:以分解替代规模化 —— 模块构建
你好,
找不到文件 'C:\Users\Administrator\AppData\Roaming\MetaQuotes\Terminal\24F345EB9F291441AFE537834F9D8A19\MQL5\Include\stdlib_mq5.mqh' 未找到 Fractal_Reaction_System.mq5
我该去哪里获取该文件?
克里斯
你好,
找不到文件 'C:\Users\Administrator\AppData\Roaming\MetaQuotes\Terminal\24F345EB9F291441AFE537834F9D8A19\MQL5\Include\stdlib_mq5.mqh' 未找到 Fractal_Reaction_System.mq5
我该去哪里获取该文件?
克里斯
看起来像是一种AI生成的模式。可能是用来清理代码的。AI代理有时会在此处引入不兼容的C/C++语法。如果删除这一行,代码就能编译通过。
为明确起见,代码中的实际包含指令是:
#include <stdlib.mqh>请注意,这里没有下划线分隔符。