
来自专业程序员的提示(第二部分):在智能交易系统、脚本和外部程序之间存储和交换参数
内容
概述
在本文里,我们将讨论终端重启(关闭)后如何恢复参数。 所有示例都是来自我的 Cayman 项目的真实工作代码片段。
参数存储位置
参数示例
- 零号柱线时间 例如,在检测烛条形态时,在给定时间帧内出现新柱线后对其进行一次评估是合乎逻辑的。
- 交易级别参数。 例如,您也许会选择一个交易级别,并用脚本设置级别突破时进行交易的时间和规模。 该脚本将参数传递给智能交易系统。 智能交易系统创建一个级别分析器。 分析器仅在指定时间帧内出现新柱线之后才会“开机”。
- 用户偏好。 这些包括颜色、交易规则、绘图方法和其他参数。 显然,此类参数应该一次性安装,例如,在设置文件中。
- 终端全局变量
- 图形对象
- 订单注释
- 文本文件
贮存 | 类型 | 范围 | 生存期 |
---|---|---|---|
终端全局变量 | 双精度 | 任意图表 | 上次调用之后 4 周 |
图形对象 | 任何 字符串 <= 63 个字符 | 当前图表 | 图表生存期 |
订单注释 | 字符串 <= 23 个字符 | 任意图表 | 终端生存期 |
文本文件 | 任何 无限制 | 任意图表 | 文件生存期 |
终端全局变量
终端全局变量可从任何图表获得。 它们的范围可以通过在变量名称中包含其他组件来限制,例如 ChartId、Symbol 或 Period。 不能改变的是变量类型。 您也无法保存文本。
有一个小窍门:打包/解包整数值。 如您所知,双精度数值占用 8 个字节(64 位)。 请查看以下示例:它显示了如何在一个变量中存储多个整数值。 最重要的是判定它们的最大值的位长。
// ----------------------------------------------------------------------------- // 将存至/取自全局变量的整数值打包/解包的示例 | // 运用按位运算 | // ----------------------------------------------------------------------------- void OnStart() { int value10 = 10; // 最大 = 255 (8 位) int value20 = 300; // 最大 = 65535 (16 位) bool value30 = true; // 最大 = 1 (1 位) // 将值打包成 25 位 (8+16+1) // 39 位 (64-25) 保持空闲 ulong packedValue = (value10 << 17) + // 保留空间 (16+1) 对于 value20, value30 (value20 << 1) + // 保留空间 (1) 对于 value30 value30; // 保存全局变量 string nameGVar = "temp"; GlobalVariableSet(nameGVar, packedValue); // 读取全局变量 packedValue = (ulong)GlobalVariableGet(nameGVar); // 数值解包 // 0xFF, 0xFFFF, 0x1 - 最大值的位掩码 int value11 = (int)((packedValue >> 17) & 0xFF); int value21 = (int)((packedValue >> 1) & 0xFFFF); bool value31 = (bool)(packedValue & 0x1); // 比较值 if (value11 == value10 && value21 == value20 && value31 == value30) Print("OK"); else PrintFormat("0x%X / 0x%X /0x%X / 0x%X", packedValue, value11, value21, value31); }
图形对象
您可以将脚本参数存储在图形对象中吗? 有何不可。 设置对象属性 OBJPROP_PRICE = 0 — 在这种情况下,对象在视觉上是“隐藏的”,但可以在程序中访问。 为了可靠性,这样的对象可以保存在图表模板之中。 参数访问逻辑如下:如果有对象,则提取参数;如果没有对象,则设置默认值。
订单注释
最大订单注释长度限制为 23 个字符。 什么可以存储在注释里? 例如,SOP/H1/SS/C2/Br/Br/Br。 其中 (从左至右)
- SOP — 订单发送者 (SOP – SendOrderByPlan 脚本)
- H1 — 订单生成时间帧 (H1)
- SS — 订单类型 (SS – Sell Stop)
- C2 — 订单平仓算法
- Br — D1 趋势 (Br – 看跌)
- Br — H4 趋势 (Br – 看跌)
- Br — 订单生成时间帧内的趋势 (Br – 看跌)
我们为什么需要这样做? 例如,此数据可用于分析交易。 此处是我如何运用它:当一笔挂单被触发,我提取平仓算法值,并创建一个虚拟止损分析器 AnalyserVirtSL,然后它将在指定条件下平仓。
文本文件
这可能是存储恢复参数的最可靠和最通用的方式。 您可一次性设置访问类,然后随时随地使用它们。
应用程序设置
AppSettings.txt 设置文件的一部分:
# ------------------------------------------------------------------- # 智能交易系统和脚本设置 # 文件编码 = UCS-2 LE with BOM (需要!!!) // 此为 Unicode # ------------------------------------------------------------------- TimeEurWinter = 10:00 # 欧洲时段开始冬季时间 (服务器时间) TimeEurSummer = 09:00 # 欧洲时段开始夏季时间 (服务器时间) ColorSessionEur = 224,255,255 # 欧洲时段颜色 ColorSessionUsd = 255,240,245 # 美洲时段颜色 NumberColorDays = 10 # 高亮天数 (时段)
AppSettings.mqh 类
#property copyright "Copyright 2020, Malik Arykov" #property link "malik.arykov@gmail.com" #property strict #include <Cayman/Params.mqh> // 应用程序参数名 #define APP_TIME_EUR_SUMMER "TimeEurSummer" #define APP_TIME_EUR_WINTER "TimeEurWinter" #define APP_TIME_TRADE_ASIA "TimeTradeAsia" #define APP_COLOR_SESSION_EUR "ColorSessionEur" #define APP_COLOR_SESSION_USD "ColorSessionUsd" #define APP_NUMBER_COLOR_DAYS "NumberColorDays" // ----------------------------------------------------------------------------- // 智能交易系统和脚本的常用设置 | // ----------------------------------------------------------------------------- class AppSettings { private: Params *m_params; public: // 设置位于 AppSettings.txt 文件 string TimeEurSummer; // 欧洲时段开始夏季时间 string TimeEurWinter; // 欧洲时段开始冬季时间 string TimeTradeAsia; // 亚洲走廊交易结束时间 color ColorSessionEur; // 欧洲时段颜色 color ColorSessionUsd; // 美洲时段颜色 int NumberColorDays; // 高亮天数 // 由程序设置 string PeriodTrends; // 趋势计算周期 (D1,H4) string TradePlan; // 交易方向(简要计划) bool IsValid; // 参数有效性 // 方法 AppSettings(); ~AppSettings() { delete m_params; }; void Dump(string sender); }; // ----------------------------------------------------------------------------- // 构造函数 | // ----------------------------------------------------------------------------- AppSettings::AppSettings() { IsValid = true; m_params = new Params(); m_params.Load(PATH_APP_SETTINGS); if (m_params.Total() == 0) { PrintFormat("%s / ERROR: Invalid file / %s", __FUNCTION__, PATH_APP_SETTINGS); IsValid = false; return; } TimeEurWinter = m_params.GetValue(APP_TIME_EUR_WINTER); TimeEurSummer = m_params.GetValue(APP_TIME_EUR_SUMMER); TimeTradeAsia = m_params.GetValue(APP_TIME_TRADE_ASIA); ColorSessionEur = StringToColor(m_params.GetValue(APP_COLOR_SESSION_EUR)); ColorSessionUsd = StringToColor(m_params.GetValue(APP_COLOR_SESSION_USD)); NumberColorDays = (int)StringToInteger(m_params.GetValue(APP_NUMBER_COLOR_DAYS)); } // ----------------------------------------------------------------------------- // 打印设定参数 | // ----------------------------------------------------------------------------- void AppSettings::Dump(string sender) { PrintFormat("sender=%s / %s", sender, PATH_APP_SETTINGS); PrintFormat("%s = %s", APP_TIME_EUR_WINTER, TimeEurWinter); PrintFormat("%s = %s", APP_TIME_EUR_SUMMER, TimeEurSummer); PrintFormat("%s = %s", APP_TIME_TRADE_ASIA, TimeTradeAsia); PrintFormat("%s = %s / %s", APP_COLOR_SESSION_EUR, ColorToString(ColorSessionEur), ColorToString(ColorSessionEur, true)); PrintFormat("%s = %s / %s", APP_COLOR_SESSION_USD, ColorToString(ColorSessionEur), ColorToString(ColorSessionEur, true)); PrintFormat("%s = %i", APP_NUMBER_COLOR_DAYS, NumberColorDays); }
特征
AppSettings 类的声明位于 Uterminal.mqh 文件当中,该文件通过 #include 与智能交易系统和所有脚本连接。
extern AppSettings *gAppSettings; // 应用程序设置
以这个解决方案,您可以:
- 在任何位置一次性初始化 gAppSettings
- 可在任何类的实例里使用 gAppSettings(替代将其作为参数传递)
分析器参数
Cayman 智能交易系统可管理各种分析器,诸如 AnalyzerTrend、AnalyserLevel、AnalyserVirtSL。 每个分析器都与特定的时间帧相关联。 这意味着只有在指定时间帧内出现新柱线时才会启动分析器。 分析器示例存储在文本文件中,带有 Key = Value 字符串。 例如,H4 交易级别分析器将其参数存储在 Files\Cayman\Params\128968168864101576\exp_05_Lev607A160E_H4.txt 文件当中
- Cayman — 项目名称
- Params — 含有分析器参数的子目录
- 128968168864101576 — 图表 ID // IntergerToString(ChartID())
- exp_05_Lev607A160E_H4.txt — 包含分析器参数的文件名 —
- exp — 前缀
- 05 — 分析器类型
- Lev607A160E — 分析器的名称(交易级别)
- H4 — 所跟踪的时间帧。
以下是带注释的文件内容(实际文件不含注释)
// 交易级别参数 nameObj=Lev607A160E // 交易级别名称 kindLevel=1 // 级别类型 (1 - 阻力) riskValue=1.00 // 级别突破时的成交量 (1) riskUnit=1 // 交易量变化单位(保证金的 1 - %) (1 - 保证金的百分比 %) algClose=2 // 成交平仓算法(2 – 两根修正柱线) ticketNew=0 // 在级别突破时的成交单据 ticketOld=0 // 在级别突破时的平仓单据 profits=0 // 以点数为单位的计划内利润 losses=0 // 以点数为单位的计划内亏损 // 分析器参数 symbol=EURUSD // 品种名称 period=16388 // 分析器周期 (H4) time0Bar=1618603200 // 零号柱线时间 (秒数) typeAnalyser=5 // 分析器类型 colorAnalyser=16711935 // 分析器结果的颜色 resultAnalyser=Lev607A160E, H4, 20:00, RS // 分析器结果
有一个基类 Analyser,其可保存和恢复任何分析器的参数。 当智能交易系统重启时(例如,切换时间帧之后),分析器从相关文本文件中恢复参数。 如果新柱线的时间尚未到来,则分析不会重新开始。 分析器针对前一根柱线的计算结果(resultAnalyser、colorAnalyser)显示在智能交易系统注释当中。
将脚本参数传递给智能交易系统
SetTradeLevel 脚本允许设置交易级别的参数。 在图表上选择了一个对象(直线、趋势线或矩形)。 SetTradeLevel 脚本查找所选对象(交易级别),并为其设置参数。
接下来,脚本将参数保存到 Files\Cayman\Params\128968168864101576\exp_05_Lev607A160E_H4.txt,并通过 SendCommand 函数发送命令和文件路径至文件。
// ----------------------------------------------------------------------------- // 将级别参数发送到智能交易系统 | // ----------------------------------------------------------------------------- NCommand SendCommand() { // 加载级别参数(如果有) Params *params = new Params(); string speriod = UConvert::PeriodToStr(_Period); params.Load(PREFIX_EXPERT, anaLevel, gNameLev, speriod); // 定义命令 NCommand cmd = (gKindLevel == levUnknown) ? cmdDelete : (params.Total() > 0) ? cmdUpdate : cmdCreate; // 保存参数 params.Clear(); params.Add(PARAM_NAME_OBJ, gNameLev); params.Add(PARAM_TYPE_ANALYSER, IntegerToString(anaLevel)); params.Add(PARAM_PERIOD, IntegerToString(_Period)); params.Add(PARAM_KIND_LEVEL, IntegerToString(gKindLevel)); params.Add(PARAM_RISK_VALUE, DoubleToString(gRiskValue, 2)); params.Add(PARAM_RISK_UNIT, IntegerToString(gRiskUnit)); params.Add(PARAM_ALG_CLOSE, IntegerToString(gAlgClose)); params.Add(PARAM_TICKET_OLD, IntegerToString(gTicketOld)); params.Add(PARAM_PROFITS, IntegerToString(gProfits)); params.Add(PARAM_LOSSES, IntegerToString(gLosses)); params.Save(); // 向智能交易系统发送命令 params.SendCommand(cmd); delete params; return cmd; }
params.SendCommand(cmd) 函数如下:
// ----------------------------------------------------------------------------- // 向智能交易系统发送命令 | // ----------------------------------------------------------------------------- void Params::SendCommand(NCommand cmd) { string nameObj = NAME_OBJECT_CMD; ObjectCreate(0, nameObj, OBJ_LABEL, 0, 0, 0); ObjectSetString(0, nameObj, OBJPROP_TEXT, m_path); ObjectSetInteger(0, nameObj, OBJPROP_ZORDER, cmd); ObjectSetInteger(0, nameObj, OBJPROP_TIMEFRAMES, 0); }
每次价格跳变 (OnTick),智能交易系统都会调用 CheckExpernalCommand() 函数检查名为 NAME_OBJECT_CMD 的对象是否存在。 如果存在,则读取含有分析器参数的命令和文件路径,并立即删除该对象。 接下来,智能交易系统按文件名搜索正在运行的分析器。 如果 cmd == cmdDelete,则分析器被删除。 如果 cmd == cmdUpdate,则分析器参数从文件里更新。 如果 cmd == cmdNew,则采用来自文件的参数创建新的分析器。
此处 Params 类的代码全本,它封装了操控参数(关键字=参数值字符串)文件的逻辑。
#property copyright "Copyright 2020, Malik Arykov" #property link "malik.arykov@gmail.com" #include <Arrays/ArrayString.mqh> #include <Cayman/UConvert.mqh> #include <Cayman/UFile.mqh> // ----------------------------------------------------------------------------- // 参数类(关键字=参数值字符串 #注释) | // ----------------------------------------------------------------------------- class Params { private: string m_path; // 参数文件路径 NCommand m_cmd; // 智能交易系统命令 CArrayString *m_items; // 配对数组 {关键字=参数值} int Find(string key); public: Params(); ~Params() { delete m_items; }; void Clear() { m_items.Clear(); }; int Total() { return m_items.Total(); }; string Path() { return m_path; }; CArrayString *Items() { return m_items; }; void Add(string line) { m_items.Add(line); }; bool Add(string key, string value); string GetValue(string key); void Load(string prefix, int typeAnalyser, string nameObj, string speriod); void Load(string path); void Save(); void SendCommand(NCommand cmd); NCommand TakeCommand(); void Dump(string sender); }; // ----------------------------------------------------------------------------- // 默认构造函数 | // ----------------------------------------------------------------------------- Params::Params() { m_items = new CArrayString(); } // ----------------------------------------------------------------------------- // 添加一个 key=value 对 | // ----------------------------------------------------------------------------- bool Params::Add(string key, string value) { int j = Find(key); string line = key + "=" + value; if (j >= 0) { // 更新 m_items.Update(j, line); return false; } else { // 添加 m_items.Add(line); return true; } } // ----------------------------------------------------------------------------- // 依据关键字获取参数值 | // ----------------------------------------------------------------------------- string Params::GetValue(string key) { // 搜索关键字 int j = Find(key); if (j < 0) return NULL; // 无关键字 // 检查分隔符 string line = m_items.At(j); j = StringFind(line, "="); if (j < 0) { // 没有 = PrintFormat("%s / ERROR: Invalid string %s", __FUNCTION__, line); return NULL; } // 返回数值 return UConvert::Trim(StringSubstr(line, j + 1)); } // ----------------------------------------------------------------------------- // 依据关键字查找参数值 | // ----------------------------------------------------------------------------- int Params::Find(string key) { int index = -1; for (int j = 0; j < m_items.Total(); j++) { if (StringFind(m_items.At(j), key) == 0) { index = j; break; } } return index; } // ----------------------------------------------------------------------------- // 加载参数 | // ----------------------------------------------------------------------------- void Params::Load(string prefix, int typeAnalyser, string nameObj, string speriod) { string nameFile = StringFormat("%s%02i_%s_%s.txt", prefix, typeAnalyser, nameObj, speriod); m_path = StringFormat("%s%s/%s", PATH_PARAMS, IntegerToString(ChartID()), nameFile); if (FileIsExist(m_path)) Load(m_path); } // ----------------------------------------------------------------------------- // 加载参数 | // ----------------------------------------------------------------------------- void Params::Load(string path) { m_path = path; if (!FileIsExist(m_path)) return; //PrintFormat("%s / %s", __FUNCTION__, m_path); string text = UFile::LoadText(m_path); if (text == NULL) return; // 将文本切分成数行 string line, lines[]; int numLines = StringSplit(text, DLM_LINE, lines); for (int j = 0; j < numLines; j++) { line = lines[j]; // 删除注释 int k = StringFind(line, "#"); if (k == 0) continue; // 整个字符串是一条注释 if (k > 0) line = StringSubstr(line, 0, k); // 添加一个非空字符串 if (line != "") m_items.Add(line); } } // ----------------------------------------------------------------------------- // 保存参数 | // ----------------------------------------------------------------------------- void Params::Save() { string text = ""; for (int j = 0; j < m_items.Total(); j++) { text += m_items.At(j) + "\n"; } // 重写现有文件 UFile::SaveText(text, m_path, true); } // ----------------------------------------------------------------------------- // 向智能交易系统发送命令 | // ----------------------------------------------------------------------------- void Params::SendCommand(NCommand cmd) { string nameObj = NAME_OBJECT_CMD; ObjectCreate(0, nameObj, OBJ_LABEL, 0, 0, 0); ObjectSetString(0, nameObj, OBJPROP_TEXT, m_path); ObjectSetInteger(0, nameObj, OBJPROP_ZORDER, cmd); ObjectSetInteger(0, nameObj, OBJPROP_TIMEFRAMES, 0); } // ----------------------------------------------------------------------------- // 从脚本接收命令 | // ----------------------------------------------------------------------------- NCommand Params::TakeCommand() { string nameObj = NAME_OBJECT_CMD; if (ObjectFind(0, nameObj) < 0) return cmdUnknown; m_path = ObjectGetString(0, nameObj, OBJPROP_TEXT); m_cmd = (NCommand)ObjectGetInteger(0, nameObj, OBJPROP_ZORDER); ObjectDelete(0, nameObj); Load(m_path); return m_cmd; } // ----------------------------------------------------------------------------- // | 转储参数 | // ----------------------------------------------------------------------------- void Params::Dump(string sender) { for (int j = 0; j < m_items.Total(); j++) { PrintFormat("%s / %s", sender, m_items.At(j)); } }
对于 MQL5 的粉丝:当将 m_items 类型更改为 CHashMap 时,Add、GetValue、Find 函数的代码将显著减少。 但 Params 类在 MQL4 中也用到了 。 甚至,在这种情况下,参数访问速度并不重要,因为参数只会被读取一次,来初始化局部变量。 为什么没有在 MQL5 里针对 CHashMap 重新编制类? 可能是因为我在银行工作了很长时间。 金融软件开发者有一个很重要的原则:如果操作正常,就不要碰它! ;-)
将参数传递给外部程序
不同系统之间的数据交换单元实际上是一个 json 文件。 以前它曾是一个 xml 文件。 json 文件的主要优点是:
- 易于创建(生成/格式化)
- 所有高级语言均完美支持
- 可读性
例如,有一个包含以下字段的 Bar 类:m_time、m_open、m_high、m_low、m_close、m_body。 其中 m_body 是烛条颜色:阳线、阴线或十字星。 Bar 类有一个 ToJson() 方法,它生成一个 json 字符串
string Bar::ToJson() { return "{" + "\n\t\"symbol\":\"" + _Symbol + "\"," + "\n\t\"period\":" + IntegerToString(_Period) + "," + "\n\t\"digits\":" + IntegerToString(_Digits) + "," + "\n\t\"timeBar\":\"" + TimeToStr(m_time) + "\"," + "\n\t\"open\":" + DoubleToString(m_open, _Digits) + "," + "\n\t\"high\":" + DoubleToString(m_high, _Digits) + "," + "\n\t\"low\":" + DoubleToString(m_low, _Digits) + "," + "\n\t\"close\":" + DoubleToString(m_close, _Digits) + "," + "\n\t\"body\":" + IntegerToString(m_body) + "," + "\n}"; }
我们可以改用 StringFormat,但这会在重新排列或删除数值时导致出问题。 格式化 “\n\t” 可以删除,因为网上有很多 json 格式化服务。 其中之一是 JSON 解析器。 一次设置有效 json 的接收,并在您需要时调用 bar.ToJson() 函数。
一个外部程序,例如 C# 应用程序,可以将任何复杂度的 json 文件转换为对象。 如何从 MQL 传输一个 json 文件? 这很简单。 例如,将 json 文件加载(保存)到 Files/Json 终端目录。 一个外部程序监视此目录中是否有新文件。 若发现文件,程序读取它,将其转换为对象,并立即删除文件,或将其移动到存档(用于统计)。
从外部程序接收参数
将一个 json 库(或重新发明轮子)连接到 MQL 程序会导致额外的麻烦。 一个更好的解决方案是传递含有“关键字=参数值”字符串的文本文件。 可以利用 Params 类(见上文)处理文件。 智能交易系统和指标是接收来自外部程序或脚本参数的候选对象。 例如,您需要在 OnTick 处理程序中调用 CheckExternalCommand() 函数,其会检查 Files/ExtCmd 目录中的文件是否存在。 当发现一个文件时,它应该读取、处理(接受参数)、并删除该文件。
如此,我们已研究了在 MQL 和外部程序之间接收和传递参数的方法。 现在思考如下问题:为什么 MQL 程序需要 DLL?MQL 市场不接受此类程序。 原因只有一个 — 安全,因为您可以从 DLL 访问任何内容。
将参数传递给智能手机
为了进一步的操作,我将使用安卓 app 程序 WirePusher。 这是一项很棒的服务(免费且无插件)。 我不知道 iPhone 是否有类似的东西。 如果有任何 iPhone 粉丝正在阅读本文,请在评论中分享。
开始使用服务:
- 在您的智能手机上安装 WirePusher
- 启动应用程序。 您将在主屏幕上看到您的 ID
- 在终端的服务/设置/智能系统/允许 WebRequest 访问的 URL 里加入 https://wirepusher.com
然后启动脚本(不要忘记在 id = “********” 中写上你的 id,替换星号)
void OnStart() { string id = "**********"; // 在 WirePusher 里填写您的智能手机 id WirePusher("Profit $1000", "Deal", "Closed", id); } // ------------------------------------------------------------------------------------------------ // 通过 WirePusher 网页服务向智能手机发送通知 // 在终端的服务/设置/智能系统/允许 WebRequest 访问的 URL 里加入 https://wirepusher.com // message - 通知文本 // title - 通知标题(例如,Attention / Alert / Deal) // type - 通知类型(例如,触发挂单/级别突破/平仓) // id - 来自 WirePusher 安卓应用程序的唯一智能手机 id // ------------------------------------------------------------------------------------------------ bool WirePusher(string message, string title, string type, string id) { char data[]; // HTTP 消息主体数据数组 char result[]; // Web 服务响应数据数组 string answer; // Web 服务响应头 string url = "https://wirepusher.com/send?id={id}&title={title}&message={message}&type={type}"; StringReplace(url, "{id}", id); StringReplace(url, "{type}", type); StringReplace(url, "{title}", title); StringReplace(url, "{message}", message); ResetLastError(); int rcode = WebRequest("GET", url, NULL, 3000, data, result, answer); if (rcode != 200) { PrintFormat("%s / error=%i / url=%s / answer=%s / %s", __FUNCTION__, GetLastError(), url, answer, CharArrayToString(result)); return false; } PrintFormat("%s / %s / %s", __FUNCTION__, title, message); return true; }
在 Cayman EA 中,在 AnalyserTrade 中调用 WirePusher 函数的时刻:
- 挂单触发
- 价格突破交易级别
- 一笔成交平仓
可以为 WirePusher 上的每种通知类型分配一个独立的声音。 以前,我曾为以盈利了结的成交发出 “ta-da” 声音,并为亏损了结的成交发出“炸弹”声音。 但随后我就被炸弹搞得疲惫不堪。
结束语
存储参数最可靠、最方便的方法是运用文本文件。 甚而,在任何操作系统(应用程序)里都完全支持/缓存文件操作。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/9327


