市场模拟(第八部分):套接字(二)·进阶篇
指标里挂交互对象,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 验证。
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。外汇与贵金属波动剧烈,这类自定义事件若漏处理可能造成图形卡死,上实盘前务必在策略测试器跑一遍。
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 配色里误用。
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 条,观察老消息是否被环形覆盖。
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,能直接看到按钮和输入框同步长高,验证布局参数是否如预期联动。
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 像素给发送按钮。窗口一缩放,输入框就自动跟着变宽,这是面板不脱节的关键。
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);