English Русский Deutsch 日本語
preview
价格行为分析工具包开发(第三十七部分):市场情绪偏向计

价格行为分析工具包开发(第三十七部分):市场情绪偏向计

MetaTrader 5示例 |
30 1
Christian Benjamin
Christian Benjamin

引言

本文是价格行为分析工具包开发系列的第三十七部分。我的总体目标始终是开发实用工具,帮助交易者解读市场行为,并在适当的场景下,将部分分析流程自动化。迄今为止,我们已经介绍了独立的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准备步骤:

  1. 打开MetaEditor,
  2. 依次点击文件 → 新建 → EA(模板),
  3. 将EA源代码粘贴至新建文件,
  4. 使用含义清晰的名称保存文件,
  5. 按下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能够融合多个时间周期的信号;权重参数(InpWeightPrimaryInpWeightExtra1InpWeightExtra2)用于控制多周期信号的融合比例。平滑系数(InpSmoothAlpha)负责对融合后的原始分值执行指数平滑;阈值调参变量(例如InpFlipThresholdInpPositiveZoneInpNegativeZone)用于判定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_scoreg_prev_smoothed),用于检测大幅情绪翻转的上一次告警分值,还有当前的区间状态g_zone_state (取值为1、-1或0)。代码声明了CTradeg_trade对象,以备后续扩展EA下单功能。保存g_last_signal_textg_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);
  }
//+------------------------------------------------------------------+
DrawTextDrawCell是用来创建和配置图表标签、矩形标签的安全封装函数。这两个函数将对象创建与属性设置逻辑集中封装(包含停靠角落、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);
  }
CreateUIObjectsDeleteUIObjects负责管理全部界面元素的生命周期。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_idxInpHistogramMaxBars取模实现,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可以提供清晰、可复盘核验的短期市场偏向读数。它加快决策速度,帮助交易者维持交易纪律。在实盘投入资金交易前,请将其与风控机制和多重核验流程搭配使用。







图表展示器
分析评论
分析大师
分析预测 
波动率导航仪
均值回归信号收割器
信号脉冲 
指标看板 
外部数据流
VWAP
Heikin Ashi   FibVWAP  
RSI背离
抛物线止损与反转指标 (PSAR) 
四分位绘制脚本
侵入探测器
TrendLoom工具  四分位看板 
ZigZag分析仪  相关性探索  市场结构反转检测工具
关联仪表盘  货币强度计 
PAQ分析工具 
双EMA分形突破器
针形柱、吞噬形态与相对强弱指数(RSI)背离
流动性扫单 开盘区间突破工具 暴涨与暴跌拦截 CCI零线EA
K线形态识别 基于TA-Lib的K线形态识别 K线区间工具 MetaTrader 5数据接收器 模型训练与部署 Python库的使用
情绪倾斜计          

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

附加的文件 |
最近评论 | 前往讨论 (1)
Mustafa Nail Sertoglu
Mustafa Nail Sertoglu | 25 8月 2025 在 21:40
感谢你分享关于交易管理的新思路;愿你与爱你的人一起度过健康快乐的每一天。
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
竞争性学习算法(CLA) 竞争性学习算法(CLA)
本文介绍了竞争性学习算法(CLA),这是一种基于模拟教育过程的新型元启发式优化方法。该算法将解的种群组织为若干班级,班级中包含学生,而每个班级中最优的学生充当教师。其中代理通过三种机制进行学习:跟随班级中的最优解、利用个人经验以及在班级之间共享知识。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
使用MQL5实现Firebase数据的增删改查 使用MQL5实现Firebase数据的增删改查
本文提供一份循序渐进指南,带您掌握Firebase的CRUD(创建、读取、更新、删除)操作,重点聚焦实时数据库与Firestore。您将学习如何使用Firebase SDK方法高效管理Web与移动端应用的数据,涵盖新增记录、查询、修改以及删除数据条目。文中提供实用代码示例,并且介绍了实时场景下的数据结构设计与处理最佳实践,帮助开发者依托Firebase灵活的NoSQL(非关系型)架构,开发动态、可扩展的应用程序。