来自专业程序员的提示(第二部分):在智能交易系统、脚本和外部程序之间存储和交换参数·进阶篇
把交易时段配置抽成全局单例
在 MT5 里写 EA 或脚本时,时段颜色、冬夏令起始时间这类参数如果散落在各个类里,后期调黄金和美盘的边界会非常痛苦。下面这套做法把配置收敛到一个 AppSettings 类,通过 Uterminal.mqh 用 #include 挂到 EA 和所有脚本上,全局只用 gAppSettings 一次初始化,任何类的实例直接读它,不必层层传参。 配置文件 AppSettings.txt 必须存成 UCS-2 LE with BOM,否则中文注释和字段解析可能直接失效。里面几个硬参数值得盯一下:欧洲时段冬季起点 10:00 服务器时间、夏季 09:00;欧洲时段色值 224,255,255,美洲 255,240,245;高亮天数默认 10 天。改这些数值不需要动代码,重启 EA 即可生效。 类声明里 private 只有一个 Params* m_params,所有对外字段都是 public——TimeEurWinter、ColorSessionUsd、NumberColorDays 等从 txt 读入,PeriodTrends、TradePlan、IsValid 由程序运行时填。构造函数先 new Params 再 Load,若 Total()==0 就把 IsValid 置 false 并退出,等于文件空了整个配置直接作废。外汇和贵金属波动受时段切换影响明显,这类全局配置出错可能让画线和计划整体偏移,实操前务必在策略测试器里跑一遍空文件场景验证容错。
# ------------------------------------------------------------------- # 智能交易系统和脚本设置 # 文件编码 = UCS-class="num">2 LE with BOM(需要!!!) class=class="str">"cmt">// 此为 Unicode # ------------------------------------------------------------------- TimeEurWinter = class="num">10:class="num">00 # 欧洲时段开始冬季时间 (服务器时间) TimeEurSummer = class="num">09:class="num">00 # 欧洲时段开始夏季时间 (服务器时间) ColorSessionEur = class="num">224,class="num">255,class="num">255 # 欧洲时段颜色 ColorSessionUsd = class="num">255,class="num">240,class="num">245 # 美洲时段颜色 NumberColorDays = class="num">10 # 高亮天数 (时段) class="macro">#class="kw">property copyright "Copyright class="num">2020, Malik Arykov" class="macro">#class="kw">property link "malik.arykov@gmail.com" class="macro">#class="kw">property strict class="macro">#include <Cayman/Params.mqh> class=class="str">"cmt">// 应用程序参数名 class="macro">#define APP_TIME_EUR_SUMMER "TimeEurSummer" class="macro">#define APP_TIME_EUR_WINTER "TimeEurWinter" class="macro">#define APP_TIME_TRADE_ASIA "TimeTradeAsia" class="macro">#define APP_COLOR_SESSION_EUR "ColorSessionEur" class="macro">#define APP_COLOR_SESSION_USD "ColorSessionUsd" class="macro">#define APP_NUMBER_COLOR_DAYS "NumberColorDays" class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 智能交易系统和脚本的常用设置 | class=class="str">"cmt">// ----------------------------------------------------------------------------- class AppSettings { class="kw">private: Params *m_params; class="kw">public: class=class="str">"cmt">// 设置位于 AppSettings.txt 文件 class="type">class="kw">string TimeEurSummer; class=class="str">"cmt">// 欧洲时段开始夏季时间 class="type">class="kw">string TimeEurWinter; class=class="str">"cmt">// 欧洲时段开始冬季时间 class="type">class="kw">string TimeTradeAsia; class=class="str">"cmt">// 亚洲走廊交易结束时间 class="type">color ColorSessionEur; class=class="str">"cmt">// 欧洲时段颜色 class="type">color ColorSessionUsd; class=class="str">"cmt">// 美洲时段颜色 class="type">int NumberColorDays; class=class="str">"cmt">// 高亮天数 class=class="str">"cmt">// 由程序设置 class="type">class="kw">string PeriodTrends; class=class="str">"cmt">// 趋势计算周期 (D1,H4) class="type">class="kw">string TradePlan; class=class="str">"cmt">// 交易方向(简要计划) class="type">bool IsValid; class=class="str">"cmt">// 参数有效性 class=class="str">"cmt">// 方法 AppSettings(); ~AppSettings() { class="kw">delete m_params; }; class="type">void Dump(class="type">class="kw">string sender); }; class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 构造函数 | class=class="str">"cmt">// ----------------------------------------------------------------------------- AppSettings::AppSettings() { IsValid = true; m_params = new Params(); m_params.Load(PATH_APP_SETTINGS); if (m_params.Total() == class="num">0) { PrintFormat("%s / ERROR: Invalid file / %s", __FUNCTION__, PATH_APP_SETTINGS); IsValid = class="kw">false; class="kw">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));
◍ 把配置落进日志:Dump 与全局指针
这段逻辑承接上一节的参数读取,把已经转好的颜色与天数变量通过 Dump 方法打印到 MT5 专家日志。注意 USD 会话颜色那两行实际复用了 ColorSessionEur 变量,若直接抄进实盘面板,美元时段会画出和欧元时段一样的颜色——这是个容易漏看的笔误。
Dump 里用了 PrintFormat 的格式化输出,例如 %s = %s / %s 配合 ColorToString 的第二参数 true,能把颜色同时以名称与 ARGB 数值双形式打出,方便你核对主题配置是否生效。
末尾的 extern AppSettings *gAppSettings 把设置对象暴露为全局指针,其他模块直接用 gAppSettings->Dump(__FUNCTION__) 就能在任意函数内吐出当前配置。外汇与贵金属行情受时段切换影响明显,这类会话配色误设可能在高风险波动期干扰你的视觉判断,建议开 MT5 跑一遍下面代码确认输出。
ColorSessionUsd = StringToColor(m_params.GetValue(APP_COLOR_SESSION_USD)); NumberColorDays = (class="type">int)StringToInteger(m_params.GetValue(APP_NUMBER_COLOR_DAYS)); } class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 打印设定参数 | class=class="str">"cmt">// ----------------------------------------------------------------------------- class="type">void AppSettings::Dump(class="type">class="kw">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); } class="kw">extern AppSettings *gAppSettings; class=class="str">"cmt">// 应用程序设置
「分析器参数怎么落盘与回灌」
Cayman 智能交易系统把 AnalyzerTrend、AnalyserLevel、AnalyserVirtSL 这类分析器各自绑死在一个时间帧上,只有该周期走完一根新柱才会触发计算。参数不写在 EA 内部,而是以 Key=Value 文本落盘,路径形如 Files\Cayman\Params\128968168864101576\exp_05_Lev607A160E_H4.txt,其中 128968168864101576 就是 ChartID() 转出来的图表 ID,H4 代表跟踪周期。 重启 EA(比如切了时间帧再切回来)时,基类 Analyser 会从对应 txt 把参数读回;若新柱时间还没到,就沿用前一根柱的 resultAnalyser 与 colorAnalyser,并把它们打到 EA 注释里,避免重复算。下面这段是实际文件抽出来的内容,原文件无注释,这里补了行内说明方便对照。 别把文件命名当摆设:exp_05_Lev607A160E_H4 里 05 是分析器类型、Lev607A160E 是具体级别名,改了名 EA 就找不回旧参数,相当于分析器被静默重置。
class=class="str">"cmt">// 交易级别参数 nameObj=Lev607A160E class=class="str">"cmt">// 交易级别名称 kindLevel=class="num">1 class=class="str">"cmt">// 级别类型 (class="num">1 - 阻力) riskValue=class="num">1.00 class=class="str">"cmt">// 级别突破时的成交量 (class="num">1) riskUnit=class="num">1 class=class="str">"cmt">// 交易量变化单位(保证金的 class="num">1 - %) (class="num">1 - 保证金的百分比 %) algClose=class="num">2 class=class="str">"cmt">// 成交平仓算法(class="num">2 – 两根修正柱线) ticketNew=class="num">0 class=class="str">"cmt">// 在级别突破时的成交单据 ticketOld=class="num">0 class=class="str">"cmt">// 在级别突破时的平仓单据 profits=class="num">0 class=class="str">"cmt">// 以点数为单位的计划内利润 losses=class="num">0 class=class="str">"cmt">// 以点数为单位的计划内亏损 class=class="str">"cmt">// 分析器参数 symbol=EURUSD class=class="str">"cmt">// 品种名称 period=class="num">16388 class=class="str">"cmt">// 分析器周期 (H4) time0Bar=class="num">1618603200 class=class="str">"cmt">// 零号柱线时间 (秒数) typeAnalyser=class="num">5 class=class="str">"cmt">// 分析器类型 colorAnalyser=class="num">16711935 class=class="str">"cmt">// 分析器结果的颜色 resultAnalyser=Lev607A160E, H4, class="num">20:class="num">00, RS class=class="str">"cmt">// 分析器结果
脚本怎么把级别参数喂给EA
SetTradeLevel 脚本在图表上选中一条直线、趋势线或矩形后,会识别该对象为交易级别,并把参数写到 Files\Cayman\Params\128968168864101576\exp_05_Lev607A160E_H4.txt 这类路径下,随后通过 SendCommand 把命令和文件路径推给 EA。 EA 侧在每次 OnTick 里调用 CheckExpernalCommand(),检查名为 NAME_OBJECT_CMD 的标签对象是否存在。命中后读取命令与文件路径,马上删掉该对象,再按文件名定位正在跑的分析器:cmdDelete 删分析器,cmdUpdate 从文件刷新参数,cmdNew 则按文件建新分析器。 Params 类封装了「关键字=参数值」文本文件的读写逻辑。原作者提过,若在 MQL5 把 m_items 换成 CHashMap,Add/GetValue/Find 代码量会明显缩水,但因该类在 MQL4 也要用、且参数仅初始化时读一次,便沿用旧实现——银行开发圈有句老话:能跑就别动它。 下面这段是 SendCommand 相关核心代码,注意它用 OBJ_LABEL 做命令载体,把文件路径塞进 OBJPROP_TEXT,把命令枚举塞进 OBJPROP_ZORDER,EA 靠读这两个属性就知道要干什么。
class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 将级别参数发送到智能交易系统 | class=class="str">"cmt">// ----------------------------------------------------------------------------- NCommand SendCommand() { class=class="str">"cmt">// 加载级别参数(如果有) Params *params = new Params(); class="type">class="kw">string speriod = UConvert::PeriodToStr(_Period); params.Load(PREFIX_EXPERT, anaLevel, gNameLev, speriod); class=class="str">"cmt">// 定义命令 NCommand cmd = (gKindLevel == levUnknown) ? cmdDelete : (params.Total() > class="num">0) ? cmdUpdate : cmdCreate; class=class="str">"cmt">// 保存参数 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, class="num">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(); class=class="str">"cmt">// 向智能交易系统发送命令 params.SendCommand(cmd); class="kw">delete params; class="kw">return cmd; } class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 向智能交易系统发送命令 | class=class="str">"cmt">// ----------------------------------------------------------------------------- class="type">void Params::SendCommand(NCommand cmd) { class="type">class="kw">string nameObj = NAME_OBJECT_CMD; ObjectCreate(class="num">0, nameObj, OBJ_LABEL, class="num">0, class="num">0, class="num">0); ObjectSetString(class="num">0, nameObj, OBJPROP_TEXT, m_path); ObjectSetInteger(class="num">0, nameObj, OBJPROP_ZORDER, cmd); ObjectSetInteger(class="num">0, nameObj, OBJPROP_TIMEFRAMES, class="num">0); } class="macro">#class="kw">property copyright "Copyright class="num">2020, Malik Arykov" class="macro">#class="kw">property link "malik.arykov@gmail.com" class="macro">#include <Arrays/ArrayString.mqh> class="macro">#include <Cayman/UConvert.mqh> class="macro">#include <Cayman/UFile.mqh> class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 参数类(关键字=参数值字符串 class="macro">#注释) | class=class="str">"cmt">// ----------------------------------------------------------------------------- class Params { class="kw">private: class="type">class="kw">string m_path; class=class="str">"cmt">// 参数文件路径 NCommand m_cmd; class=class="str">"cmt">// 智能交易系统命令 CArrayString *m_items; class=class="str">"cmt">// 配对数组 {关键字=参数值}
◍ 参数容器的增查与析构细节
在 MT5 的 EA 或指标工程里,把配置项塞进一个 Params 类来管理,比直接写全局变量要干净得多。下面这段实现用的是 CArrayString 存「key=value」文本行,构造时 new、析构时 delete,避免内存泄漏。
[CODE]
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++) {
[/CODE]
逐行看:Params() 里 m_items = new CArrayString(); 在堆上开字符串数组;析构 ~Params() 直接 delete m_items 释放。Add(key,value) 先 Find 看 key 在不在,在就 Update 返回 false(算更新),不在就 Add 返回 true(算新增)——调用方能用返回值判断是首次写入还是覆盖。
GetValue 的查找链是 Find→取行→StringFind 找「=」→截子串并 Trim。若 key 不存在或行里没「=」,都返回 NULL 并打印函数级错误。外汇与贵金属交易使用此类自管参数模块时波动剧烈、风险高,参数读写异常可能让 EA 逻辑偏离预期,建议在 MT5 策略测试器里先跑空载校验 Add/GetValue 的往返一致性。
class="type">int Find(class="type">class="kw">string key); class="kw">public: Params(); ~Params() { class="kw">delete m_items; }; class="type">void Clear() { m_items.Clear(); }; class="type">int Total() { class="kw">return m_items.Total(); }; class="type">class="kw">string Path() { class="kw">return m_path; }; CArrayString *Items() { class="kw">return m_items; }; class="type">void Add(class="type">class="kw">string line) { m_items.Add(line); }; class="type">bool Add(class="type">class="kw">string key, class="type">class="kw">string value); class="type">class="kw">string GetValue(class="type">class="kw">string key); class="type">void Load(class="type">class="kw">string prefix, class="type">int typeAnalyser, class="type">class="kw">string nameObj, class="type">class="kw">string speriod); class="type">void Load(class="type">class="kw">string path); class="type">void Save(); class="type">void SendCommand(NCommand cmd); NCommand TakeCommand(); class="type">void Dump(class="type">class="kw">string sender); }; class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 默认构造函数 | class=class="str">"cmt">// ----------------------------------------------------------------------------- Params::Params() { m_items = new CArrayString(); } class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 添加一个 key=value 对 | class=class="str">"cmt">// ----------------------------------------------------------------------------- class="type">bool Params::Add(class="type">class="kw">string key, class="type">class="kw">string value) { class="type">int j = Find(key); class="type">class="kw">string line = key + "=" + value; if (j >= class="num">0) { class=class="str">"cmt">// 更新 m_items.Update(j, line); class="kw">return class="kw">false; } else { class=class="str">"cmt">// 添加 m_items.Add(line); class="kw">return true; } } class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 依据关键字获取参数值 | class=class="str">"cmt">// ----------------------------------------------------------------------------- class="type">class="kw">string Params::GetValue(class="type">class="kw">string key) { class=class="str">"cmt">// 搜索关键字 class="type">int j = Find(key); if (j < class="num">0) class="kw">return NULL; class=class="str">"cmt">// 无关键字 class=class="str">"cmt">// 检查分隔符 class="type">class="kw">string line = m_items.At(j); j = StringFind(line, "="); if (j < class="num">0) { class=class="str">"cmt">// 没有 = PrintFormat("%s / ERROR: Invalid class="type">class="kw">string %s", __FUNCTION__, line); class="kw">return NULL; } class=class="str">"cmt">// 返回数值 class="kw">return UConvert::Trim(StringSubstr(line, j + class="num">1)); } class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 依据关键字查找参数值 | class=class="str">"cmt">// ----------------------------------------------------------------------------- class="type">int Params::Find(class="type">class="kw">string key) { class="type">int index = -class="num">1; for (class="type">int j = class="num">0; j < m_items.Total(); j++) {
「参数读写与命令下发落到文件对象」
上面这段把分析器的参数持久化逻辑收口了:用 ChartID 拼出独立路径,避免多图表实例互相覆盖参数文件。文件名格式是 %s%02i_%s_%s.txt,其中 typeAnalyser 占两位、nameObj 和周期字符串后缀,实盘里开两个 EURUSD 的 H1 与 M15 窗口会生成不同文件。
Load 的重载先从文本按行切分,遇到 # 开头的整行直接跳过,行内 # 之后的内容也截掉,所以你在参数文件里写注释不会影响解析。m_items 只存非空行,意味着空行和纯注释不占内存。
Save 是覆盖写:把 m_items 逐行拼成文本再调用 UFile::SaveText 重写原路径。如果你在 MT5 里改了参数没生效,先确认 m_path 指向的文件是不是被别的图表实例占用。
SendCommand 借一个隐藏的 OBJ_LABEL 把文件路径写进对象属性,再把命令枚举塞进 ZORDER,EA 端轮询这个对象就能拿到指令。外汇和贵金属波动剧烈,这种文件+对象的跨模块通信要在多图表下自测竞态。
if (StringFind(m_items.At(j), key) == class="num">0) { index = j; class="kw">break; } } class="kw">return index; } class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 加载参数 | class=class="str">"cmt">// ----------------------------------------------------------------------------- class="type">void Params::Load(class="type">class="kw">string prefix, class="type">int typeAnalyser, class="type">class="kw">string nameObj, class="type">class="kw">string speriod) { class="type">class="kw">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); } class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 加载参数 | class=class="str">"cmt">// ----------------------------------------------------------------------------- class="type">void Params::Load(class="type">class="kw">string path) { m_path = path; if (!FileIsExist(m_path)) class="kw">return; class=class="str">"cmt">//PrintFormat("%s / %s", __FUNCTION__, m_path); class="type">class="kw">string text = UFile::LoadText(m_path); if (text == NULL) class="kw">return; class=class="str">"cmt">// 将文本切分成数行 class="type">class="kw">string line, lines[]; class="type">int numLines = StringSplit(text, DLM_LINE, lines); for (class="type">int j = class="num">0; j < numLines; j++) { line = lines[j]; class=class="str">"cmt">// 删除注释 class="type">int k = StringFind(line, "#"); if (k == class="num">0) class="kw">continue; class=class="str">"cmt">// 整个字符串是一条注释 if (k > class="num">0) line = StringSubstr(line, class="num">0, k); class=class="str">"cmt">// 添加一个非空字符串 if (line != "") m_items.Add(line); } } class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 保存参数 | class=class="str">"cmt">// ----------------------------------------------------------------------------- class="type">void Params::Save() { class="type">class="kw">string text = ""; for (class="type">int j = class="num">0; j < m_items.Total(); j++) { text += m_items.At(j) + "\n"; } class=class="str">"cmt">// 重写现有文件 UFile::SaveText(text, m_path, true); } class=class="str">"cmt">// ----------------------------------------------------------------------------- class=class="str">"cmt">// 向智能交易系统发送命令 | class=class="str">"cmt">// ----------------------------------------------------------------------------- class="type">void Params::SendCommand(NCommand cmd) { class="type">class="kw">string nameObj = NAME_OBJECT_CMD; ObjectCreate(class="num">0, nameObj, OBJ_LABEL, class="num">0, class="num">0, class="num">0); ObjectSetString(class="num">0, nameObj, OBJPROP_TEXT, m_path); ObjectSetInteger(class="num">0, nameObj, OBJPROP_ZORDER, cmd);