价格行为分析工具包开发(第三十七部分):市场情绪偏向计
引言
本文是价格行为分析工具包开发系列的第三十七部分。我的总体目标始终是开发实用工具,帮助交易者解读市场行为,并在适当的场景下,将部分分析流程自动化。迄今为止,我们已经介绍了独立的MQL5实用工具、外部集成方案以及混合解决方案。今天,我们通过增加另一个有价值且实用的组件,进一步扩展该工具包。
数据可视化常常因标签重叠、指标过多以及临时注释而变得杂乱无章,这些因素掩盖了潜在的信号变化及其原理。这种模糊性会阻碍决策,降低交易者对规则化交易系统的信心,同时也难以生成可靠的审计记录,而审计记录对于严谨的自动化交易至关重要。
STM通过将可配置时间框架内的K线级别方向性指标聚合为一个统一的、平滑的标量分数(范围从-100至+100)来解决这一问题。该分数显示在一个简洁、尽量避免元素重叠的紧凑型仪表盘布局中。持续保留的标注(如箭头和文本标签)会精确锚定K线收盘时间戳,确保每条警报都能通过时间戳和价格水平独立核验。
STM提供明确且可自定义参数的规则,例如翻转阈值、符号翻转幅度与持续时长、动量判定条件和各时间周期的权重等。用户可以调节灵敏度,在抑制噪声和响应速度之间取得平衡。
本文将先概述策略原理,再讲解MQL5实现方案,展示测试与结果,最后进行简要总结。详见以下内容目录:
策略概述
市场充满噪声,而噪声正是稳定交易决策最大的敌人。指标层层堆叠、标签互相重叠,很快就无法分辨究竟是什么驱动了价格变动,背后逻辑又是什么。STM的设计初衷就是拨开这片迷雾:输出单一、可核验的短期市场偏向读数,在图表上直接展示对应依据,让您无需猜测,做出严谨的交易决策。
本文提到的市场情绪,指市场短期偏向 —— 近期K线内多头买入与空头卖出的净力量。STM直接基于价格行为测算该偏向,将每一根已收盘的K线转化为一个易于解读的小分值。相关的微观特征包括:
- K线实体比率(CBR)
![]()
- 收盘价位置百分比(CPP)

- 基于波动率调整的距离(VAD)
![]()
其中,ATR为近期真实波幅基准;
Clamp ( x , a , b ) =max ( a , min ( b , x ) ) ,即限幅函数:把变量x限制在区间[a,b]之内。
每根K线的微分值为加权求和(示例):
![]()
随后将结果限幅至区间[-1, +1],再乘以置信系数与低波动市场系数。单个时间周期内的所有样本分值取平均值,并通过用户自定义权重融合多时间周期数据:
![]()
最后做指数平滑处理:
![]()
价格走势图与分值图如下:

分值图展示随时间变化的平滑情绪数值;原始符号穿越点用小号×标记。经过幅度、持续性和动量过滤后,被确认有效的情绪翻转信号使用大点标记。该可视化方式能够轻松区分容易产生假信号的小幅噪声波动,以及过滤器放行的真实多空转向。
在价格图表上呈现这些有效信号的位置。箭头、买入或卖出标签精准定位在信号触发价位,可以立刻核验该信号是否与支撑阻力位、趋势方向或邻近波段等关键价格结构匹配。
信号合格标准:EA产生的信号必须可追溯(记录时间与价格);在回测与短期前向测试中表现稳健;把明显的假阳性信号降低到具备可执行性的程度。如果满足以上条件,STM就达成了设计目标 —— 它提供清晰、可调、可核验的短期市场偏向指标,作为严谨交易流程中的一环。
MQL5实现
在开始编写代码前,请花两分钟完成环境配置,这能节省后续大量时间。确认已安装好MetaTrader 5以及MetaEditor。MetaEditor既可独立启动,也能通过MetaTrader 5内部打开,两种方式都可行。
EA准备步骤:
- 打开MetaEditor,
- 依次点击文件 → 新建 → EA(模板),
- 将EA源代码粘贴至新建文件,
- 使用含义清晰的名称保存文件,
- 按下F7键编译代码,
确认编译无错误或警告。如果编译器报错,检查并修正代码,重新编译直至构建完全通过。
返回至MetaTrader 5:
- 将EA加载至策略测试器,或者直接挂载至图表上,
- 观察仪表盘、日志与信号,
- 在Experts和Journal页签查看运行信息与调试内容。
这套环境配置可以保障高效开发与测试。接下来,我们将分步开发STM EA,引导您走完整个开发流程。
头部与元数据块用于设置EA标识、版权、作者主页链接以及编译模式(#property strict)。这部分代码仅做声明,但十分重要:它会在编译后的程序内嵌入版本信息与作者署名,同时启用更严格的编译检查。紧随其后,EA引入标准交易辅助库Trade.mqh,代码从而可以调用CTrade类以及其他交易相关工具。尽管这份示例代码对CTrade g_trade使用很少(甚至完全未调用),但提前引入该库,后续如需下单,无需重构代码。
//+------------------------------------------------------------------+ //| STM EA| //| Copyright 2025, MetaQuotes Ltd.| //| https://www.mql5.com/en/users/lynnchris| //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/en/users/lynnchris" #property version "1.0" #property strict #include <Trade\Trade.mqh>
输入参数块定义了所有可供用户配置的参数。输入参数按照用途分组:时间周期选择、信号算法调参、可视化布局与面板样式、声音/推送告警,以及箭头/标记的显示行为。主时间周期与可选的附加时间周期,使该EA能够融合多个时间周期的信号;权重参数(InpWeightPrimary、InpWeightExtra1、InpWeightExtra2)用于控制多周期信号的融合比例。平滑系数(InpSmoothAlpha)负责对融合后的原始分值执行指数平滑;阈值调参变量(例如InpFlipThreshold、InpPositiveZone、InpNegativeZone)用于判定EA何时识别一次大幅情绪翻转,或是市场进入多头/空头区间。
可视化相关输入参数控制图表上仪表盘的外观:尺寸、停靠角落、字体、颜色、柱形数量以及面板边距。箭头/标记类参数用于开启或调整图表上绘制的信号标记;同时多项安全校验条件(最小K线数量、符号翻转幅度、动量要求)可减少噪声与虚假情绪翻转信号。所有输入参数均显式声明类型,因此参数会展示在EA对话框中,用户无需修改代码即可完成调节。
// --- User inputs (layout / visuals tuned) ---------------------------- input ENUM_TIMEFRAMES InpTFPrimary = PERIOD_M5; input bool InpUseMultiTF = true; input ENUM_TIMEFRAMES InpExtraTF1 = PERIOD_M15; input ENUM_TIMEFRAMES InpExtraTF2 = PERIOD_H1; input int InpLookback = 20; input int InpSSWindow = 5; input int InpATRPeriod = 14; input double InpATRMultiplier = 1.0; input double InpVolQuietThreshold = 0.4; input int InpHistogramMaxBars = 24; input int InpPanelPadding = 10; input int InpPanelCorner = CORNER_LEFT_UPPER; input int InpPanelX = 5; input int InpPanelY = 25; input string InpFont = "Arial"; input int InpTitleFontSize = 12; input int InpScoreFontSize = 18; input int InpSmallFontSize = 9; input color InpPanelColor = clrBlack; input int InpPanelAlpha = 200; input bool InpEnableSound = true; input string InpSoundFile = "alert.wav"; input bool InpEnablePush = false; input double InpWeightPrimary = 0.6; input double InpWeightExtra1 = 0.2; input double InpWeightExtra2 = 0.2; input double InpSmoothAlpha = 0.28; input double InpFlipThreshold = 60.0; input double InpPositiveZone = 30.0; input double InpNegativeZone = -30.0; input int InpMinBarsForSignal = 3; // arrows / marker options input bool InpShowArrows = true; input int InpArrowFontSize = 16; input int InpArrowOffsetPoints = 8; input int InpMaxSignalsToKeep = 50; input bool InpSignalOnSignFlip = true; input double InpMinSignFlipAbs = 6.0; input int InpSignFlipHoldBars = 1; input bool InpRequireMomentum = true; input double InpMinMomentum = 0.5; //+------------------------------------------------------------------+接下来是有关常量与全局变量。EA会为每张图表生成唯一的g_prefix 前缀(品种 + 图表ID),用来给图表绘图对象划分命名空间,防止和其他EA、用户手动创建的图表对象发生冲突。代码中保存了各个时间周期ATR指标的句柄、柱形缓冲区(g_hist_buffer),以及用于管理循环存储的索引。全局变量还会维护平滑分值状态(g_smoothed_score、g_prev_smoothed),用于检测大幅情绪翻转的上一次告警分值,还有当前的区间状态g_zone_state (取值为1、-1或0)。代码声明了CTradeg_trade对象,以备后续扩展EA下单功能。保存g_last_signal_text与g_last_signal_time,供界面在面板上展示最近的交易动作。
// globals long g_chart_id = 0; string g_prefix = ""; string ui_bg_name = ""; string ui_shadow_name = ""; string ui_title_name = ""; string ui_score_name = ""; string ui_zone_name = ""; string ui_hist_base = ""; string ui_recent_name = ""; string ui_advice_name = ""; string ui_signal_name = ""; int g_atr_handle_primary = INVALID_HANDLE; int g_atr_handle_extra1 = INVALID_HANDLE; int g_atr_handle_extra2 = INVALID_HANDLE; double g_hist_buffer[]; int g_hist_idx = 0; int g_hist_count = 0; double g_smoothed_score = 0.0; double g_prev_smoothed = 0.0; double g_last_alert_score = 0.0; int g_zone_state = 0; CTrade g_trade; string g_last_signal_text = "None"; datetime g_last_signal_time = 0; const string BASE_HIST = "STM_HBAR_"; //+------------------------------------------------------------------+代码中包含若干小型工具函数,用于提升界面与绘图显示代码的健壮性。ARGB_uint根据透明度通道值与MQL颜色值,组合生成一个ARGB无符号整数,方便简洁地设置半透明矩形背景。EstimateTextWidth是轻量型字符串像素宽度估算函数:依据字体大小和经过调优的字符宽度系数计算文本宽度。绘制区间标识与最新信号文本时,依靠该函数防止标签互相重叠。对于图表内嵌面板而言,这类近似算法是合理的,因为精确布局并非硬性要求,但图形元素互相遮挡会严重影响观感。
// ARGB helper - returns a color as unsigned int uint ARGB_uint(int a, color c) { uint u = ((uint)c) & 0x00FFFFFF; return ((((uint)a) & 0xFF) << 24) | u; } // approximate text width estimator (pixels) int EstimateTextWidth(string txt,int fontSize) { if(StringLen(txt) <= 0) return 6; double factor = 0.58; int w = (int)MathRound(StringLen(txt) * fontSize * factor); return MathMax(8, w); } //+------------------------------------------------------------------+DrawText和DrawCell是用来创建和配置图表标签、矩形标签的安全封装函数。这两个函数将对象创建与属性设置逻辑集中封装(包含停靠角落、X/Y偏移距离、字体、可选标记、图层顺序等参数),保证所有界面标签都遵循统一规范。使用这类辅助函数可以避免大量重复样板代码,同时在面板重建或刷新时,保证表现效果一致。
// safe DrawText - sets font explicitly and uses exact x/y void DrawText(string id,string txt,int x,int y,color clr,int sz) { if(ObjectFind(g_chart_id,id) < 0) ObjectCreate(g_chart_id,id,OBJ_LABEL,0,0,0); ObjectSetInteger(g_chart_id,id,OBJPROP_CORNER,InpPanelCorner); ObjectSetInteger(g_chart_id,id,OBJPROP_XDISTANCE,x); ObjectSetInteger(g_chart_id,id,OBJPROP_YDISTANCE,y); ObjectSetInteger(g_chart_id,id,OBJPROP_COLOR,(int)clr); ObjectSetInteger(g_chart_id,id,OBJPROP_FONTSIZE,sz); ObjectSetString(g_chart_id,id,OBJPROP_FONT,InpFont); ObjectSetString(g_chart_id,id,OBJPROP_TEXT,txt); ObjectSetInteger(g_chart_id,id,OBJPROP_SELECTABLE,false); ObjectSetInteger(g_chart_id,id,OBJPROP_ZORDER,0); } // small helper to draw rectangle label cell void DrawCell(string id,int x,int y,int w,int h,color bg,color br) { if(ObjectFind(g_chart_id,id) < 0) ObjectCreate(g_chart_id,id,OBJ_RECTANGLE_LABEL,0,0,0); ObjectSetInteger(g_chart_id,id,OBJPROP_CORNER,InpPanelCorner); ObjectSetInteger(g_chart_id,id,OBJPROP_XDISTANCE,x); ObjectSetInteger(g_chart_id,id,OBJPROP_YDISTANCE,y); ObjectSetInteger(g_chart_id,id,OBJPROP_XSIZE,w); ObjectSetInteger(g_chart_id,id,OBJPROP_YSIZE,h); ObjectSetInteger(g_chart_id,id,OBJPROP_BGCOLOR,(int)bg); ObjectSetInteger(g_chart_id,id,OBJPROP_BORDER_COLOR,(int)br); ObjectSetInteger(g_chart_id,id,OBJPROP_SELECTABLE,false); ObjectSetInteger(g_chart_id,id,OBJPROP_ZORDER,1); }CreateUIObjects和DeleteUIObjects负责管理全部界面元素的生命周期。CreateUIObjects预先创建背景、阴影、标题、分值、区间状态、最新信号以及柱状图标签占位对象,并设置默认字体与图层顺序。DeleteUIObjects在反初始化阶段清理这些绘图对象。这是一项良好的编程习惯,EA被移除后,图表会保持整洁。基于参数InpHistogramMaxBars,预先创建固定数量的柱状图标签对象,避免程序运行时频繁新建图表对象,以此提升运行性能与稳定性。
void CreateUIObjects() { int default_w = 200; int default_h = 80; int scol = (int)ARGB_uint(InpShadowAlpha, InpShadowColor); DrawCell(ui_shadow_name, InpPanelX + 3, InpPanelY + 3, default_w, default_h, (color)scol, InpGridClr); DrawCell(ui_bg_name, InpPanelX, InpPanelY, default_w, default_h, InpPanelBG, InpGridClr); DrawText(ui_title_name, "", InpPanelX + InpPanelPadding, InpPanelY + InpPanelPadding, InpTxtClr, InpTitleFontSize); DrawText(ui_score_name, "", InpPanelX + InpPanelPadding, InpPanelY + InpPanelPadding + InpTitleFontSize + 4, InpTxtClr, InpScoreFontSize); DrawText(ui_zone_name, "", InpPanelX + default_w - InpPanelPadding - 80, InpPanelY + InpPanelPadding + 4, InpTxtClr, InpSmallFontSize); DrawText(ui_advice_name, "", InpPanelX + InpPanelPadding, InpPanelY + InpPanelPadding + InpTitleFontSize + InpScoreFontSize + 8, InpTxtClr, InpSmallFontSize); DrawText(ui_recent_name, "", InpPanelX + InpPanelPadding, InpPanelY + default_h - InpPanelPadding - 18, InpTxtClr, InpSmallFontSize); DrawText(ui_signal_name, "", InpPanelX + InpPanelPadding + 120, InpPanelY + InpPanelPadding + InpTitleFontSize + InpScoreFontSize + 8, InpTxtClr, InpSmallFontSize); for(int i = 0; i < InpHistogramMaxBars; i++) { string name = ui_hist_base + IntegerToString(i); if(ObjectFind(g_chart_id, name) < 0) { ObjectCreate(g_chart_id, name, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(g_chart_id, name, OBJPROP_CORNER, InpPanelCorner); ObjectSetString(g_chart_id, name, OBJPROP_FONT, InpFont); ObjectSetInteger(g_chart_id, name, OBJPROP_FONTSIZE, InpSmallFontSize); ObjectSetInteger(g_chart_id, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(g_chart_id, name, OBJPROP_ZORDER,0); } } } void DeleteUIObjects() { if(g_chart_id == 0) return; if(ObjectFind(g_chart_id, ui_shadow_name) >= 0) ObjectDelete(g_chart_id, ui_shadow_name); if(ObjectFind(g_chart_id, ui_bg_name) >= 0) ObjectDelete(g_chart_id, ui_bg_name); if(ObjectFind(g_chart_id, ui_title_name) >= 0) ObjectDelete(g_chart_id, ui_title_name); if(ObjectFind(g_chart_id, ui_score_name) >= 0) ObjectDelete(g_chart_id, ui_score_name); if(ObjectFind(g_chart_id, ui_zone_name) >= 0) ObjectDelete(g_chart_id, ui_zone_name); if(ObjectFind(g_chart_id, ui_recent_name) >= 0) ObjectDelete(g_chart_id, ui_recent_name); if(ObjectFind(g_chart_id, ui_advice_name) >= 0) ObjectDelete(g_chart_id, ui_advice_name); if(ObjectFind(g_chart_id, ui_signal_name) >= 0) ObjectDelete(g_chart_id, ui_signal_name); for(int i = 0; i < InpHistogramMaxBars; i++) { string name = ui_hist_base + IntegerToString(i); if(ObjectFind(g_chart_id, name) >= 0) ObjectDelete(g_chart_id, name); } }
RefreshUI是面板布局与柱状图实际进行计算和绘制的核心函数。该函数计算字体高度,为标题、分值、小字说明以及柱状图行预留空间;动态计算面板宽度,保证柱状图完整显示,同时设置合理的最小宽度下限。它绘制阴影与背景矩形框,调用EstimateTextWidth计算区间标识的摆放位置,防止与标题重叠;并根据阈值为平滑分值选取对应颜色scoreCol。“Recent”这一行文本根据主时间周期最新K线数据生成(收盘价相对开盘价及其波幅),如果文本过长则用省略号截断。
对于柱形部分,函数计算文本符号块的数量,用来可视化表达分值大小;将每个符号绑定到预先创建好的标签对象,并根据分值正负选择颜色。该子程序加入了防御性判断(检测可用空间且必要时缩减间距),设计目标是避免标签互相重叠,同时保持面板紧凑、易于阅读。
void RefreshUI(double rawScore, double smoothScore) { int title_h = InpTitleFontSize + 6; int score_h = InpScoreFontSize + 8; int small_h = InpSmallFontSize + 4; int hist_area_h = 22; int gap_between_sections = 6; int hist_px_step = 7; int hist_px_width = InpHistogramMaxBars * hist_px_step; int panel_width = MathMax(340, hist_px_width + InpPanelPadding*2 + 30); int extra_recent_space = small_h + 12; int panel_height = InpPanelPadding*2 + title_h + score_h + small_h + hist_area_h + (gap_between_sections * 4) + extra_recent_space; int scol = (int)ARGB_uint(InpShadowAlpha, InpShadowColor); DrawCell(ui_shadow_name, InpPanelX + 3, InpPanelY + 3, panel_width, panel_height, (color)scol, InpGridClr); DrawCell(ui_bg_name, InpPanelX, InpPanelY, panel_width, panel_height, InpPanelBG, InpGridClr); int x_start = InpPanelX + InpPanelPadding; int y_title = InpPanelY + InpPanelPadding; string titleText = StringFormat("Sentiment Tilt Meter — %s [%s]", Symbol(), TFToText(InpTFPrimary)); DrawText(ui_title_name, titleText, x_start, y_title, InpTxtClr, InpTitleFontSize); // ... zone, score, advice, recent and histogram painting (omitted here for brevity) // The full EA code contains the rest and iterates label objects to draw histogram blocks. }TFToText是一个简易辅助函数,用于将枚举时间周期类型ENUM_TIMEFRAMES 转换为简短的可读文本标签(M1、M5、M15、H1等)。该函数用于面板标题,让用户随时知晓EA当前所参考的主时间周期。
string TFToText(ENUM_TIMEFRAMES tf) { switch(tf) { 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 "MN"; default: return IntegerToString((int)tf); } }
OnInit函数负责初始化工作:获取图表ID并生成绘图对象前缀;调整柱状图缓冲区大小并完成初始化;在启用多时间周期模式时,为指定周期创建ATR指标句柄(iATR);调用ComputeFusedSentiment计算初始融合原始情绪分值;设置平滑分值与区间状态;创建界面元素,并调用RefreshUI绘制初始面板;同时启用1秒定时器(EventSetTimer(1)),供OnTimer函数检测主周期新K线生成。该设计采用第一个原始分值初始化平滑序列,保证首次展示的平滑分值稳定。请注意,代码会提前调用UpdateZoneState,确保初始区间状态正确。
OnDeinit在EA被移除时执行清理操作:关闭定时器、删除界面对象,并依据g_prefix + "SIG_"命名规则,清除由该EA生成的所有信号标记。同时通过IndicatorRelease释放ATR指标句柄。这样可以干净地释放资源,避免图表残留多余标签,或是出现指标句柄资源泄漏。
int OnInit() { g_chart_id = ChartID(); g_prefix = Symbol() + "_" + IntegerToString((int)g_chart_id) + "_"; ui_bg_name = g_prefix + "BG"; ui_shadow_name = g_prefix + "SHDW"; ui_title_name = g_prefix + "TITLE"; ui_score_name = g_prefix + "SCORE"; ui_zone_name = g_prefix + "ZONE"; ui_hist_base = g_prefix + BASE_HIST; ui_recent_name = g_prefix + "RECENT"; ui_advice_name = g_prefix + "ADVICE"; ui_signal_name = g_prefix + "LASTSIG"; ArrayResize(g_hist_buffer, InpHistogramMaxBars); ArrayInitialize(g_hist_buffer, 0.0); g_hist_idx = 0; g_hist_count = 0; g_atr_handle_primary = iATR(Symbol(), InpTFPrimary, InpATRPeriod); if(InpUseMultiTF) { g_atr_handle_extra1 = iATR(Symbol(), InpExtraTF1, InpATRPeriod); g_atr_handle_extra2 = iATR(Symbol(), InpExtraTF2, InpATRPeriod); } double raw_init = ComputeFusedSentiment(); g_smoothed_score = raw_init; g_prev_smoothed = g_smoothed_score; g_last_alert_score = raw_init; UpdateZoneState(raw_init); CreateUIObjects(); RefreshUI(raw_init, g_smoothed_score); EventSetTimer(1); return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { EventKillTimer(); DeleteUIObjects(); // delete signal markers created by this EA (prefix-based) string name; int total = ObjectsTotal(g_chart_id); for(int i = total - 1; i >= 0; --i) { name = ObjectName(g_chart_id, i); if(StringFind(name, g_prefix + "SIG_") == 0) ObjectDelete(g_chart_id, name); } if(g_atr_handle_primary != INVALID_HANDLE) IndicatorRelease(g_atr_handle_primary); if(g_atr_handle_extra1 != INVALID_HANDLE) IndicatorRelease(g_atr_handle_extra1); if(g_atr_handle_extra2 != INVALID_HANDLE) IndicatorRelease(g_atr_handle_extra2); }该EA使用OnTimer 作为核心轮询机制。它读取当前主时间周期K线的时间戳(iTime(Symbol(), InpTFPrimary, 0)) ,如果该时间戳与上一次处理的时间戳不一致,则调用HandleNewPrimaryBar。这种方案不会在每次定时器触发时都执行计算,仅在每一根新K线收盘后执行一次。当您需要基于K线生成信号,并且选用定时器而非OnTick机制时,这种设计模式非常合适。
datetime g_last_primary_time = 0; void OnTimer() { datetime t = iTime(Symbol(), InpTFPrimary, 0); if(t == g_last_primary_time) return; g_last_primary_time = t; HandleNewPrimaryBar(); } void HandleNewPrimaryBar() { double raw = ComputeFusedSentiment(); double alpha = MathMax(0.0, MathMin(1.0, InpSmoothAlpha)); g_prev_smoothed = g_smoothed_score; g_smoothed_score = alpha * raw + (1.0 - alpha) * g_smoothed_score; g_hist_buffer[g_hist_idx] = g_smoothed_score; g_hist_idx = (g_hist_idx + 1) % InpHistogramMaxBars; if(g_hist_count < InpHistogramMaxBars) g_hist_count++; RefreshUI(raw, g_smoothed_score); ProcessAlerts(raw, g_smoothed_score); }
HandleNewPrimaryBar函数通过ComputeFusedSentiment计算最新的原始融合情绪分值;使用配置好的平滑系数alpha执行指数平滑(g_smoothed_score = alpha * raw + (1-alpha) * prev);将平滑后的分值存入循环柱状图缓冲区,更新索引与有效数据计数,刷新界面,并调用ProcessAlerts检查新平滑分值是否需要触发消息或交易信号。循环缓冲区利用g_hist_idx对InpHistogramMaxBars取模实现,g_hist_count用于记录有效数据条目数量。该机制保存滚动历史数据,供可视化柱形以及符号持续校验使用。
ComputeFusedSentiment是顶层分值聚合函数。它对三个权重输入的绝对值做归一化处理,使权重总和等于1(如果权重总和为0,则自动回退)。接下来,函数调用ComputeTFScore计算主时间周期分值,如果开启多周期模式,则额外计算两个附加时间周期的分值。最后,计算加权平均值,并将融合结果限幅在区间[-100,100]。这样一来,无论单个时间周期原始分值的尺度如何变化,后续界面渲染与阈值判断逻辑都可以保持统一。
double ComputeFusedSentiment() { double w1 = MathAbs(InpWeightPrimary); double w2 = MathAbs(InpWeightExtra1); double w3 = MathAbs(InpWeightExtra2); double sum = w1 + w2 + w3; if(sum <= 0.0) { w1 = 1.0; w2 = 0.0; w3 = 0.0; sum = 1.0; } w1 /= sum; w2 /= sum; w3 /= sum; double s1 = ComputeTFScore(Symbol(), InpTFPrimary, InpLookback, InpSSWindow, g_atr_handle_primary); if(!InpUseMultiTF) return s1; double s2 = ComputeTFScore(Symbol(), InpExtraTF1, InpLookback, InpSSWindow, g_atr_handle_extra1); double s3 = ComputeTFScore(Symbol(), InpExtraTF2, InpLookback, InpSSWindow, g_atr_handle_extra2); double fused = s1 * w1 + s2 * w2 + s3 * w3; return MathMax(-100.0, MathMin(100.0, fused)); } double ComputeTFScore(string sym, ENUM_TIMEFRAMES tf, int lookback, int ssWindow, int atrHandle) { MqlRates rates[]; int needed = MathMax(lookback, ssWindow) + 6; int copied = CopyRates(sym, tf, 0, needed, rates); if(copied <= 0) return 0.0; double atr = 0.0001; if(atrHandle != INVALID_HANDLE) { double abuf[]; if(CopyBuffer(atrHandle, 0, 1, 1, abuf) > 0) atr = abuf[0] * InpATRMultiplier; } else { int rc = MathMin(10, copied - 1); double ar = 0.0; for(int i = 1; i <= rc; i++) ar += (rates[i].high - rates[i].low); if(rc > 0) ar /= rc; if(ar > 0) atr = ar * 0.6; } int limit = MathMin(ssWindow, copied - 1); double avgRange = 0.0; int vr = MathMin(10, copied - 1); for(int k = 1; k <= vr; k++) avgRange += (rates[k].high - rates[k].low); if(vr > 0) avgRange /= vr; double sumSS = 0.0; int count = 0; for(int i = 1; i <= limit; i++) { double open = rates[i].open; double close = rates[i].close; double high = rates[i].high; double low = rates[i].low; double range = high - low; if(range <= 0.0) continue; double dir = (close > open) ? 1.0 : -1.0; double cbr = (MathAbs(close - open) / range) * dir; double cpp = (((close - low) / range) - 0.5) * 2.0 * dir; double vad = 0.0; if(atr > 0.0) vad = (range / atr) - 1.0; vad = MathMax(-1.0, MathMin(3.0, vad)); double ss = cbr * 0.4 + cpp * 0.3 + vad * 0.3; ss = MathMax(-1.0, MathMin(1.0, ss)); bool conf = false; if((i - 1) >= 0 && (i - 1) <= copied - 1) { double nOpen = rates[i - 1].open; double nClose = rates[i - 1].close; if(dir > 0.0 && nClose > nOpen) conf = true; if(dir < 0.0 && nClose < nOpen) conf = true; } double confFactor = conf ? 1.0 : 0.6; double volFactor = 1.0; if(atr > 0.0 && avgRange / atr < InpVolQuietThreshold) volFactor = 0.6; double finalSS = ss * confFactor * volFactor; sumSS += finalSS; count++; } if(count == 0) return 0.0; double avgSS = sumSS / count; return avgSS * 100.0; }
ComputeTFScore是计算单个时间周期核心信号特征的函数。它读取指定时间周期内一段最新的MqlRates K线数据,并计算ATR的替代值:如果ATR指标句柄存在,则直接读取指标缓冲区;否则采用简易平均真实波幅作为备用计算方案。代码算出平均波幅avgRange,然后遍历最多ssWindow根历史K线,为每根K线计算三个归一化特征:cbr(实体比率:收盘价相对开盘价,结合波幅,捕捉方向与力度)、cpp(收盘价在K线内部的位置,归一化并附加方向偏向)和vad(基于波动率调整的距离:相对ATR的波幅减1)。每个特征分别赋予权重(0.4、0.3、0.3),限幅到区间[-1,1],再乘以两个置信系数:通过confFactor校验前一根K线是否确认当前方向;当市场波动相对ATR明显偏低时,通过volFactor降低分值贡献。
单根K线最终情绪样本值finalSS在窗口内累加并求平均,再乘以100,得到便于展示的百分比区间。简言之,该子程序将方向偏向、K线内收盘位置、相对波幅大小,以及简单的跨K线确认逻辑,编码为单一的单周期分值。
RecentSmoothedSignHold是供符号翻转逻辑调用的辅助函数,用于要求最近若干根K线的平滑分值必须统一为新的正负符号。函数反向遍历环形缓冲区g_hist_buffer,只要任意一根所需校验的近期平滑分值符号不满足条件,就会返回false。该机制可以避免单根异常K线造成信号闪烁。
ProcessAlerts 是消息推送与图表信号的决策引擎。首先校验历史数据是否足够(InpMinBarsForSignal);接着判断是否发生大幅突变翻转(与上一次告警分值g_last_alert_score的差值绝对值超过InpFlipThreshold)。如果满足条件,则触发告警,更新区间状态,并可选择绘制BUY/SELL文本标记。如果不属于大幅翻转,则根据InpPositiveZone/InpNegativeZone判定新区间状态。如果开启InpSignalOnSignFlip,还会检测分值符号翻转(由正转负或由负转正),并执行三项校验:新平滑分值的最小绝对值(InpMinSignFlipAbs)、符号持续保持K线数量(InpSignFlipHoldBars)和可选动量条件(InpRequireMomentum + InpMinMomentum)只有通过全部校验,才认定本次符号翻转为有效真实信号,在图表上绘制标记,更新g_last_alert_score,并设置g_zone_state。
如果区间状态发生常规切换(进入多头区间或空头区间),代码同样触发告警,记录最新信号内容与时间,并绘制标记。最终形成一套分层、偏保守的信号策略:优先识别清晰的结构性行情变化,过滤震荡杂波。
bool RecentSmoothedSignHold(int bars, int desiredSign) { if(bars <= 1) return true; if(g_hist_count < bars) return false; int max = InpHistogramMaxBars; int idx = (g_hist_idx - 1 + max) % max; for(int i = 0; i < bars; i++) { double v = g_hist_buffer[(idx - i + max) % max]; if(desiredSign == 1 && v <= 0.0) return false; if(desiredSign == -1 && v >= 0.0) return false; } return true; } void ProcessAlerts(double rawScore, double smoothScore) { if(g_hist_count < InpMinBarsForSignal) { PrintFormat("%s: waiting hist_count=%d (need %d)", __FUNCTION__, g_hist_count, InpMinBarsForSignal); return; } // large flip detection if(MathAbs(smoothScore - g_last_alert_score) >= InpFlipThreshold) { string m = StringFormat("STM Flip: %.1f -> %.1f on %s", g_last_alert_score, smoothScore, Symbol()); SendAlert(m); g_last_alert_score = smoothScore; UpdateZoneState(smoothScore); int newZoneFlip = 0; if(smoothScore >= InpPositiveZone) newZoneFlip = 1; else if(smoothScore <= InpNegativeZone) newZoneFlip = -1; if(newZoneFlip != 0) { g_last_signal_text = (newZoneFlip == 1) ? "BUY" : "SELL"; g_last_signal_time = TimeCurrent(); DrawSignalOnChart(newZoneFlip, smoothScore); PruneOldSignals(InpMaxSignalsToKeep); } RefreshUI(rawScore, g_smoothed_score); return; } }UpdateZoneState是一个简易函数,根据平滑分值以及配置好的多头/空头区间阈值,将g_zone_state设置为1、-1或0。SendAlert函数统一处理告警推送:如果开启声音告警则播放提示音;开启推送通知则发送移动端推送;同时调用Alert()弹出终端本地提示框,并调用Print()将信息写入日志。告警逻辑集中封装,便于后续扩展功能(例如接入邮件或者Webhook)。
void UpdateZoneState(double smoothScore) { if(smoothScore >= InpPositiveZone) g_zone_state = 1; else if(smoothScore <= InpNegativeZone) g_zone_state = -1; else g_zone_state = 0; } void SendAlert(string msg) { if(InpEnableSound) PlaySound(InpSoundFile); if(InpEnablePush) SendNotification(msg); Alert(msg); Print(__FILE__, ": ", msg); }
DrawSignalOnChart负责将买入/卖出文本与箭头符号精准绘制在最新一根已收盘主周期K线(索引1)上。函数利用时间戳和g_hist_idx生成唯一对象名,保证多个信号标记相互独立;计算小幅价格偏移量,将文本标签放在箭头上方/下方,而箭头本身严格定位在信号价位;同时创建两个图表对象:一个文本标签和一个箭头符号。该EA支持简单的箭头样式选择,并设置字体、颜色、图层顺序以及不可选中属性,避免这些标注干扰交易者在图表上的手动操作。生成信号后,函数会调用PruneOldSignals,防止图表堆积过多标记。
PruneOldSignals是实用的清理子程序:遍历全部图表对象,筛选名称以EA前缀+SIG_开头的对象,提取对象名中编码的时间戳;按时间戳排序(通过循环移除时间戳最早的对象实现),删除最旧信号,仅保留设定数量。这样确保图表上最多只保留InpMaxSignalsToKeep个最新标记,防止图表杂乱;在保留数量合理的前提下,也能降低遍历图表对象的开销。
void DrawSignalOnChart(int zone, double score) { datetime tm = iTime(Symbol(), InpTFPrimary, 1); double price = iClose(Symbol(), InpTFPrimary, 1); if(tm == 0 || price == 0.0) return; double point = SymbolInfoDouble(Symbol(), SYMBOL_POINT); double textOffsetPts = MathMax(1, InpArrowOffsetPoints); double textOffset = point * textOffsetPts * 1.5; string name = g_prefix + "SIG_" + IntegerToString((int)tm) + "_" + IntegerToString(g_hist_idx); string arrName = g_prefix + "SIG_ARR_" + IntegerToString((int)tm) + "_" + IntegerToString(g_hist_idx); if(ObjectFind(g_chart_id, name) >= 0) ObjectDelete(g_chart_id, name); if(ObjectFind(g_chart_id, arrName) >= 0) ObjectDelete(g_chart_id, arrName); double text_price = (zone == 1) ? price + textOffset : price - textOffset; if(ObjectCreate(g_chart_id, name, OBJ_TEXT, 0, tm, text_price)) { string txt = (zone == 1) ? "BUY" : ((zone == -1) ? "SELL" : "NEUTRAL"); color col = (zone == 1) ? clrLime : ((zone == -1) ? clrRed : clrSilver); ObjectSetString(g_chart_id, name, OBJPROP_TEXT, txt); ObjectSetInteger(g_chart_id, name, OBJPROP_COLOR, (int)col); ObjectSetInteger(g_chart_id, name, OBJPROP_FONTSIZE, 12); ObjectSetString(g_chart_id, name, OBJPROP_FONT, InpFont); ObjectSetInteger(g_chart_id, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(g_chart_id, name, OBJPROP_ZORDER, 2); } if(InpShowArrows) { string arrowTxt = (zone == 1) ? "▲" : ((zone == -1) ? "v" : "■"); if(ObjectCreate(g_chart_id, arrName, OBJ_TEXT, 0, tm, price)) { color acol = (zone == 1) ? clrLime : ((zone == -1) ? clrRed : clrSilver); ObjectSetString(g_chart_id, arrName, OBJPROP_TEXT, arrowTxt); ObjectSetInteger(g_chart_id, arrName, OBJPROP_COLOR, (int)acol); ObjectSetInteger(g_chart_id, arrName, OBJPROP_FONTSIZE, InpArrowFontSize); ObjectSetString(g_chart_id, arrName, OBJPROP_FONT, InpFont); ObjectSetInteger(g_chart_id, arrName, OBJPROP_SELECTABLE, false); ObjectSetInteger(g_chart_id, arrName, OBJPROP_ZORDER, 3); } } PruneOldSignals(InpMaxSignalsToKeep); } void PruneOldSignals(int keep) { if(keep <= 0) return; int total = ObjectsTotal(g_chart_id); string names[]; int times[]; int n = 0; string sigPrefix = g_prefix + "SIG_"; for(int i = 0; i < total; i++) { string nm = ObjectName(g_chart_id, i); if(StringFind(nm, sigPrefix) == 0) { string rest = StringSubstr(nm, StringLen(sigPrefix)); int pos = StringFind(rest, "_"); if(pos > 0) { string tsStr = StringSubstr(rest, 0, pos); int ts = (int)StringToInteger(tsStr); ArrayResize(names, n+1); ArrayResize(times, n+1); names[n] = nm; times[n] = ts; n++; } } } if(n <= keep) return; while(n > keep) { int minIdx = 0; for(int j = 1; j < n; j++) if(times[j] < times[minIdx]) minIdx = j; ObjectDelete(g_chart_id, names[minIdx]); for(int k = minIdx; k < n-1; k++) { names[k] = names[k+1]; times[k] = times[k+1]; } ArrayResize(names, n-1); ArrayResize(times, n-1); n--; } }
最后,该EA包含多项防御性设计以及面向用户体验的优化:给图表对象名称设置唯一前缀以避免冲突;ATR指标句柄失效时启用备用ATR计算逻辑;将融合分值限幅至固定区间;使用可配置的平滑系数alpha降低噪声;采用分层式信号校验机制(翻转阈值、符号翻转幅度/持续时长/动量条件、区间进入判定)。界面代码在绘制区间标识与最新信号文本时做了元素防重叠处理;柱形采用文本块而非图形绘图,程序负载更低,同时兼容旧版本MetaTrader5。
测试与结果
在本章节中,我们分析该EA的实测结果,包含回测结果以及实盘性能指标。
Crash 300指数的回测演示GIF动图如下。这是一套简洁且便于审计复盘的STM状态摘要面板:面板首行显示工具名称、交易品种(Crash 300指数)以及当前生效的时间周期(M5)。其下方是大号彩色编码的情绪分值,在‑100至+100刻度上展示经过平滑处理的市场偏向;简短偏向标签(例如BULL、BEAR、NEUTRAL)可以让交易者一眼定性判断行情。分值下方会展示上一条信号记录(例如“Bias: Long BUY@16:30”),便于核对多空方向与精确K线时间戳。横向强度条可视化分值大小;紧凑的“Recent”行展示最新K线指标,帮助理解分值背后的行情背景。全部界面元素均锚定在已收盘K线上,每一条告警、每一处标签,都可以追溯到精确的时间与价位。

在Crash 1000指数实盘测试过程中,EA发出了一条买入信号。对照已收盘K线的时间戳核验,经过短暂持仓后,证明该信号的判断基本正确。仪表盘展示明确的多头偏向,买入标记精准锚定K线收盘时刻与对应价位,行情走势与触发时间均可在图表完整复盘追溯。该结果表明STM能够识别具备持续性的短期多头偏向:平滑算法与动量校验过滤掉噪声,规避早期的震荡扫损行情;而持仓管理工作(持仓大小、止损、止盈)依旧交由交易者自行处理。
- Crash 300指数的实盘测试结果
结论
市场情绪偏向计(STM)精准实现了其设计目标:在Crash 300与Crash 1000指数的测试中,清晰且可靠地识别短期市场偏向。信号易于核验:所有箭头与标签都绑定至已收盘K线和对应价位,您可以回溯行情发生的时间与价格,无需主观猜测。平滑处理与动量校验机制,避免指标在细小波动中来回摆动;多时间周期权重设置功能,允许交易者优先选用适配自身交易风格的周期。
该工具仅作为信号生成与记录工具,并非一套完整的交易系统。它可以提示市场何时出现偏向,但不会帮助您计算持仓大小、设置止损或是管理离场。测试结果会随交易品种与交易时段发生变化:本次测试中Crash系列指数表现良好,但外汇、股票或期货品种可能需要重新调参。
将STM用作信号过滤器或信号确认层。记录信号至CSV文件,开展多品种回测与短期向前测试,之后再考虑自动化入场。当基于该信号进行交易时,务必搭配一套简单的风控方案:固定风险R值、明确止损规则,叠加更高时间周期验证。这样即便遇到一段充满噪声的行情,也不会侵蚀掉一个月积累下来的交易优势。
简言之,STM可以提供清晰、可复盘核验的短期市场偏向读数。它加快决策速度,帮助交易者维持交易纪律。在实盘投入资金交易前,请将其与风控机制和多重核验流程搭配使用。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/19137
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
竞争性学习算法(CLA)
新手在交易中的10个基本错误
使用MQL5实现Firebase数据的增删改查