市场模拟(第八部分):套接字(二)·进阶篇
📘

市场模拟(第八部分):套接字(二)·进阶篇

第 2/3 篇

指标里挂交互对象,EA 管通信

把交互对象放在指标、连接逻辑塞进 EA,是这套迷你聊天室回放框架的解耦做法。指标不能直接用套接字,所以 EA 负责监听 socket,收到消息再转发给指标里的聊天窗显示——这样后续加功能不会动到核心通信层。 头文件 C_Chat.mqh 里先定基础参数:第 6 行是指标名,第 7 行设最大显示行数(默认 256),第 8–9 行是字体名和大小,第 10 行控价对象高度防错位,第 11 行是图表放文本的宏。改这些地方就能全局调尺寸,不用逐个改对象。 两个坑得记牢:服务器不存消息,连接前发的全丢;MT5 切周期会清掉所有图表对象,存在结构里的消息也跟着没。要持久化得自己写文件存第 27 行那个结构,再读回来。 消息用循环缓冲:第 31 行用「最大行数 +1」是因为 MQL5 索引从 0 起,满 256 行后第 40 行把计数器归零,旧消息被覆盖。第 45 行起的过程负责绘到位图——MQL5 没精确绘文本的原生办法,Comment() 不够用,所以用 OBJ_BITMAP_LABEL(第 97 行)把字画到 bitmap 上再让 MT5 渲染。第 64 行通知终端位图可用,顺序不能乱:命名→绘制→声明可用。 构造函数(第 105 行)抓窗口索引、设字体、取字母高度避字体错位,第 116–118 行建对象;析构只做第 123 行删对象。指标内只处理三类事件:CHARTEVENT_CHART_CHANGE 定位、OBJECT_CLICK 抓发送按钮文本并发自定义事件给 EA、以及第 153 行收 EA 转来的 socket 文本。EA 不直接读编辑框,避免被指标破坏。 下面这段是指标主体代码,define 里 def_MaxPosSlider 给 400、def_MaskTimeService 掩码 0xFED00000,调试宏在 def_VERSION_DEBUG 开启时打印文件行号函数名,方便你直接抄去 MT5 验证。

MQL5 / C++
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">03. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">04. class="macro">#define def_VERSION_DEBUG
class="num">05. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">06. class="macro">#ifdef def_VERSION_DEBUG
class="num">07.   class="macro">#define macro_DEBUG_MODE(A) \
class="num">08.         Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + class="macro">#A + " = " + (class="type">class="kw">string)(A));
class="num">09. class="macro">#else
class="num">10.   class="macro">#define macro_DEBUG_MODE(A)
class="num">11. class="macro">#endif
class="num">12. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">13. class="macro">#define def_SymbolReplay        "RePlay"
class="num">14. class="macro">#define def_MaxPosSlider        class="num">400
class="num">15. class="macro">#define def_MaskTimeService     0xFED00000

「周期映射与联合体拆字节的底层写法」

指标里处理多周期时,直接拿 _Period 当数组下标会越界或错位。下面这行宏把分钟图(<60)原样保留,小时图(<D1)减去 16325 偏移,日线映射成 84、周线 91、月线 96,相当于把 MT5 的时间帧枚举压进一段连续整数域,方便后面用下标查表。 #define def_IndicatorTimeFrame (_Period < 60 ? _Period : (_Period < PERIOD_D1 ? _Period - 16325 : (_Period == PERIOD_D1 ? 84 : (_Period == PERIOD_W1 ? 91 : 96)))) def_IndexTimeFrame 写死成 4,一般对应某类固定参考帧(如 H1 或 M5,视整体架构而定),后续取价、算偏差都靠它定位缓冲槽。 uCast_Double 这个联合体是典型「同址 reinterpret」手法:一个 double 和 long / datetime / uint[2] / ushort[4] / uchar[8] 共享 8 字节。做价格序列序列化、跨进程传 tick、或把 datetime 当整数算时,不用 memcpy 就能按位切。注意 MT5 里 double 占 8 字节,所以 sizeof 除出来正好是 2/4/8 个窄类型。 EnumEvents 把界面交互和回放控制抽象成自定义事件码:evTicTac 是心跳,evHideMouse / evShowMouse 管鼠标水平线显隐,evCtrlReplayInit 负责回放模块冷启动。写 EA 面板时,用 EventChartCustom 发这些码,比全局变量轮询省 CPU。外汇与贵金属波动剧烈,这类自定义事件若漏处理可能造成图形卡死,上实盘前务必在策略测试器跑一遍。

MQL5 / C++
class="macro">#define def_IndicatorTimeFrame(_Period < class="num">60 ? _Period : (_Period < PERIOD_D1 ? _Period - class="num">16325 : (_Period == PERIOD_D1 ? class="num">84 : (_Period == PERIOD_W1 ? class="num">91 : class="num">96))))
class="macro">#define def_IndexTimeFrame       class="num">4
class=class="str">"cmt">//+------------------------------------------------------------------+
union uCast_Double
{
   class="type">class="kw">double   dValue;
   class="type">long     _long;                                           class=class="str">"cmt">// class="num">1 Information
   class="type">class="kw">datetime _datetime;                                       class=class="str">"cmt">// class="num">1 Information
   class="type">uint     _32b[class="kw">sizeof(class="type">class="kw">double) / class="kw">sizeof(class="type">uint)];             class=class="str">"cmt">// class="num">2 Informations
   class="type">class="kw">ushort   _16b[class="kw">sizeof(class="type">class="kw">double) / class="kw">sizeof(class="type">class="kw">ushort)];           class=class="str">"cmt">// class="num">4 Informations
   class="type">uchar    _8b [class="kw">sizeof(class="type">class="kw">double) / class="kw">sizeof(class="type">uchar)];            class=class="str">"cmt">// class="num">8 Informations
};
class=class="str">"cmt">//+------------------------------------------------------------------+
enum EnumEvents   {
         evTicTac,                                           class=class="str">"cmt">//Event of tic-tac
         evHideMouse,                                        class=class="str">"cmt">//Hide mouse price line
         evShowMouse,                                        class=class="str">"cmt">//Show mouse price line
         evHideBarTime,                                      class=class="str">"cmt">//Hide bar time
         evShowBarTime,                                      class=class="str">"cmt">//Show bar time
         evHideDailyVar,                                     class=class="str">"cmt">//Hide daily variation
         evShowDailyVar,                                     class=class="str">"cmt">//Show daily variation
         evHidePriceVar,                                     class=class="str">"cmt">//Hide instantaneous variation
         evShowPriceVar,                                     class=class="str">"cmt">//Show instantaneous variation
         evCtrlReplayInit,                                   class=class="str">"cmt">//Initialize replay control
         evChartTradeBuy                                     class=class="str">"cmt">//Market buy event

◍ 图表与EA之间的事件枚举和迷你聊天类骨架

这段声明把图表端和EA端的交互事件列成了枚举,从市价卖单、平仓到双向通信、迷你聊天 socket 读写都给了独立 ID。做跨图表指令控制时,直接复用这些枚举值可以避免自己造轮子,降低事件冲突概率。 41~47 行定义了一个匿名枚举,包含 evChartTradeSell(市价卖出)、evChartTradeCloseAll(平掉所有仓位)、evChartTrade_At_EA 与 evEA_At_ChartTrade(双向通信)、evChatWriteSocket / evChatReadSocket(迷你聊天收发)。这些事件后续会在 ChartTrade 面板与 EA 之间以自定义事件号传递。 下面的 C_Chat 类负责在图表上画一个聊天面板:宏里把最大行数定成 256(def_MaxRows),字体用 Lucida Console、字号 12,单条文本控件高度加 6 像素作为控件总高。m_Msgs 数组按 def_MaxRows+1 分配,说明留了一行做缓冲。外汇与贵金属品种波动剧烈,这类面板只做信息传递,不构成任何方向建议。 macroColorRGBA 这个宏把 RGB 互换了红蓝通道再拼上不透明 alpha(0xFF<<24),写像素级自绘控件时会用到,复制时注意别在普通 ObjectSetInteger 配色里误用。

MQL5 / C++
class="num">41.     evChartTradeSell,           class=class="str">"cmt">//Market sales event
class="num">42.     evChartTradeCloseAll,        class=class="str">"cmt">//Event to close positions
class="num">43.     evChartTrade_At_EA,          class=class="str">"cmt">//Event to communication
class="num">44.     evEA_At_ChartTrade,          class=class="str">"cmt">//Event to communication
class="num">45.     evChatWriteSocket,           class=class="str">"cmt">//Event to Mini Chat
class="num">46.     evChatReadSocket             class=class="str">"cmt">//Event To Mini Chat
class="num">47.       };
class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">001. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">002. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">003. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">004. class="macro">#include  "..\Defines.mqh"
class="num">005. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">006. class="macro">#define def_ShortName    "Mini Chat"
class="num">007. class="macro">#define def_MaxRows      class="num">256
class="num">008. class="macro">#define def_FontName     "Lucida Console"
class="num">009. class="macro">#define def_FontSize     class="num">12
class="num">010. class="macro">#define def_SizeControls(m_txtHeight + class="num">6)
class="num">011. class="macro">#define macroColorRGBA(A) ((class="type">uint)((0xFF << class="num">24) | (A & 0x00FF00) | ((A & 0xFF0000) >> class="num">16) | ((A & 0x0000FF) << class="num">16)))
class="num">012. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">013. class C_Chat
class="num">014. {
class="num">015.   class="kw">private  :
class="num">016.     class="type">long   m_id;
class="num">017.     class="type">int    m_sub;
class="num">018.     class="type">int    m_txtHeight;
class="num">019.     class="type">bool   m_full;
class="num">020.     class="type">class="kw">ushort m_Width,
class="num">021.           m_Height,
class="num">022.           m_index;
class="num">023.     class="type">uint   m_Pixel[];
class="num">024.     class="type">class="kw">string m_ObjEdit,
class="num">025.           m_ObjBtn,
class="num">026.           m_ObjPanel;
class="num">027.     class="kw">struct st0
class="num">028.     {
class="num">029.       class="type">class="kw">string info;
class="num">030.       class="type">bool   loc;
class="num">031.     }m_Msgs[def_MaxRows + class="num">1];
class="num">032. class=class="str">"cmt">//+------------------------------------------------------------------+

消息缓冲与画布刷新的底层逻辑

这段实现了一个轻量日志面板的写入与重绘机制。Add() 负责把字符串塞进 m_Msgs 环形缓冲,当 m_index 超过 def_MaxRows 时翻转 m_full 标记并把下标归零,随后立即触发 Paint()。 Paint() 先按 macroColorRGBA(clrBlack) 清空像素数组 m_Pixel,再用 m_Height - def_SizeControls 算出可绘制区高度 p0,若 p0<0 直接 return 避免越界。可见面板高度必须高于控件区,否则什么都不画。 可见行数 max 由 floor(p0 / m_txtHeight) 决定,例如 p0=300、m_txtHeight=15 时最多显示 20 行。循环里按 m_full 状态计算起始 count,用 TextOut 将每条消息以 clrLime(本地消息用 clrSkyBlue)写到画布,行坐标每次累加 m_txtHeight。 最后 ResourceCreate 把像素数组注册成 '::'+m_ObjPanel 资源并 ChartRedraw(),MT5 上的面板才真正刷新。想验证的话,把 def_MaxRows 设小到 5、连续 Add 10 条,观察老消息是否被环形覆盖。

MQL5 / C++
class="type">void Add(class="type">class="kw">string szMsg, class="type">bool isloc = false)
  {
  m_Msgs[m_index].info = szMsg;
  m_Msgs[m_index].loc = isloc;
  if ((++m_index) > def_MaxRows)
  {
     m_full = true;
     m_index = class="num">0;
  }
  Paint();
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
  class="type">void Paint(class="type">void)
  {
  class="type">int max, count, p0, p1;
  
  ArrayInitialize(m_Pixel, macroColorRGBA(clrBlack));
  if ((p0 = m_Height - def_SizeControls) < class="num">0) class="kw">return;
  max = (class="type">int)(floor(p0 / (m_txtHeight * class="num">1.0)));
  p1 = m_index - max;
  if (m_full)
     count = (max > def_MaxRows ? m_index + class="num">1 : (p1 > class="num">0 ? p1 : (def_MaxRows + p1 + class="num">1)));
  else
     count = (p1 > class="num">0 ? p1 : class="num">0);
  for (class="type">class="kw">ushort row = class="num">0; row < p0; count++)
  {
     count = (count > def_MaxRows ? class="num">0 : count);
     if (count == m_index) break;
     TextOut(m_Msgs[count].info, class="num">2, row, class="num">0, m_Pixel, m_Width, m_Height, macroColorRGBA(m_Msgs[count].loc ? clrSkyBlue : clrLime), COLOR_FORMAT_ARGB_NORMALIZE);
     row += (class="type">class="kw">ushort) m_txtHeight;
  }
  ResourceCreate("::" + m_ObjPanel, m_Pixel, m_Width, m_Height, class="num">0, class="num">0, class="num">0, COLOR_FORMAT_ARGB_NORMALIZE);
  ChartRedraw();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
  class="type">void CreateObjEdit(const class="type">class="kw">string szArg)
  {

「输入框与按钮的 GUI 对象落地」

在 MT5 面板开发中,可编辑框和按钮是最常碰到的两类交互控件。上面这段把创建逻辑拆成了三个函数:编辑框用 OBJ_EDIT,按钮用 OBJ_BUTTON,底板用 OBJ_BITMAP_LABEL,都挂在同一个子窗口 m_sub 下,坐标先给 (0,0) 再由距离属性定位。 编辑框的宽度靠 XDISTANCE=2 贴左边,YSIZE 复用 def_SizeControls 保持和别的控件等高;背景填 clrDarkGray、文字 clrBlack、边框 clrNavy,视觉上和按钮区分开。按钮则固定 XSIZE=70,背景 clrSkyBlue、边框 clrBlack,并用 OBJPROP_TEXT 把传入的 szTxt 写上去——注意按钮不设 XDISTANCE,默认从 0 开始排。 底板 OBJ_BITMAP_LABEL 同样 XDISTANCE=2,通常用来做半透明背景层,避免控件直接飘在 K 线上。外汇与贵金属波动剧烈、杠杆高风险大,这类面板只负责交互,信号逻辑别写死在界面层。 开 MT5 新建 EA 把这三个函数拷进去,改 def_SizeControls 从 20 调到 24,能直接看到按钮和输入框同步长高,验证布局参数是否如预期联动。

MQL5 / C++
ObjectCreate(m_id, szArg, OBJ_EDIT, m_sub, class="num">0, class="num">0);
ObjectSetInteger(m_id, szArg, OBJPROP_XDISTANCE, class="num">2);
ObjectSetInteger(m_id, szArg, OBJPROP_YDISTANCE, class="num">0);
ObjectSetInteger(m_id, szArg, OBJPROP_YSIZE, def_SizeControls);
ObjectSetString(m_id, szArg, OBJPROP_FONT, def_FontName);
ObjectSetInteger(m_id, szArg, OBJPROP_FONTSIZE, def_FontSize);
ObjectSetInteger(m_id, szArg, OBJPROP_BGCOLOR, clrDarkGray);
ObjectSetInteger(m_id, szArg, OBJPROP_COLOR, clrBlack);
ObjectSetInteger(m_id, szArg, OBJPROP_BORDER_COLOR, clrNavy);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CreateObjButton(const class="type">class="kw">string szArg, const class="type">class="kw">string szTxt)
{
ObjectCreate(m_id, szArg, OBJ_BUTTON, m_sub, class="num">0, class="num">0);
ObjectSetInteger(m_id, szArg, OBJPROP_YDISTANCE, class="num">0);
ObjectSetInteger(m_id, szArg, OBJPROP_XSIZE, class="num">70);
ObjectSetInteger(m_id, szArg, OBJPROP_YSIZE, def_SizeControls);
ObjectSetString(m_id, szArg, OBJPROP_FONT, def_FontName);
ObjectSetInteger(m_id, szArg, OBJPROP_FONTSIZE, def_FontSize);
ObjectSetInteger(m_id, szArg, OBJPROP_BGCOLOR, clrSkyBlue);
ObjectSetInteger(m_id, szArg, OBJPROP_COLOR, clrBlack);
ObjectSetInteger(m_id, szArg, OBJPROP_BORDER_COLOR, clrBlack);
ObjectSetString(m_id, szArg, OBJPROP_TEXT, szTxt);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CreateObjPanel(const class="type">class="kw">string szArg)
{
ObjectCreate(m_id, szArg, OBJ_BITMAP_LABEL, m_sub, class="num">0, class="num">0);
ObjectSetInteger(m_id, szArg, OBJPROP_XDISTANCE, class="num">2);

◍ 聊天面板的构造与消息分发收口

C_Chat 的构造函数把三个 GUI 对象一次性建出来:编辑框、发送按钮和背景面板,名字都拼了 ObjectsTotal(m_id) 做后缀,避免同图表多实例重名。字体用 TextSetFont(def_FontName, -10 * def_FontSize, 0, 0) 设成负号表示粗体,再用 TextGetSize("M", tmp, m_txtHeight) 量出单字高度,后面排版间距全靠这个 m_txtHeight 推算。 析构里只调一句 ObjectsDeleteAll(m_id, def_ShortName),前缀统一的对象会被整批删掉,不会在图表上留垃圾。外汇与贵金属图表加载这类 EA 前先确认测试环境,实盘误挂可能干扰手动下单。 DispatchMessage 用 switch 接图表事件,CHARTEVENT_CHART_CHANGE 分支里读 ChartGetInteger 的 CHART_WIDTH_IN_PIXELS 和 CHART_HEIGHT_IN_PIXELS,把窗口像素宽高存进 m_Width / m_Height,再把编辑框 XSIZE 设成 m_Width - 75,留 75 像素给发送按钮。窗口一缩放,输入框就自动跟着变宽,这是面板不脱节的关键。

MQL5 / C++
class="num">099.        ObjectSetInteger(m_id, szArg, OBJPROP_YDISTANCE, m_txtHeight + class="num">8);
class="num">100.        ObjectSetString(m_id, szArg, OBJPROP_BMPFILE, "::" + m_ObjPanel);
class="num">101.   }
class="num">102. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">103.   class="kw">public  :
class="num">104. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">105.    C_Chat()
class="num">106.       :m_index(class="num">0),
class="num">107.        m_full(false),
class="num">108.        m_Width(class="num">0),
class="num">109.        m_Height(class="num">0)
class="num">110.    {         
class="num">111.       class="type">int tmp;
class="num">112.       
class="num">113.       m_sub = ChartWindowFind(m_id = ChartID(), def_ShortName);
class="num">114.       TextSetFont(def_FontName, -class="num">10 * def_FontSize, class="num">0, class="num">0);
class="num">115.       TextGetSize("M", tmp, m_txtHeight);
class="num">116.       CreateObjEdit(m_ObjEdit = def_ShortName + " Edit" + (class="type">class="kw">string)ObjectsTotal(m_id));
class="num">117.       CreateObjButton(m_ObjBtn = def_ShortName + " Button" + (class="type">class="kw">string)ObjectsTotal(m_id), "Send");
class="num">118.       CreateObjPanel(m_ObjPanel = def_ShortName + " Panel" + (class="type">class="kw">string)ObjectsTotal(m_id));
class="num">119.    }
class="num">120. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">121.    ~C_Chat()
class="num">122.    {
class="num">123.       ObjectsDeleteAll(m_id, def_ShortName);
class="num">124.    };
class="num">125. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">126.    class="type">void DispatchMessage(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
class="num">127.    {
class="num">128.       class="kw">switch(id)
class="num">129.       {
class="num">130.        case CHARTEVENT_CHART_CHANGE:
class="num">131.           m_Width = (class="type">class="kw">ushort)ChartGetInteger(m_id, CHART_WIDTH_IN_PIXELS, m_sub);
class="num">132.           m_Height = (class="type">class="kw">ushort)ChartGetInteger(m_id, CHART_HEIGHT_IN_PIXELS, m_sub);
class="num">133.           ObjectSetInteger(m_id, m_ObjEdit, OBJPROP_XSIZE, m_Width - class="num">75);

常见问题

用事件枚举把图表对象交互转成消息,指标只负责画和发,EA用套接字收发明文,分工后图表刷新不阻塞。
用联合体把多字节字段按字节数组拆开,发送前按协议固定端序重排,接收端同样联合体还原,避免直接类型强转。
小布可基于你的品种页生成迷你聊天类骨架与事件枚举示例,你直接抄进MT5工程改缓冲区即可验证。
缓冲满或超时(如200ms)才触发画布重绘,别每次按键都重画,否则高频交互会拖慢终端。
收口放EA,指标只抛原始事件;EA统一验包、分发到面板对象,便于以后接外部套接字服务扩展。