金融工具的基础货币、报价货币和保证金货币

每种金融工具最重要的特性之一是其工作货币:

  • 表示购买或出售资产的基础货币(对于外汇金融工具)
  • 利润计算(报价)货币
  • 保证金计算货币

MQL 程序可以使用 SymbolInfoString 函数和下表中的三个特性来获取这些货币的名称。

标识符

说明

SYMBOL_CURRENCY_BASE

基础货币

SYMBOL_CURRENCY_PROFIT

盈利货币

SYMBOL_CURRENCY_MARGIN

保证金货币

这些特性有助于分析外汇金融工具,许多经纪商会在这些金融工具的名称中添加各种前缀和后缀,以及兑换金融工具。特别是,其算法将能够找到一个交易品种,以获得两种给定货币的交叉汇率或选择具有给定共同报价货币的指数组合。

有一个很常见的任务是根据特定需求来搜索工具,因此我们创建一个类 SymbolFilter (SymbolFilter.mqh) 来构建一个合适交易品种及其选定特性的列表。将来,我们不仅要用这个类来分析货币,还要分析其他特性。

首先,我们将考虑一个简化版本,然后用方便的功能予以补充。

开发中,我们将使用现成的辅助工具:一个关联映射数组 (MapArray.mqh) 来存储所选类型的键值对和一个交易品种特性监视器 (SymbolMonitor.mqh)。

#include <MQL5Book/MapArray.mqh>
#include <MQL5Book/SymbolMonitor.mqh>

为了简化在数组中累加工作结果的语句,我们使用了 PUSH 宏的改进版本,我们已经在前面的示例中看到过该版本及其多维数组的扩展版本(在这种情况下,不可能进行简单的赋值)。

#define PUSH(A,V) (A[ArrayResize(AArraySize(A) + 1ArraySize(A) * 2) - 1] = V)
#define EXPAND(A) (ArrayResize(AArrayRange(A0) + 1ArrayRange(A0) * 2) - 1)

SymbolFilter 类的对象必须存储用于筛选交易品种的特性值。因此,我们将在类中描述三个 MapArray 数组,分别用于整数、实数和字符串特性。

class SymbolFilter
{
   MapArray<ENUM_SYMBOL_INFO_INTEGER,longlongs;
   MapArray<ENUM_SYMBOL_INFO_DOUBLE,doubledoubles;
   MapArray<ENUM_SYMBOL_INFO_STRING,stringstrings;
   ...

使用重载的 let 方法来设置所需的筛选特性。

public:
   SymbolFilter *let(const ENUM_SYMBOL_INFO_INTEGER propertyconst long value)
   {
      longs.put(propertyvalue);
      return &this;
   }
   
   SymbolFilter *let(const ENUM_SYMBOL_INFO_DOUBLE propertyconst double value)
   {
      doubles.put(propertyvalue);
      return &this;
   }
   
   SymbolFilter *let(const ENUM_SYMBOL_INFO_STRING propertyconst string value)
   {
      strings.put(propertyvalue);
      return &this;
   }
   ...

请注意,这些方法会返回一个指向过滤器的指针,允许你将条件写成一个链:例如,如果在代码的前面说明了 SymbolFilter 类的一个对象 f,那么你可以对价格类型和盈利货币的名称施加两个条件,如下所示:

f.let(SYMBOL_CHART_MODESYMBOL_CHART_MODE_LAST).let(SYMBOL_CURRENCY_PROFIT"USD");

满足条件的交易品种数组排列是由筛选对象在 select 方法的几种变体中执行的,下面给出了其中最简单的一种(其他选项将在后面讨论)。

watch 参数定义了交易品种的搜索上下文:搜索 Market Watch 中搜索对应 true,搜索所有可用交易品种则对应 false。将用匹配交易品种的名称来填充输出数组 symbols。我们已经知道了该方法内部的代码结构:其中包含一个循环,该循环会遍历交易品种,并为每个交易品种创建一个监视对象 m

   void select(const bool watchstring &symbols[]) const
   {
      const int n = SymbolsTotal(watch);
      for(int i = 0i < n; ++i)
      {
         const string s = SymbolName(iwatch);
         SymbolMonitor m(s);
         if(match<ENUM_SYMBOL_INFO_INTEGER,long>(mlongs)
         && match<ENUM_SYMBOL_INFO_DOUBLE,double>(mdoubles)
         && match<ENUM_SYMBOL_INFO_STRING,string>(mstrings))
         {
            PUSH(symbolss);
         }
      }
   }

借助监视器,我们可以以统一的方式获得任何特性的值。检查与 longsdoublesstrings 数组中的条件集匹配的当前交易品种的特性是否是通过辅助方法 match 实现的。只有所有请求的特性匹配,交易品种名称才会保存在 symbols 的输出数组中。

在最简单的情况下,match 方法的实现如下(随后其将被改变)。

protected:
   template<typename K,typename V>
   bool match(const SymbolMonitor &mconst MapArray<K,V> &dataconst
   {
      for(int i = 0i < data.getSize(); ++i)
      {
         const K key = data.getKey(i);
         if(!equal(m.get(key), data.getValue(i)))
         {
            return false;
         }
      }
      return true;
   }

如果 data 数组中至少有一个值与相应的字符特性不匹配,则该方法返回 false。如果所有特性都匹配(或者这种类型的特性没有条件),则该方法返回 true

使用 equal 比较两个值。鉴于在这些特性中可能存在 double 类型的特性,其实现并不像人们想象的那么简单。

   template<typename V>
   static bool equal(const V v1const V v2)
   {
      return v1 == v2 || eps(v1v2);
   }

对于 double 类型,表达式 v1 == v2 可能不适用于接近的数字,因此应考虑实数 DBL_EPSILON 类型的精度。这是在一个单独的方法 eps 中完成的,由于模板的原因,该方法可为 double 类型和所有其他类型分别重载。

   static bool eps(const double v1const double v2)
   {
      return fabs(v1 - v2) < DBL_EPSILON * fmax(v1v2);
   }
   
   template<typename V>
   static bool eps(const V v1const V v2)
   {
      return false;
   }

如果除 double 之外的任何类型的值都相等,则不会调用模板方法 eps;在所有其他情况下(包括值不同时),其会按要求返回 false(因此,v1 == v2 为唯一条件)。

上述筛选器选项仅允许你检查特性是否相等。但在实践中,经常需要分析不相等的条件,以及大于/小于的条件。出于这个原因,SymbolFilter 类具有带基本比较运算的 IS 枚举(如果需要,可以进行补充)。

class SymbolFilter
{
   ...
   enum IS
   {
      EQUAL,
      GREATER,
      NOT_EQUAL,
      LESS
   };
   ...

对于 ENUM_SYMBOL_INFO_INTEGER、ENUM_SYMBOL_INFO_DOUBLE 和ENUM_SYMBOL_INFO_STRING 枚举中的每个特性,不仅需要保存所需的特性值(注意关联数组 longsdoublesstrings),还需要保存新的 IS 枚举中的比较方法。

由于标准枚举的元素具有不重叠的值(有一种与 交易量 相关的例外,但不是很重要),因此为比较方法保留一个公共映射数组 conditions 是有意义的。这就提出了一个问题:为映射键选择哪种类型才能从技术上“组合”不同的枚举。为了做到这一点,我们必须说明伪枚举 ENUM_ANY,其仅表示某种类型的泛型枚举。注意,所有枚举都有一个等价于整数 int 的内部表示,因此可以相互简化。

   enum ENUM_ANY
   {
   };
   
   MapArray<ENUM_ANY,ISconditions;
   MapArray<ENUM_ANY,longlongs;
   MapArray<ENUM_ANY,doubledoubles;
   MapArray<ENUM_ANY,stringstrings;
   ...

现在,我们可以通过添加指定比较方法的 cmp 输入参数来补全设置所需特性值的所有 let 方法。默认情况下,该参数用于设置相等检查 (EQUAL)。

   SymbolFilter *let(const ENUM_SYMBOL_INFO_INTEGER propertyconst long value,
      const IS cmp = EQUAL)
   {
      longs.put((ENUM_ANY)propertyvalue);
      conditions.put((ENUM_ANY)propertycmp);
      return &this;
   }

这是整数特性的一个变体。其他两个重载以同样的方式改变。

考虑到关于比较和同时消除映射数组中不同类型键的不同方法的新信息,我们修改了 match 方法。其中,对于每个指定的特性,我们基于 data 映射数组中的键从 conditions 数组中检索一个条件,并使用 switch 运算符执行适当的检查。

   template<typename V>
   bool match(const SymbolMonitor &mconst MapArray<ENUM_ANY,V> &dataconst
   {
      // dummy variable to select m.get method overload below
      static const V type = (V)NULL;
      // cycle by conditions imposed on the properties of the symbol
      for(int i = 0i < data.getSize(); ++i)
      {
         const ENUM_ANY key = data.getKey(i);
         // choice of comparison method in the condition
         switch(conditions[key])
         {
         case EQUAL:
            if(!equal(m.get(keytype), data.getValue(i))) return false;
            break;
         case NOT_EQUAL:
            if(equal(m.get(keytype), data.getValue(i))) return false;
            break;
         case GREATER:
            if(!greater(m.get(keytype), data.getValue(i))) return false;
            break;
         case LESS:
            if(greater(m.get(keytype), data.getValue(i))) return false;
            break;
         }
      }
      return true;
   }

新模板 greater 方法实现起来很简单。

   template<typename V>
   static bool greater(const V v1const V v2)
   {
      return v1 > v2;
   }

现在,match 方法调用可以以更短的形式编写,因为模板 V 的唯一剩余类型是由传递的data 自变量自动确定的(longsdoublesstrings 数组中的一个)。

   void select(const bool watchstring &symbols[]) const
   {
      const int n = SymbolsTotal(watch);
      for(int i = 0i < n; ++i)
      {
         const string s = SymbolName(iwatch);
         SymbolMonitor m(s);
         if(match(mlongs)
            && match(mdoubles)
            && match(mstrings))
         {
            PUSH(symbolss);
         }
      }
   }

这还不是 SymbolFilter 类的最终版本,但是我们已经可以测试其实际运行效果了。

我们来创建一个脚本 SymbolFilterCurrency.mq5,该脚本可根据基础货币和盈利货币的特性筛选交易品种;此处为美元。默认情况下,MarketWatchOnly 参数仅在 Market Watch 中进行搜索。

#include <MQL5Book/SymbolFilter.mqh>
   
input bool MarketWatchOnly = true;
   
void OnStart()
{
   SymbolFilter f;   // filter object
   string symbols[]; // array for results
   ...

比方说,我们希望找到有直接报价的外汇金融工具,即“美元”出现在其名称的开头。为了不依赖于特定经纪商的名称构成细节,我们将使用 SYMBOL_CURRENCY_BASE 特性,其包含第一种货币。

让我们写下交易品种的基础货币等于美元的条件,并应用过滤器。

   f.let(SYMBOL_CURRENCY_BASE"USD")
   .select(MarketWatchOnlysymbols);
   Print("===== Base is USD =====");
   ArrayPrint(symbols);
   ...

生成的数组会输出到日志中。

===== Base is USD =====
"USDCHF" "USDJPY" "USDCNH" "USDRUB" "USDCAD" "USDSEK" "SP500m" "Brent" 

如你所见,该数组不仅包括交易代码以美元开头的外汇交易品种,还包括 S&P500 指数和商品(原油)。后两个交易品种以美元报价,但它们也有相同的基础货币。同时,外汇交易品种的报价货币(也是盈利货币)是第二位的,且不同于美元。这允许你补充筛选条件,让非外汇交易品种不再匹配。

我们清除数组,添加一个条件,即盈利货币不等于“美元”,并再次请求合适的交易品种(前面的条件保存在 f 对象中)。

   ...
   ArrayResize(symbols0);
   
   f.let(SYMBOL_CURRENCY_PROFIT"USD"SymbolFilter::IS::NOT_EQUAL)
   .select(MarketWatchOnlysymbols);
   Print("===== Base is USD and Profit is not USD =====");
   ArrayPrint(symbols);
}

这次,日志中实际上只显示了你要查找的交易品种。

===== Base is USD and Profit is not USD =====
"USDCHF" "USDJPY" "USDCNH" "USDRUB" "USDCAD" "USDSEK"