来自专业程序员的提示(第二部分):在智能交易系统、脚本和外部程序之间存储和交换参数

Malik Arykov | 14 七月, 2021

内容


概述

在本文里,我们将讨论终端重启(关闭)后如何恢复参数。 所有示例都是来自我的 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。 其中 (从左至右)

我们为什么需要这样做? 例如,此数据可用于分析交易。 此处是我如何运用它:当一笔挂单被触发,我提取平仓算法值,并创建一个虚拟止损分析器 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; // 应用程序设置

以这个解决方案,您可以:


分析器参数

Cayman 智能交易系统可管理各种分析器,诸如 AnalyzerTrend、AnalyserLevel、AnalyserVirtSL。 每个分析器都与特定的时间帧相关联。 这意味着只有在指定时间帧内出现新柱线时才会启动分析器。 分析器示例存储在文本文件中,带有 Key = Value 字符串。 例如,H4 交易级别分析器将其参数存储在 Files\Cayman\Params\128968168864101576\exp_05_Lev607A160E_H4.txt 文件当中

以下是带注释的文件内容(实际文件不含注释)

// 交易级别参数
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 脚本查找所选对象(交易级别),并为其设置参数。

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 粉丝正在阅读本文,请在评论中分享。 

开始使用服务:

然后启动脚本(不要忘记在 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” 声音,并为亏损了结的成交发出“炸弹”声音。 但随后我就被炸弹搞得疲惫不堪。


结束语

存储参数最可靠、最方便的方法是运用文本文件。 甚而,在任何操作系统(应用程序)里都完全支持/缓存文件操作。