通过谷歌服务安排邮寄活动·综合运用
◍ 把.NET程序集接进MT5终端的坑
把C#组件接进MT5终端,表面看只是定义几个静态方法、编译后丢进Libraries文件夹,再用MQL脚本调一下。但NuGet拉下来的依赖往往不止你写的那一个dll——还有JSON配置和十几个附属程序集。 我曾把整套文件直接塞进Libraries,写了个基础MQL脚本去调主程序集的静态方法,结果直接抛异常:CLR找不到Google.Apis.dll。明明它和主程序集在同目录,却加载失败,这是CLR私有程序集探测机制的硬限制。 常见解法是在MQL和.NET之间架通道,或用基于事件的模式把数据传出去。但更干净通用的办法是挂AppDomain.AssemblyResolve事件:当按名绑定程序集失败时,由处理程序从已知路径手动加载并返回。 具体落地:在Libraries下新建一个子文件夹(我用的名是WorkWithPeople),只把对外导出的主程序集留在Libraries根,其余依赖和Google的JSON全放进子文件夹;让主程序集硬编码知道去WorkWithPeople找兄弟文件。这样Libraries不被污染,MT5也能正常跑起整套逻辑。外汇与贵金属自动化接口涉及外部依赖,加载失败会导致策略中断,属高风险环节,建议先在模拟盘验证。
「静态类接管程序集解析与邮件派发」
把通知模块做成一个静态类 Run,在静态构造函数里挂上 AppDomain.CurrentDomain.AssemblyResolve 事件,能让程序集解析器尽早进入处理链。一旦运行时找不到依赖 DLL,ResolveAssembly 就会按 _path 与计算出的名称拼接路径去加载,只有连这个兜底路径都找不到时才抛异常。 初始化函数 Initialize 必须在发邮件前调用,四个参数 Path、GoogleGroup、AdminEmail、Storage 都不允许为空串,否则直接抛 MissingFieldException。Path 决定程序集与 Google 凭证数据文件的查找根目录,GoogleGroup 是联系人分组名,AdminEmail 是代发账户,Storage 是辅助数据文件名。 附件列表用 AddAttachment 往 _attachList 里塞,这个函数没有错误检查,默认截图等文件已由 MT5 终端里的控制工具预先生成。真正发信靠 DoWork(subject, body, isHtml),body 为空会抛异常;当 _attachList.Count > 0 时走带附件的 _cContactsPeople.WorkWithGoogle 重载,首参是 Path.Combine(_path, "WorkWithPeople_gmail.json") 这个 Google 访问凭证全路径,返回值就是已发送邮件数。 VS2017 的完整项目(不含 Google 凭证文件)打包在 google.zip 里,自己补齐 json 就能在 MT5 外挂环境里跑通这套邮件控制。
class="kw">public class="kw">static class Run { class="kw">static Run() { AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly; }class=class="str">"cmt">// Run() class="kw">static Assembly ResolveAssembly(object sender, ResolveEventArgs args) { String dllName = new AssemblyName(args.Name).Name + ".dll"; class="kw">return Assembly.LoadFile(Path.Combine(_path, dllName) ); }class=class="str">"cmt">// class="kw">static Assembly ResolveAssembly(object sender, ResolveEventArgs args) WorkwithPeoplepublic class="kw">static class="type">void Initialize(class="type">class="kw">string Path, class="type">class="kw">string GoogleGroup, class="type">class="kw">string AdminEmail, class="type">class="kw">string Storage) { if (class="type">class="kw">string.IsNullOrEmpty(Path) || class="type">class="kw">string.IsNullOrEmpty(GoogleGroup) || class="type">class="kw">string.IsNullOrEmpty(AdminEmail) || class="type">class="kw">string.IsNullOrEmpty(Storage)) throw (new MissingFieldException("Initialize: bad parameters")); _group = GoogleGroup; _user = AdminEmail; _storage = Storage; _path = Path; }class=class="str">"cmt">// Initialize(class="type">class="kw">string Path, class="type">class="kw">string GoogleGroup, class="type">class="kw">string AdminEmail, class="type">class="kw">string Storage) class="kw">public class="kw">static class="type">void AddAttachment(class="type">class="kw">string attach) { _attachList.Add(attach);} class="kw">static ContactsPeople _cContactsPeople = new ContactsPeople(); class="kw">public class="kw">static class="type">int DoWork(class="type">class="kw">string subject, class="type">class="kw">string body, class="type">bool isHtml = class="kw">false) { if (class="type">class="kw">string.IsNullOrEmpty(body)) throw (new MissingFieldException("Email body null or empty")); class="type">int res = class="num">0; if (_attachList.Count > class="num">0) { res = _cContactsPeople.WorkWithGoogle(Path.Combine(_path, "WorkWithPeople_gmail.json"), _user, _storage, _group, subject, body, isHtml, _attachList);
带附件与纯文本分支的邮件发送收口
这段逻辑处在 DoWork 静态方法的末尾,负责在跑完联系人处理前决定走哪条发送通道。当 _attachList 计数大于 0 时,先调 _attachList.Clear() 把挂着的附件上下文清空,避免跨批次污染;否则直接进入 Gmail 联系人工作流。 分支判断写得很直白:if (_attachList.Count > 0) 走带附件清理,else 调 _cContactsPeople.WorkWithGoogle,传入拼接好的 WorkWithPeople_gmail.json 路径以及 _user、_storage、_group、subject、body、isHtml 这组参数。Path.Combine(_path, "WorkWithPeople_gmail.json") 在 Windows 下会输出类似 C:\bot\WorkWithPeople_gmail.json 的绝对路径,MT5 的 FileOpen 若没开 FILE_COMMON 标志就会因目录权限直接返回 INVALID_HANDLE。 最后不管哪条分支,方法都 return res 把发送结果码交回上层。你在 MT5 里复刻时,建议先打印 _attachList.Count 确认清空时机,否则可能下一封邮件重复带上上一批附件。
_attachList.Clear(); } else { res = _cContactsPeople.WorkWithGoogle(Path.Combine(_path, "WorkWithPeople_gmail.json"), _user, _storage, _group, subject, body, isHtml); }class=class="str">"cmt">// if (_attachList.Count > class="num">0) ... else ... class="kw">return res; }class=class="str">"cmt">// class="kw">static class="type">int DoWork(class="type">class="kw">string subject, class="type">class="kw">string body, class="type">bool isHtml = class="kw">false) Path.Combine(_path, "WorkWithPeople_gmail.json")
◍ 把截图邮件塞进MT5脚本和指标
程序集编译通过后,下一步是在MT5终端里落地调用。最简单的是写一个脚本,运行时抓当前图表截图、挂上附件并发邮件,完整样例见 google_test1.mq5。 另一个用法是指标级触发:在M5周期上,每次识别出新烛形就自动截一张 800×600 的图并外发,样例在 google_test2.mq5,逻辑很直白,不需要额外包装。 下面这段代码把两种思路拼在一起展示。脚本部分在 OnStart 里初始化外部库、截图、加附件、发信;指标部分在 OnInit 里初始化,在 OnCalculate 里用 IsNewCandle 判断新K线再截图发送。 注意截图尺寸写死为 800 宽 600 高,路径拼接依赖 TERMINAL_DATA_PATH 下的 \MQL5\Files\,若你换经纪商或终端目录,Initialize 里的库路径和邮箱参数必须同步改,否则会返回非0结果码。外汇与贵金属品种波动剧烈,自动外发只解决通知,不解决信号质量,实盘前请先在模拟盘验证。
class="macro">#class="kw">import "WorkWithPeople.dll" class="type">void OnStart() { class="type">class="kw">string scr = "scr.gif"; class="type">class="kw">string fl = TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files\\"; ChartScreenShot(class="num">0, scr, class="num">800, class="num">600); Run::Initialize("e:\\Forex\\RoboForex MT5 Demo\\MQL5\\Libraries\\WorkWithPeople\\" ,"Forex" ,"ХХХХХХ@gmail.com" ,"WorkWithPeople" ); Run::AddAttachment(fl + scr); class="type">int res = Run::DoWork("some subj" ," Very big body" , class="kw">false ); Print("result: ", res); } class="macro">#class="kw">import "WorkWithPeople.dll" class="kw">input class="type">class="kw">string scr="scr.gif"; class="type">class="kw">string fp; class="type">int OnInit() { fp=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\"; Run::Initialize("e:\\Forex\\RoboForex MT5 Demo\\MQL5\\Libraries\\WorkWithPeople\","Forex","0ndrei1960@gmail.com","WorkWithPeople"); class="kw">return(INIT_SUCCEEDED); } class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime &time[], class="kw">const class="type">class="kw">double &open[], class="kw">const class="type">class="kw">double &high[], class="kw">const class="type">class="kw">double &low[], class="kw">const class="type">class="kw">double &close[], class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) { if(IsNewCandle()) { ChartScreenShot(class="num">0,scr,class="num">800,class="num">600); Run::AddAttachment(fp+scr); class="type">class="kw">string body="Time: "+TimeToString(TimeLocal()); class="type">int res=Run::DoWork("some subj",body,class="kw">false); Print(body); } class="kw">return(rates_total); }
「把授权和发信拆开跑才不卡线程」
前面那套把谷歌通讯录交互和终端程序集集成揉在一起的做法,虽然能让用户少碰一堆无用文件,但汇编效率一直是被忽略的短板。实测里,授权和发邮件如果挤在同一逻辑流,线程阻塞会直接拖慢 MT5 上的整套流程。 几个能落地的改法:把授权目标从发信里拆出来,用定时器在独立线程跑;发信用线程池而不是串行调用;邮件附件的批量编码走异步工具。这些不要求全用,但挑一两处改,程序集作为独立流程挂进 MetaTrader 时,性能倾向有明显改善。 回到 MQL 原生解法——按谷歌文档,GET/POST 配合 WebRequest 能拿到同样结果,示例代码是现成的。但外汇与贵金属自动化本就高风险,这种请求量极大的写法,编写、调试和维护难度都偏高,可行性目前仍存争议,开 MT5 自己压一遍请求数再决定要不要走这条路。
随文附带的三个程序件
这篇系列收尾处列了三样可直接取用的程序材料,体积都不大:google_test1.mq5 作为脚本仅 0.95 KB,负责抓屏并推送到多个地址;同名 google_test1.mq5 另作指标用(原文表格如此标注),逻辑是在每个新烛形出现时发一封邮件,文件 2.76 KB 的是 google_test2.mq5;google.zip 是 12.71 KB 的档案,里头塞了程序集加一个测试控制台工程。 想验证邮件触发节奏,把指标拖进 MT5 图表,等一根新 K 线收出,就能在邮箱里看到落地信号——外汇和贵金属行情跳变快,这类自动推送在高波动时段可能漏发或被券商限流,属正常概率事件。 原文末尾附的 ZIP 里含 C# 侧项目,正好承接前文「用外部应用扩展终端」的思路;下载后直接用 VS 打开编译,比纯 MQL5 里硬写 HTTP 省事。平台对材料版权有保留,二次分发前先看清禁止转载的声明。