Appearance
金融工具与市场报价
MetaTrader 5 允许用户分析和交易金融工具(也称为交易品种或代码),这些金融工具几乎构成了终端所有子系统的基础。图表、指标以及报价的价格历史记录都与交易品种相关联。终端的主要功能是基于诸如交易订单、交易、保证金要求控制以及交易账户历史记录等金融工具构建的。
通过终端,经纪商向交易者提供指定的交易品种列表,每个用户可以从中选择自己偏好的品种,从而形成市场报价窗口的内容。市场报价窗口确定了终端请求在线报价的交易品种,并允许用户打开图表和查看历史数据。
MQL5 API 提供了类似的软件工具,使您能够查看和分析所有交易品种的特征,将它们添加到市场报价窗口中,或者从其中排除。
除了经纪商提供信息的标准交易品种外,MetaTrader 5 还能够创建自定义交易品种:它们的属性和价格历史数据可以从任意数据源加载,并使用公式或 MQL 程序进行计算。自定义交易品种也会出现在市场报价窗口中,可用于测试交易策略和进行技术分析,然而,它们也有一个自然的限制——无法使用常规的 MQL5 API 工具进行在线交易,因为这些交易品种在服务器上并不存在。自定义交易品种将在本书的最后一部分,即第七章中单独进行介绍。
不久前,在相关章节中,我们已经通过指标示例涉及了单个交易品种的价格数据时间序列,包括历史数据分页。所有这些功能实际上都假定相应的交易品种已经在市场报价窗口中启用。对于多货币指标和智能交易系统来说尤其如此,它们不仅涉及图表的当前交易品种,还涉及其他交易品种。在本章中,我们将学习如何从 MQL 程序中管理市场报价列表。
关于图表的章节已经描述了一些通过当前图表的基本属性获取函数(如 Point
、Digits
)可获取的交易品种属性,因为图表离不开与之相关联的交易品种。现在我们将研究交易品种的大多数属性,包括它们的详细说明。完整的属性集合可以在 MQL5 官方网站的文档中找到。
获取可用品种和市场报价列表
MQL5 API 提供了多个用于处理品种的函数。利用这些函数,你可以获取可用品种的总数、市场报价窗口中所选品种的数量以及它们的名称。如你所知,终端中可用的品种通用列表以分层结构的形式显示在“品种”对话框中,用户可以通过“视图” -> “品种”命令或在市场报价窗口的上下文菜单中打开该对话框。此列表既包括经纪商提供的品种,也包括本地创建的自定义品种。你可以使用 SymbolsTotal
函数来获取品种的总数。
c
int SymbolsTotal(bool selected)
selected
参数指定是仅请求市场报价窗口中的品种(true
),还是请求所有可用的品种(false
)。
SymbolName
函数通常与 SymbolsTotal
函数一起使用。它通过品种的索引返回其名称(这里不考虑将品种存储分组到逻辑文件夹中,可参考属性 SYMBOL_PATH
)。
c
string SymbolName(int index, bool selected)
index
参数指定请求的品种的索引。索引值必须在 0 到品种数量之间,具体取决于第二个参数 selected
指定的请求上下文:true
会将枚举范围限制为市场报价窗口中选择的品种,而 false
则匹配所有品种(与 SymbolsTotal
类似)。因此,在调用 SymbolName
时,应将 selected
参数设置为与之前调用 SymbolsTotal
时相同的值,以确定索引范围。
如果发生错误,特别是当请求的索引超出列表范围时,该函数将返回一个空字符串,并且错误代码将写入变量 _LastError
。
需要注意的是,当启用 selected
选项时,SymbolsTotal
和 SymbolName
这两个函数返回的是终端实际更新的品种列表信息,即与服务器进行持续同步且 MQL 程序可以获取其报价历史的品种。此列表可能比市场报价窗口中可见的列表更大,市场报价窗口中的元素是显式添加的:由用户或 MQL 程序添加(关于如何添加,请参阅“编辑市场报价列表”部分)。窗口中不可见的这些品种会在需要计算交叉汇率时由终端自动连接。在品种属性中,有两个属性可以区分显式选择(SYMBOL_VISIBLE
)和隐式选择(SYMBOL_SELECT
),这将在“品种状态检查”部分进行讨论。严格来说,对于 SymbolsTotal
和 SymbolName
函数,将 selected
设置为 true
匹配的是 SYMBOL_SELECT
为 true
的扩展品种集,而不仅仅是 SYMBOL_VISIBLE
等于 true
的品种。
市场报价窗口中品种的返回顺序与终端窗口中的顺序相对应(考虑用户可能进行的重新排列,不考虑如果启用了按任何列进行排序的情况)。无法通过编程方式更改市场报价窗口中品种的顺序。
品种通用列表的顺序由终端本身设置(市场报价窗口的内容和排序不会影响它)。
下面来看一个简单的脚本 SymbolList.mq5
示例,它会将可用品种打印到日志中。输入参数 MarketWatchOnly
允许用户将列表限制为仅市场报价窗口中的品种(如果该参数为 true
),或者获取完整列表(false
)。
c
#property script_show_inputs
#include <MQL5Book/PRTF.mqh>
input bool MarketWatchOnly = true;
void OnStart()
{
const int n = SymbolsTotal(MarketWatchOnly);
Print("Total symbol count: ", n);
// 将市场报价窗口中的品种列表或所有可用品种写入日志
for(int i = 0; i < n; ++i)
{
PrintFormat("%4d %s", i, SymbolName(i, MarketWatchOnly));
}
// 故意请求超出范围的索引以显示错误
PRTF(SymbolName(n, MarketWatchOnly)); // MARKET_UNKNOWN_SYMBOL(4301)
}
以下是一个示例日志:
plaintext
Total symbol count: 10
0 EURUSD
1 XAUUSD
2 BTCUSD
3 GBPUSD
4 USDJPY
5 USDCHF
6 AUDUSD
7 USDCAD
8 NZDUSD
9 USDRUB
SymbolName(n,MarketWatchOnly)= / MARKET_UNKNOWN_SYMBOL(4301)
编辑市场报价列表
通过使用 SymbolSelect
函数,MQL 程序开发者能够向市场报价列表(Market Watch)中添加特定的交易品种,或者从该列表中移除它。
c
bool SymbolSelect(const string name, bool select)
name
参数包含受此操作影响的交易品种名称。根据 select
参数的值,若为 true
,则将交易品种添加到市场报价列表中;若为 false
,则从列表中移除该交易品种。交易品种名称区分大小写:例如,“EURUSD.m” 与 “EURUSD.M” 是不同的。
该函数返回操作成功(true
)或失败(false
)的指示。错误代码可以在 _LastError
中找到。
如果存在针对某个交易品种的打开图表或未平仓头寸,那么该交易品种无法被移除。此外,你不能删除一个在添加到市场报价列表的合成(自定义)交易品种的计算公式中被明确使用的交易品种。
应该记住,即使对于某个交易品种不存在打开的图表和头寸,它也可能被 MQL 程序间接使用:例如,程序可能会读取它的报价或报价点历史记录。移除这样的交易品种可能会导致这些程序出现问题。
下面的脚本 SymbolRemoveUnused.mq5
能够隐藏所有未被明确使用的交易品种,所以建议先在模拟账户上测试该脚本,或者通过上下文菜单保存当前的交易品种设置。
c
#include <MQL5Book/MqlError.mqh>
#define PUSH(A,V) (A[ArrayResize(A, ArraySize(A) + 1) - 1] = V)
void OnStart()
{
// 请求用户确认是否进行删除操作
if(IDOK == MessageBox("This script will remove all unused symbols"
" from the Market Watch. Proceed?", "Please, confirm", MB_OKCANCEL))
{
const int n = SymbolsTotal(true);
ResetLastError();
string removed[];
// 以逆序遍历市场报价列表中的交易品种
for(int i = n - 1; i >= 0; --i)
{
const string s = SymbolName(i, true);
if(SymbolSelect(s, false))
{
// 记录已删除的交易品种
PUSH(removed, s);
}
else
{
// 若出现错误,显示错误原因
PrintFormat("Can't remove '%s': %s (%d)", s, E2S(_LastError), _LastError);
}
}
const int r = ArraySize(removed);
PrintFormat("%d out of %d symbols removed", r, n);
ArrayPrint(removed);
...
在用户确认对交易品种列表进行分析后,程序会尝试通过调用 SymbolSelect(s, false)
依次隐藏每个交易品种。这仅对未被明确使用的交易品种有效。为了不破坏索引顺序,交易品种的枚举是按逆序进行的。所有成功移除的交易品种都收集在 removed
数组中。日志会显示相关统计信息以及该数组内容。
如果市场报价列表发生了变化,然后会给用户提供一个机会,通过在循环中调用 SymbolSelect(removed[i], true)
将所有已删除的交易品种恢复到列表中。
c
if(r > 0)
{
// 可以将已删除的交易品种恢复到市场报价列表中
// (此时,窗口显示的是精简后的列表)
if(IDOK == MessageBox("Do you want to restore removed symbols"
" in the Market Watch?", "Please, confirm", MB_OKCANCEL))
{
int restored = 0;
for(int i = r - 1; i >= 0; --i)
{
restored += SymbolSelect(removed[i], true);
}
PrintFormat("%d symbols restored", restored);
}
}
}
}
以下是日志输出可能的样子。
Can't remove 'EURUSD': MARKET_SELECT_ERROR (4305)
Can't remove 'XAUUSD': MARKET_SELECT_ERROR (4305)
Can't remove 'BTCUSD': MARKET_SELECT_ERROR (4305)
Can't remove 'GBPUSD': MARKET_SELECT_ERROR (4305)
...
Can't remove 'USDRUB': MARKET_SELECT_ERROR (4305)
2 out of 10 symbols removed
"NZDUSD" "USDCAD"
2 symbols restored
请注意,尽管已恢复的交易品种按照它们在市场报价列表中原来的相对顺序恢复,但添加操作是在列表的末尾进行的,即在剩余的交易品种之后。因此,所有“被占用”的交易品种会在列表的开头,而所有已恢复的交易品种会跟随其后。这就是 SymbolSelect
函数的特定操作方式:交易品种总是被添加到列表的末尾,也就是说,不可能在特定位置插入一个交易品种。所以,列表元素的重新排列仅适用于手动编辑。
检查交易品种是否存在
MQL 程序无需遍历整个交易品种列表,就可以通过交易品种的名称来检查特定交易品种是否存在。为此,提供了 SymbolExist
函数。
c
bool SymbolExist(const string name, bool &isCustom)
在 name
参数中,你需要传入所需交易品种的名称。通过引用传递的 isCustom
参数会根据指定的交易品种是标准交易品种(false
)还是自定义交易品种(true
)来设置。
如果在标准交易品种或自定义交易品种中都未找到该交易品种,函数将返回 false
。
该函数的部分等效操作是查询 SYMBOL_EXIST
属性。
下面来分析一个简单的脚本 SymbolExists.mq5
,用于测试此功能。用户可以在脚本的参数中指定名称,然后将其传递给 SymbolExist
函数,并将结果记录到日志中。如果输入为空字符串,则会检查当前图表的交易品种。默认情况下,参数设置为 "XYZ",推测它与任何可用的交易品种都不匹配。
c
#property script_show_inputs
input string SymbolToCheck = "XYZ";
void OnStart()
{
const string _SymbolToCheck = SymbolToCheck == "" ? _Symbol : SymbolToCheck;
bool custom = false;
PrintFormat("Symbol '%s' is %s", _SymbolToCheck,
(SymbolExist(_SymbolToCheck, custom) ? (custom ? "custom" : "standard") : "missing"));
}
当脚本运行两次时,第一次使用默认值,第二次在 EURUSD 图表上使用空字符串,日志中将出现以下记录:
Symbol 'XYZ' is missing
Symbol 'EURUSD' is standard
如果你已经有自定义交易品种,或者使用简单的计算公式创建了一个新的自定义交易品种,就可以验证 custom
变量是否被正确填充。例如,如果你在终端中打开 “交易品种” 窗口并按下 “创建交易品种” 按钮,可以在 “合成工具公式” 字段中输入 “SP500/FTSE100”(指数名称可能因你的经纪商而异),并在 “交易品种名称” 字段中输入 “GBPUSD.INDEX”。点击 “确定” 后将创建一个自定义交易工具,你可以为其打开图表,我们的脚本应该会显示如下内容:
Symbol 'GBPUSD.INDEX' is custom
在设置自己的交易品种时,不要忘记不仅要设置公式,还要为点值大小和价格变动步长(跳动点)设置足够 “小” 的值。否则,合成报价序列可能会呈现 “阶梯状”,甚至退化为一条直线。
检查品种数据的相关性
由于采用分布式的客户端 - 服务器架构,客户端和服务器的数据偶尔会出现不一致的情况。例如,这种情况可能在终端会话刚启动时、连接丢失时或者计算机资源负载过重时发生。此外,某个品种刚被添加到市场报价窗口后,很可能在一段时间内数据处于不同步状态。MQL5 API 允许你使用 SymbolIsSynchronized
函数来检查特定品种的报价数据是否同步。
c
bool SymbolIsSynchronized(const string name)
若名为 name
的品种的本地数据与交易服务器上的数据同步,该函数返回 true
。
在“获取价格数组特征”这部分内容中,除了介绍其他时间序列属性外,还引入了 SERIES_SYNCHRONIZED
属性。该属性返回的同步属性含义更窄:它适用于特定品种和时间周期的组合。与这个属性不同,SymbolIsSynchronized
函数返回的是某个品种的总体历史数据的同步属性。
所有时间周期的构建只有在历史数据下载完成后才会开始。由于终端采用多线程架构和并行计算,可能会出现 SymbolIsSynchronized
返回 true
,但同一品种的某个时间周期的 SERIES_SYNCHRONIZED
属性暂时为 false
的情况。
下面来看看新函数在 SymbolListSync.mq5
指标中是如何工作的。该指标旨在定期检查市场报价窗口中所有品种的同步情况。检查周期由用户在 SyncCheckupPeriod
参数中以秒为单位进行设置。它会在 OnInit
函数中启动定时器。
c
#property indicator_chart_window
#property indicator_plots 0
input int SyncCheckupPeriod = 1; // 同步检查周期(秒)
void OnInit()
{
EventSetTimer(SyncCheckupPeriod);
}
在 OnTimer
处理程序中,我们通过循环调用 SymbolIsSynchronized
函数,将所有未同步的品种收集到一个公共字符串中,然后将这些品种显示在注释和日志中。
c
void OnTimer()
{
string unsynced;
const int n = SymbolsTotal(true);
// 检查市场报价窗口中的所有品种
for(int i = 0; i < n; ++i)
{
const string s = SymbolName(i, true);
if(!SymbolIsSynchronized(s))
{
unsynced += s + "\n";
}
}
if(StringLen(unsynced) > 0)
{
Comment("未同步的品种:\n" + unsynced);
Print("未同步的品种:\n" + unsynced);
}
else
{
Comment("市场报价窗口中的所有品种均已同步");
}
}
例如,如果我们将之前缺失的某个品种(如布伦特原油,Brent)添加到市场报价窗口中,会得到如下记录:
plaintext
未同步的品种:
Brent
在正常情况下,在指标运行期间,日志中大部分时间不应出现此类消息。不过,在通信出现问题时,可能会产生大量警报信息。
获取交易品种的最新报价点
在关于时间序列的章节中,“处理真实报价点数组”部分,我们介绍了内置结构 MqlTick
,它包含了特定交易品种在每次报价变化时已知的价格和成交量值的字段。在联机模式下,MQL 程序可以使用采用相同结构的 SymbolInfoTick
函数来查询最新收到的价格和成交量。
c
bool SymbolInfoTick(const string symbol, MqlTick &tick)
对于给定名称为 symbol
的交易品种,该函数会填充通过引用传递的 tick
结构。如果成功,它将返回 true
。
如你所知,如果指标和智能交易系统包含相应的处理函数 OnCalculate
和 OnTick
的描述,那么当新的报价点到达时,终端会自动调用它们。然而,关于价格变化的意义、最后一笔交易的成交量以及报价点生成时间的信息并不会直接传递给这些处理函数。通过 SymbolInfoTick
函数可以获取更详细的信息。
报价点事件仅针对图表交易品种生成,因此我们已经考虑了基于自定义事件获取我们自己的多交易品种报价点事件的选项。在这种情况下,SymbolInfoTick
使得在收到通知时能够读取关于第三方交易品种报价点的信息。
让我们以 EventTickSpy.mq5
指标为例,并将其转换为 SymbolTickSpy.mq5
,它将在每个“多货币”报价点时请求相应交易品种的 MqlTick
结构,然后计算并在图表上显示所有的点差。
我们添加一个新的输入参数 Index
。对于新的通知发送方式,这是必需的:我们将在用户事件中仅发送已更改交易品种的索引(见下文)。
c
#define TICKSPY 0xFEED // 65261
input string SymbolList =
"EURUSD,GBPUSD,XAUUSD,USDJPY,USDCHF"; // 交易品种列表,以逗号分隔(示例)
input ushort Message = TICKSPY; // 自定义消息 ID
input long Chart = 0; // 接收图表 ID(请勿编辑)
input int Index = 0; // 交易品种列表中的索引(请勿编辑)
此外,我们添加 Spreads
数组来存储各交易品种的点差,并添加 SelfIndex
变量来记住当前图表交易品种在列表中的位置(如果它包含在列表中,通常是这样的)。后者是从指标的原始副本中的 OnCalculate
调用我们新的报价点处理函数所必需的。显式获取 _Symbol
的现成索引,而不是在事件中把它发送回自身,这样做更简单且更正确。
c
int Spreads[];
int SelfIndex = -1;
引入的数据结构在 OnInit
中进行初始化。否则,OnInit
保持不变,包括在第三方交易品种上启动指标的从属实例(此处省略了这些代码行)。
c
void OnInit()
{
...
const int n = StringSplit(SymbolList, ',', Symbols);
ArrayResize(Spreads, n);
for(int i = 0; i < n; ++i)
{
if(Symbols[i] != _Symbol)
{
...
}
else
{
SelfIndex = i;
}
Spreads[i] = 0;
}
...
}
在 OnCalculate
处理函数中,如果指标副本在其他交易品种上运行(同时,应发送通知的 Chart
图表 ID 不等于 0),我们会在每个报价点生成一个自定义事件。请注意,在事件中填充的唯一参数是 lparam
,它等于 Index
(dparam
为 0,sparam
为 NULL
)。如果 Chart
等于 0,这意味着我们处于在图表交易品种 _Symbol
上运行的指标主副本中,并且如果在输入的交易品种列表中找到它,我们将使用相应的 SelfIndex
索引直接调用 OnSymbolTick
。
c
int OnCalculate(const int rates_total, const int prev_calculated, const int, const double &price[])
{
if(prev_calculated)
{
if(Chart > 0)
{
EventChartCustom(Chart, Message, Index, 0, NULL);
}
else if(SelfIndex > -1)
{
OnSymbolTick(SelfIndex);
}
}
return rates_total;
}
在 OnChartEvent
中的事件算法接收部分,我们也调用 OnSymbolTick
,但这次我们从 lparam
中获取列表中的交易品种编号(即从指标的另一个副本作为 Index
参数发送的内容)。
c
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
if(id == CHARTEVENT_CUSTOM + Message)
{
OnSymbolTick((int)lparam);
}
}
OnSymbolTick
函数使用 SymbolInfoTick
请求完整的报价点信息,并通过将卖价(Ask
)和买价(Bid
)之间的差值除以点值(SYMBOL_POINT
属性将在后面讨论)来计算点差。
c
void OnSymbolTick(const int index)
{
const string symbol = Symbols[index];
MqlTick tick;
if(SymbolInfoTick(symbol, tick))
{
Spreads[index] = (int)MathRound((tick.ask - tick.bid)
/ SymbolInfoDouble(symbol, SYMBOL_POINT));
string message = "";
for(int i = 0; i < ArraySize(Spreads); ++i)
{
message += Symbols[i] + "=" + (string)Spreads[i] + "\n";
}
Comment(message);
}
}
新的点差会更新 Spreads
数组中相应的单元格,然后整个数组会以注释的形式显示在图表上。如下所示。
请求的交易品种列表的当前点差
请求的交易品种列表的当前点差
你可以实时比较注释中的信息与市场报价窗口中的信息是否一致。
交易和报价时段安排
在后续的章节中,我们将讨论 MQL5 API 中能够实现交易操作自动化的函数。但首先,我们需要研究该平台的技术特点,这些特点决定了调用这些 API 能否成功。特别是,金融工具的相关规范会带来一些限制。在本章中,我们将逐步全面地探讨如何通过编程分析这些内容,首先从交易时段这个方面入手。
在进行金融工具交易时,需要考虑到许多国际市场,如证券交易所,都有预定的开盘时间,只有在这些时间段内才能够获取信息和进行交易。尽管终端会持续连接到经纪商的服务器,但在工作时间之外尝试进行交易将会失败。因此,对于每个交易品种,终端都会存储其交易时段安排,也就是一天中可以执行特定操作的时间段。
如你所知,主要有两种类型的时段:报价时段和交易时段。在报价时段,终端会接收(可能接收)当前的报价。在交易时段,则允许发送交易订单并进行交易。一天中,每种类型的时段可能会有多个,并且中间会有休息时间(例如,分为上午和晚上时段)。显然,报价时段的时长大于或等于交易时段的时长。
无论如何,时段时间,即开盘和收盘时间,终端会将其从交易所的本地时区转换为经纪商的时区(服务器时间)。
MQL5 API 允许使用 SymbolInfoSessionQuote
和 SymbolInfoSessionTrade
函数来查询每种金融工具的报价时段和交易时段。特别是,这些重要信息可以让程序在向服务器发送交易请求之前,检查市场当前是否开放。这样,我们可以避免不可避免的错误结果,并防止不必要的服务器负载。请记住,如果由于 MQL 程序实现不正确而向服务器发送大量错误请求,服务器可能会开始 “忽略” 你的终端,在一段时间内拒绝执行后续命令(即使是正确的命令)。
c
bool SymbolInfoSessionQuote(const string symbol, ENUM_DAY_OF_WEEK dayOfWeek, uint sessionIndex, datetime &from, datetime &to)
bool SymbolInfoSessionTrade(const string symbol, ENUM_DAY_OF_WEEK dayOfWeek, uint sessionIndex, datetime &from, datetime &to)
这两个函数的工作方式相同。对于给定的交易品种 symbol
和星期几 dayOfWeek
,它们会将时段 sessionIndex
的开盘时间和收盘时间填充到通过引用传递的 from
和 to
参数中。时段索引从 0 开始。ENUM_DAY_OF_WEEK
结构体在 “枚举” 部分中已有描述。
没有单独的函数用于查询时段的数量:相反,我们应该不断增加索引 sessionIndex
来调用 SymbolInfoSessionQuote
和 SymbolInfoSessionTrade
函数,直到函数返回错误标志(false
)。当具有指定编号的时段存在,并且输出参数 from
和 to
接收到正确的值时,函数将返回成功标志(true
)。
根据 MQL5 文档,在接收到的 datetime
类型的 from
和 to
值中,应该忽略日期,只考虑时间。这是因为这些信息是日内时段安排。然而,这条规则有一个重要的例外。
由于市场有可能是全天 24 小时开放的,例如外汇市场,或者是位于世界另一端的交易所,其白天的交易时间与经纪商 “时区” 中的日期变更时间重合,因此时段的结束时间可能等于或大于 24 小时。例如,如果外汇市场时段的开始时间是 00:00,那么结束时间就是 24:00。然而,从 datetime
类型的角度来看,24 小时实际上是第二天的 00 时 00 分。
对于那些其时段安排相对于经纪商的时区偏移了几个小时,以至于时段在一天开始而在另一天结束的交易所来说,情况会更加复杂。因此,to
变量不仅记录了时间,还记录了额外的一天,这是不能忽略的,因为否则日内时间 from
会大于日内时间 to
(例如,一个时段可能从今天的 21:00 持续到明天的 8:00,即 21 > 8)。在这种情况下,检查当前时间是否在时段内(“时间 x
大于开始时间且小于结束时间”)将会是不正确的(例如,对于 x = 23
,条件 x >= 21 && x < 8
不成立,尽管该时段实际上是活跃的)。
因此,我们得出结论,不能忽略 from/to
参数中的日期,在算法中应该考虑到这一点(见示例)。
为了演示这些函数的功能,让我们回到 “权限” 部分中介绍的脚本 EnvPermissions.mq5
的示例。其中一种类型的权限(或者如果你愿意,也可以说是限制)专门涉及交易的可用性。之前,该脚本考虑了终端设置(TERMINAL_TRADE_ALLOWED
)和特定 MQL 程序的设置(MQL_TRADE_ALLOWED
)。现在,我们可以在其中添加时段检查,以确定在给定时刻对于特定交易品种有效的交易权限。
新版本的脚本名为 SymbolPermissions.mq5
。它也不是最终版本:在后续的某一章节中,我们将研究由交易账户设置所带来的限制。
回想一下,该脚本实现了 Permissions
类,该类提供了对适用于 MQL 程序的所有类型权限/限制的集中描述。除其他功能外,该类还具有检查交易可用性的方法:isTradeEnabled
和 isTradeOnSymbolEnabled
。其中第一个方法与全局权限相关,并且几乎保持不变:
c
class Permissions
{
public:
static bool isTradeEnabled(const string symbol = NULL, const datetime now = 0)
{
return TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)
&& MQLInfoInteger(MQL_TRADE_ALLOWED)
&& isTradeOnSymbolEnabled(symbol == NULL ? _Symbol : symbol, now);
}
...
在检查了终端和 MQL 程序的属性后,脚本会进入 isTradeOnSymbolEnabled
方法,在该方法中分析交易品种的规范。之前,这个方法实际上是空的。
除了通过 symbol
参数传入的当前交易品种外,isTradeOnSymbolEnabled
函数还接收当前时间(now
)和所需的交易模式(mode
)。我们将在后续部分更详细地讨论后者(见 “交易权限”)。目前,我们只需注意到 SYMBOL_TRADE_MODE_FULL
的默认值提供了最大的自由度(允许所有交易操作)。
c
static bool isTradeOnSymbolEnabled(string symbol, const datetime now = 0,
const ENUM_SYMBOL_TRADE_MODE mode = SYMBOL_TRADE_MODE_FULL)
{
// 检查时段
bool found = now == 0;
if(!found)
{
const static ulong day = 60 * 60 * 24;
const ulong time = (ulong)now % day;
datetime from, to;
int i = 0;
ENUM_DAY_OF_WEEK d = TimeDayOfWeek(now);
while(!found && SymbolInfoSessionTrade(symbol, d, i++, from, to))
{
found = time >= (ulong)from && time < (ulong)to;
}
}
// 检查交易品种的交易模式
return found && (SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE) == mode);
}
如果未指定 now
时间(默认值为 0),我们认为不需要考虑时段。这意味着表示找到合适时段(即包含给定时间的时段)的 found
变量会立即设置为 true
。但是,如果指定了 now
参数,函数将进入交易时段分析模块。
为了从 datetime
类型的值中提取不考虑日期的时间,我们定义了 day
常量,其值等于一天中的秒数。像 now % day
这样的表达式将返回完整日期和时间除以一天的时长的余数,这将只得到时间(datetime
中的高位数字将为零)。
TimeDayOfWeek
函数会返回给定 datetime
值对应的星期几。它位于我们之前已经使用过的 MQL5Book/DateTime.mqh
头文件中(见 “日期和时间”)。
在后续的 while
循环中,我们不断递增时段索引 i
并调用 SymbolInfoSessionTrade
函数,直到找到合适的时段或函数返回 false
(没有更多的时段)。这样,程序可以获取按星期几划分的完整时段列表,类似于在终端的交易品种 “规格” 窗口中显示的内容。
显然,合适的时段是指其开始时间 from
和结束时间 to
之间包含指定时间值的时段。正是在这里,我们考虑到了与可能的全天候交易相关的问题:from
和 to
按原样与 time
进行比较,而不丢弃日期(from % day
或 to % day
)。
一旦 found
变为 true
,我们就退出循环。否则,当超过允许的时段数量时(函数 SymbolInfoSessionTrade
将返回 false
),循环将结束,并且永远找不到合适的时段。
如果根据时段安排,现在允许进行交易,我们还会额外检查交易品种的交易模式(SYMBOL_TRADE_MODE
)。例如,交易品种的交易可能被完全禁止(“仅供参考”),或者处于 “仅平仓” 模式。
上述代码与 SymbolPermissions.mq5
文件中的最终版本相比有一些简化。它还实现了一种机制,用于标记导致交易被禁止的限制来源。所有这些来源都汇总在 TRADE_RESTRICTIONS
枚举中。
c
enum TRADE_RESTRICTIONS
{
TERMINAL_RESTRICTION = 1,
PROGRAM_RESTRICTION = 2,
SYMBOL_RESTRICTION = 4,
SESSION_RESTRICTION = 8,
};
目前,限制可能来自 4 个方面:终端、程序、交易品种和时段安排。我们稍后会添加更多选项。
为了在 Permissions
类中记录发现限制的事实,我们有 lastFailReasonBitMask
变量,它允许使用辅助方法 pass
从枚举元素中收集位掩码(当检查的条件值为 false
时设置位,并且该位等于 false
)。
c
static uint lastFailReasonBitMask;
static bool pass(const bool value, const uint bitflag)
{
if(!value) lastFailReasonBitMask |= bitflag;
return value;
}
在适当的验证步骤中调用带有特定标志的 pass
方法。例如,完整的 isTradeEnabled
方法如下:
c
static bool isTradeEnabled(const string symbol = NULL, const datetime now = 0)
{
lastFailReasonBitMask = 0;
return pass(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED), TERMINAL_RESTRICTION)
&& pass(MQLInfoInteger(MQL_TRADE_ALLOWED), PROGRAM_RESTRICTION)
&& isTradeOnSymbolEnabled(symbol == NULL ? _Symbol : symbol, now);
}
因此,当 TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)
或 MQLInfoInteger(MQL_TRADE_ALLOWED)
的调用结果为 false
时,将分别设置 TERMINAL_RESTRICTION
或 PROGRAM_RESTRICTION
标志。
isTradeOnSymbolEnabled
方法在检测到问题时也会设置自己的标志,包括时段标志。
c
static bool isTradeOnSymbolEnabled(string symbol, const datetime now = 0,
const ENUM_SYMBOL_TRADE_MODE mode = SYMBOL_TRADE_MODE_FULL)
{
...
return pass(found, SESSION_RESTRICTION)
&& pass(SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE) == mode, SYMBOL_RESTRICTION);
}
结果,使用 Permissions::isTradeEnabled
查询的 MQL 程序在收到限制后,可以使用 getFailReasonBitMask
和 explainBitMask
方法来阐明其含义:第一个方法按原样返回设置的禁止标志的掩码,第二个方法形成限制的用户友好文本描述。
c
static uint getFailReasonBitMask()
{
return lastFailReasonBitMask;
}
static string explainBitMask()
{
string result = "";
for(int i = 0; i < 4; ++i)
{
if(((1 << i) & lastFailReasonBitMask) != 0)
{
result += EnumToString((TRADE_RESTRICTIONS)(1 << i));
}
}
return result;
}
在 OnStart
处理程序中使用上述 Permissions
类,会检查市场报价中所有交易品种(当前为 TimeCurrent
)的交易可用性。
c
void OnStart()
{
string disabled = "";
const int n = SymbolsTotal(true);
for(int i = 0; i < n; ++i)
{
const string s = SymbolName(i, true);
if(!Permissions::isTradeEnabled(s, TimeCurrent()))
{
disabled += s + "=" + Permissions::explainBitMask() +"\n";
}
}
if(disabled != "")
{
Print("Trade is disabled for the following symbols and origins:");
Print(disabled);
}
}
如果对于某个交易品种禁止交易,我们将在日志中看到相应的解释。
Trade is disabled for following symbols and origins:
USDRUB=SESSION_RESTRICTION
SP500m=SYMBOL_RESTRICTION
在这种情况下,“USDRUB” 的市场已关闭,并且 “SP500m” 交易品种的交易被禁止(更严格地说,它不符合 SYMBOL_TRADE_MODE_FULL
模式)。
假设在运行脚本时,终端全局启用了算法交易。否则,我们还会在日志中看到 TERMINAL_RESTRICTION
和 PROGRAM_RESTRICTION
禁止标志。
品种保证金比率
在 MQL5 API 提供的品种规格特征中(我们将在后续章节详细讨论),有几个特征与保证金要求相关,这些要求适用于开仓和持仓的情况。由于终端支持在不同市场和不同类型的交易工具上进行交易,这些要求可能会有很大差异。概括来说,这体现在针对不同品种和不同类型的交易操作设置了单独的保证金修正比率。对于用户而言,这些比率会显示在终端的“规格”窗口中。
正如我们接下来会看到的,如果应用了乘数,它会与品种属性中的保证金值相乘。可以通过编程方式使用 SymbolInfoMarginRate
函数来获取保证金比率。
c
bool SymbolInfoMarginRate(const string symbol, ENUM_ORDER_TYPE orderType, double &initial, double &maintenance)
对于指定的品种和订单类型(ENUM_ORDER_TYPE
),该函数会将初始保证金比率和维持保证金比率分别填充到通过引用传递的 initial
和 maintenance
参数中。得到的比率应与相应类型的保证金值相乘(如何获取保证金值将在关于保证金要求的章节中介绍),从而得到在下达 orderType
类型的订单时账户中会预留的金额。
若函数成功执行,将返回 true
。
下面以一个简单的脚本 SymbolMarginRate.mq5
为例,该脚本会根据 MarketWatchOnly
参数输出市场报价窗口中或所有可用品种的保证金比率。可以在 OrderType
参数中指定操作类型。
c
#include <MQL5Book/MqlError.mqh>
input bool MarketWatchOnly = true;
input ENUM_ORDER_TYPE OrderType = ORDER_TYPE_BUY;
void OnStart()
{
const int n = SymbolsTotal(MarketWatchOnly);
PrintFormat("Margin rates per symbol for %s:", EnumToString(OrderType));
for(int i = 0; i < n; ++i)
{
const string s = SymbolName(i, MarketWatchOnly);
double initial = 1.0, maintenance = 1.0;
if(!SymbolInfoMarginRate(s, OrderType, initial, maintenance))
{
PrintFormat("Error: %s(%d)", E2S(_LastError), _LastError);
}
PrintFormat("%4d %s = %f %f", i, s, initial, maintenance);
}
}
以下是日志输出:
plaintext
Margin rates per symbol for ORDER_TYPE_BUY:
0 EURUSD = 1.000000 0.000000
1 XAUUSD = 1.000000 0.000000
2 BTCUSD = 0.330000 0.330000
3 USDCHF = 1.000000 0.000000
4 USDJPY = 1.000000 0.000000
5 AUDUSD = 1.000000 0.000000
6 USDRUB = 1.000000 1.000000
你可以将得到的值与终端中的品种规格进行比较。
获取交易品种属性的函数概述
通过查询每个交易品种的属性,可以获取其完整的详细信息。为此,MQL5 API 提供了三个函数,即 SymbolInfoInteger
、SymbolInfoDouble
和 SymbolInfoString
,每个函数分别负责获取特定类型的属性。这些属性分别被描述为三个枚举 ENUM_SYMBOL_INFO_INTEGER
、ENUM_SYMBOL_INFO_DOUBLE
和 ENUM_SYMBOL_INFO_STRING
的成员。我们已经熟悉的图表和对象 API 中也采用了类似的技术。
交易品种的名称和请求属性的标识符会传递给上述任何一个函数。
每个函数都有两种形式:简略形式和完整形式。简略形式直接返回请求的属性,而完整形式则将属性值写入通过引用传递的 out
参数中。例如,对于与整数类型兼容的属性,函数的原型如下:
c
long SymbolInfoInteger(const string symbol, ENUM_SYMBOL_INFO_INTEGER property)
bool SymbolInfoInteger(const string symbol, ENUM_SYMBOL_INFO_INTEGER property, long &value)
第二种形式返回一个布尔值,表示操作成功(true
)或失败(false
)。函数可能返回 false
的最常见原因包括无效的交易品种名称(MARKET_UNKNOWN_SYMBOL
,4301)或请求属性的无效标识符(MARKET_WRONG_PROPERTY
,4303)。具体的错误细节会记录在 _LastError
中。
和之前一样,ENUM_SYMBOL_INFO_INTEGER
枚举中的属性具有各种与整数兼容的类型:bool
、int
、long
、color
、datetime
以及特殊枚举(所有这些都将在单独的章节中讨论)。
对于实数类型的属性,SymbolInfoDouble
函数定义了以下两种形式:
c
double SymbolInfoDouble(const string symbol, ENUM_SYMBOL_INFO_DOUBLE property)
bool SymbolInfoDouble(const string symbol, ENUM_SYMBOL_INFO_DOUBLE property, double &value)
最后,对于字符串类型的属性,类似的函数形式如下:
c
string SymbolInfoString(const string symbol, ENUM_SYMBOL_INFO_STRING property)
bool SymbolInfoString(const string symbol, ENUM_SYMBOL_INFO_STRING property, string &value)
在后续开发智能交易系统时经常会用到的各种类型的属性,在本章的以下各节描述中进行了逻辑分组。
基于上述函数,我们将创建一个通用类 SymbolMonitor
(文件 SymbolMonitor.mqh
)来获取任何交易品种的属性。它将基于针对三个枚举的一组重载的 get
方法。
c
class SymbolMonitor
{
public:
const string name;
SymbolMonitor(): name(_Symbol) { }
SymbolMonitor(const string s): name(s) { }
long get(const ENUM_SYMBOL_INFO_INTEGER property) const
{
return SymbolInfoInteger(name, property);
}
double get(const ENUM_SYMBOL_INFO_DOUBLE property) const
{
return SymbolInfoDouble(name, property);
}
string get(const ENUM_SYMBOL_INFO_STRING property) const
{
return SymbolInfoString(name, property);
}
...
另外三个类似的方法使得可以在第一个参数中省略枚举类型,并让编译器根据第二个虚拟参数(其类型始终与结果类型匹配)来选择必要的重载。我们将在未来的模板类中使用这一点。
c
long get(const int property, const long) const
{
return SymbolInfoInteger(name, (ENUM_SYMBOL_INFO_INTEGER)property);
}
double get(const int property, const double) const
{
return SymbolInfoDouble(name, (ENUM_SYMBOL_INFO_DOUBLE)property);
}
string get(const int property, const string) const
{
return SymbolInfoString(name, (ENUM_SYMBOL_INFO_STRING)property);
}
...
因此,通过创建一个具有所需交易品种名称的对象,就可以统一查询该交易品种的任何类型的属性。为了查询并记录同一类型的所有属性,我们可以实现类似以下的功能:
c
// 项目(草案)
template<typename E,typename R>
void list2log()
{
E e = (E)0;
int array[];
const int n = EnumToArray(e, array, 0, USHORT_MAX);
for(int i = 0; i < n; ++i)
{
e = (E)array[i];
R r = get(e);
PrintFormat("% 3d %s=%s", i, EnumToString(e), (string)r);
}
}
然而,由于在 long
类型的属性中实际上“隐藏”了其他类型的值,这些值应该以特定的方式显示(例如,对于枚举类型调用 EnumToString
,对于日期和时间类型调用 TimeToString
等),因此定义另外三个重载方法来返回属性的字符串表示形式是有意义的。我们将它们命名为 stringify
。然后,在上述 list2log
草案中,可以使用 stringify
代替将值强制转换为 (string)
,并且该方法本身将消除一个模板参数。
c
template<typename E>
void list2log()
{
E e = (E)0;
int array[];
const int n = EnumToArray(e, array, 0, USHORT_MAX);
for(int i = 0; i < n; ++i)
{
e = (E)array[i];
PrintFormat("% 3d %s=%s", i, EnumToString(e), stringify(e));
}
}
对于实数和字符串类型,stringify
的实现相当直接明了:
c
string stringify(const ENUM_SYMBOL_INFO_DOUBLE property, const string format = NULL) const
{
if(format == NULL) return (string)SymbolInfoDouble(name, property);
return StringFormat(format, SymbolInfoDouble(name, property));
}
string stringify(const ENUM_SYMBOL_INFO_STRING property) const
{
return SymbolInfoString(name, property);
}
但对于 ENUM_SYMBOL_INFO_INTEGER
类型,情况稍微复杂一些。当然,当属性为 long
或 int
类型时,将其强制转换为 (string)
就足够了。所有其他情况需要在 switch
操作符中单独分析和转换。
c
string stringify(const ENUM_SYMBOL_INFO_INTEGER property) const
{
const long v = SymbolInfoInteger(name, property);
switch(property)
{
...
}
return (string)v;
}
例如,如果一个属性是 bool
类型,用字符串 "true"
或 "false"
来表示它会很方便(这样它在视觉上就会与简单的数字 1
和 0
区分开来)。提前举例来说,在这些属性中有 SYMBOL_EXIST
,它等同于 SymbolExist
函数,即返回一个布尔值表示指定的交易品种是否存在。对于处理该属性以及其他逻辑属性,实现一个辅助方法 boolean
是有意义的。
c
static string boolean(const long v)
{
return v ? "true" : "false";
}
string stringify(const ENUM_SYMBOL_INFO_INTEGER property) const
{
const long v = SymbolInfoInteger(name, property);
switch(property)
{
case SYMBOL_EXIST:
return boolean(v);
...
}
return (string)v;
}
对于枚举类型的属性,最合适的解决方案是使用 EnumToString
函数的模板方法。
c
template<typename E>
static string enumstr(const long v)
{
return EnumToString((E)v);
}
例如,SYMBOL_SWAP_ROLLOVER3DAYS
属性确定对于某个交易品种,在一周中的哪一天对未平仓头寸收取三倍的掉期费用,并且该属性的类型为 ENUM_DAY_OF_WEEK
。因此,为了处理它,我们可以在 switch
中编写如下代码:
c
case SYMBOL_SWAP_ROLLOVER3DAYS:
return enumstr<ENUM_DAY_OF_WEEK>(v);
一个特殊情况是属性值为位标志组合的属性。特别是,对于每个交易品种,经纪商会设置特定类型订单的权限,如市价单、限价单、止损单、止盈单等(我们将单独讨论这些权限)。每种订单类型都由一个只有一位被设置为 1
的常量表示,所以它们的叠加(通过按位或运算符 |
组合)存储在 SYMBOL_ORDER_MODE
属性中,并且在没有限制的情况下,所有位同时被设置为 1
。对于这样的属性,我们将在头文件中定义自己的枚举,例如:
c
enum SYMBOL_ORDER
{
_SYMBOL_ORDER_MARKET = 1,
_SYMBOL_ORDER_LIMIT = 2,
_SYMBOL_ORDER_STOP = 4,
_SYMBOL_ORDER_STOP_LIMIT = 8,
_SYMBOL_ORDER_SL = 16,
_SYMBOL_ORDER_TP = 32,
_SYMBOL_ORDER_CLOSEBY = 64,
};
这里,对于每个内置常量,如 SYMBOL_ORDER_MARKET
,都声明了一个相应的元素,其标识符与常量相同,但前面有一个下划线以避免命名冲突。
为了将这种枚举中的标志组合以字符串形式表示,我们实现了另一个模板方法 maskstr
:
c
template<typename E>
static string maskstr(const long v)
{
string text = "";
for(int i = 0; ; ++i)
{
ResetLastError();
const string s = EnumToString((E)(1 << i));
if(_LastError != 0)
{
break;
}
if((v & (1 << i)) != 0)
{
text += s + " ";
}
}
return text;
}
它的功能类似于 enumstr
,但 EnumToString
函数会针对属性值中每个被设置为 1
的位进行调用,然后将得到的字符串“拼接”起来。
现在可以以类似的方式在 switch
语句中处理 SYMBOL_ORDER_MODE
:
c
case SYMBOL_ORDER_MODE:
return maskstr<SYMBOL_ORDER>(v);
以下是 ENUM_SYMBOL_INFO_INTEGER
类型的 stringify
方法的完整代码。我们将在后续章节中逐步熟悉所有的属性和枚举。
c
string stringify(const ENUM_SYMBOL_INFO_INTEGER property) const
{
const long v = SymbolInfoInteger(name, property);
switch(property)
{
case SYMBOL_SELECT:
case SYMBOL_SPREAD_FLOAT:
case SYMBOL_VISIBLE:
case SYMBOL_CUSTOM:
case SYMBOL_MARGIN_HEDGED_USE_LEG:
case SYMBOL_EXIST:
return boolean(v);
case SYMBOL_TIME:
return TimeToString(v, TIME_DATE|TIME_SECONDS);
case SYMBOL_TRADE_CALC_MODE:
return enumstr<ENUM_SYMBOL_CALC_MODE>(v);
case SYMBOL_TRADE_MODE:
return enumstr<ENUM_SYMBOL_TRADE_MODE>(v);
case SYMBOL_TRADE_EXEMODE:
return enumstr<ENUM_SYMBOL_TRADE_EXECUTION>(v);
case SYMBOL_SWAP_MODE:
return enumstr<ENUM_SYMBOL_SWAP_MODE>(v);
case SYMBOL_SWAP_ROLLOVER3DAYS:
return enumstr<ENUM_DAY_OF_WEEK>(v);
case SYMBOL_EXPIRATION_MODE:
return maskstr<SYMBOL_EXPIRATION>(v);
case SYMBOL_FILLING_MODE:
return maskstr<SYMBOL_FILLING>(v);
case SYMBOL_START_TIME:
case SYMBOL_EXPIRATION_TIME:
return TimeToString(v);
case SYMBOL_ORDER_MODE:
return maskstr<SYMBOL_ORDER>(v);
case SYMBOL_OPTION_RIGHT:
return enumstr<ENUM_SYMBOL_OPTION_RIGHT>(v);
case SYMBOL_OPTION_MODE:
return enumstr<ENUM_SYMBOL_OPTION_MODE>(v);
case SYMBOL_CHART_MODE:
return enumstr<ENUM_SYMBOL_CHART_MODE>(v);
case SYMBOL_ORDER_GTC_MODE:
return enumstr<ENUM_SYMBOL_ORDER_GTC_MODE>(v);
case SYMBOL_SECTOR:
return enumstr<ENUM_SYMBOL_SECTOR>(v);
case SYMBOL_INDUSTRY:
return enumstr<ENUM_SYMBOL_INDUSTRY>(v);
case SYMBOL_BACKGROUND_COLOR: // Bytes: Transparency Blue Green Red
return StringFormat("TBGR(0x%08X)", v);
}
return (string)v;
}
为了测试 SymbolMonitor
类,我们创建了一个简单的脚本 SymbolMonitor.mq5
。它会记录工作图表上交易品种的所有属性。
c
#include <MQL5Book/SymbolMonitor.mqh>
void OnStart()
{
SymbolMonitor m;
m.list2log<ENUM_SYMBOL_INFO_INTEGER>();
m.list2log<ENUM_SYMBOL_INFO_DOUBLE>();
m.list2log<ENUM_SYMBOL_INFO_STRING>();
}
例如,如果我们在 EURUSD 图表上运行该脚本,可能会得到以下记录(以缩短形式给出):
ENUM_SYMBOL_INFO_INTEGER Count=36
0 SYMBOL_SELECT=true
...
4 SYMBOL_TIME=2022.01.12 10:52:22
5 SYMBOL_DIGITS=5
6 SYMBOL_SPREAD=0
7 SYMBOL_TICKS_BOOKDEPTH=10
8 SYMBOL_TRADE_CALC_MODE=SYMBOL_CALC_MODE_FOREX
9 SYMBOL_TRADE_MODE=SYMBOL_TRADE_MODE_FULL
10 SYMBOL_TRADE_STOPS_LEVEL=0
11 SYMBOL_TRADE_FREEZE_LEVEL=0
12 SYMBOL_TRADE_EXEMODE=SYMBOL_TRADE_EXECUTION_INSTANT
13 SYMBOL_SWAP_MODE=SYMBOL_SWAP_MODE_POINTS
14 SYMBOL_SWAP_ROLLOVER3DAYS=WEDNESDAY
15 SYMBOL_SPREAD_FLOAT=true
16 SYMBOL_EXPIRATION_MODE=_SYMBOL_EXPIRATION_GTC _SYMBOL_EXPIRATION_DAY »
_SYMBOL_EXPIRATION_SPECIFIED _SYMBOL_EXPIRATION_SPECIFIED_DAY
17 SYMBOL_FILLING_MODE=_SYMBOL_FILLING_FOK
...
23 SYMBOL_ORDER_MODE=_SYMBOL_ORDER_MARKET _SYMBOL_ORDER_LIMIT _SYMBOL_ORDER_STOP »
_SYMBOL_ORDER_STOP_LIMIT _SYMBOL_ORDER_SL _SYMBOL_ORDER_TP _SYMBOL_ORDER_CLOSEBY
...
26 SYMBOL_VISIBLE=true
27 SYMBOL_CUSTOM=false
28 SYMBOL_BACKGROUND_COLOR=TBGR(0xFF000000)
29 SYMBOL_CHART_MODE=SYMBOL_CHART_MODE_BID
30 SYMBOL_ORDER_GTC_MODE=SYMBOL_ORDERS_GTC
31 SYMBOL_MARGIN_HEDGED_USE_LEG=false
32 SYMBOL_EXIST=true
33 SYMBOL_TIME_MSC=1641984742149
34 SYMBOL_SECTOR=SECTOR_CURRENCY
35 SYMBOL_INDUSTRY=INDUSTRY_UNDEFINED
ENUM_SYMBOL_INFO_DOUBLE Count=57
0 SYMBOL_BID=1.13681
1 SYMBOL_BIDHIGH=1.13781
2 SYMBOL_BIDLOW=1.13552
3 SYMBOL_ASK=1.13681
4 SYMBOL_ASKHIGH=1.13781
5 SYMBOL_ASKLOW=1.13552
...
12 SYMBOL_POINT=1e-05
13 SYMBOL_TRADE_TICK_VALUE=1.0
14 SYMBOL_TRADE_TICK_SIZE=1e-05
15 SYMBOL_TRADE_CONTRACT_SIZE=100000.0
16 SYMBOL_VOLUME_MIN=0.01
17 SYMBOL_VOLUME_MAX=500.0
18 SYMBOL_VOLUME_STEP=0.01
19 SYMBOL_SWAP_LONG=-0.7
20 SYMBOL_SWAP_SHORT=-1.0
21 SYMBOL_MARGIN_INITIAL=0.0
22 SYMBOL_MARGIN_MAINTENANCE=0.0
...
28 SYMBOL_TRADE_TICK_VALUE_PROFIT=1.0
29 SYMBOL_TRADE_TICK_VALUE_LOSS=1.0
...
43 SYMBOL_MARGIN_HEDGED
47 SYMBOL_PRICE_CHANGE=0.0132
ENUM_SYMBOL_INFO_STRING Count=15
0 SYMBOL_BANK=
1 SYMBOL_DESCRIPTION=Euro vs US Dollar
2 SYMBOL_PATH=Forex\EURUSD
3 SYMBOL_CURRENCY_BASE=EUR
4 SYMBOL_CURRENCY_PROFIT=USD
5 SYMBOL_CURRENCY_MARGIN=EUR
...
13 SYMBOL_SECTOR_NAME=Currency
特别是,你可以看到该交易品种的价格显示有5位小数(SYMBOL_DIGITS),该交易品种确实存在(SYMBOL_EXIST),合约规模为100000.0(SYMBOL_TRADE_CONTRACT_SIZE)等等。所有信息都与规格相符。
以下是转换为 Markdown 格式的内容:
### 获取交易品种属性的函数概述
通过查询每个交易品种的属性,可以获取其完整的详细信息。为此,MQL5 API 提供了三个函数,即 `SymbolInfoInteger`、`SymbolInfoDouble` 和 `SymbolInfoString`,每个函数分别负责获取特定类型的属性。这些属性分别被描述为三个枚举 `ENUM_SYMBOL_INFO_INTEGER`、`ENUM_SYMBOL_INFO_DOUBLE` 和 `ENUM_SYMBOL_INFO_STRING` 的成员。我们已经熟悉的图表和对象 API 中也采用了类似的技术。
交易品种的名称和请求属性的标识符会传递给上述任何一个函数。
每个函数都有两种形式:简略形式和完整形式。简略形式直接返回请求的属性,而完整形式则将属性值写入通过引用传递的 `out` 参数中。例如,对于与整数类型兼容的属性,函数的原型如下:
```c
long SymbolInfoInteger(const string symbol, ENUM_SYMBOL_INFO_INTEGER property)
bool SymbolInfoInteger(const string symbol, ENUM_SYMBOL_INFO_INTEGER property, long &value)
第二种形式返回一个布尔值,表示操作成功(true
)或失败(false
)。函数可能返回 false
的最常见原因包括无效的交易品种名称(MARKET_UNKNOWN_SYMBOL
,4301)或请求属性的无效标识符(MARKET_WRONG_PROPERTY
,4303)。具体的错误细节会记录在 _LastError
中。
和之前一样,ENUM_SYMBOL_INFO_INTEGER
枚举中的属性具有各种与整数兼容的类型:bool
、int
、long
、color
、datetime
以及特殊枚举(所有这些都将在单独的章节中讨论)。
对于实数类型的属性,SymbolInfoDouble
函数定义了以下两种形式:
c
double SymbolInfoDouble(const string symbol, ENUM_SYMBOL_INFO_DOUBLE property)
bool SymbolInfoDouble(const string symbol, ENUM_SYMBOL_INFO_DOUBLE property, double &value)
最后,对于字符串类型的属性,类似的函数形式如下:
c
string SymbolInfoString(const string symbol, ENUM_SYMBOL_INFO_STRING property)
bool SymbolInfoString(const string symbol, ENUM_SYMBOL_INFO_STRING property, string &value)
在后续开发智能交易系统时经常会用到的各种类型的属性,在本章的以下各节描述中进行了逻辑分组。
基于上述函数,我们将创建一个通用类 SymbolMonitor
(文件 SymbolMonitor.mqh
)来获取任何交易品种的属性。它将基于针对三个枚举的一组重载的 get
方法。
c
class SymbolMonitor
{
public:
const string name;
SymbolMonitor(): name(_Symbol) { }
SymbolMonitor(const string s): name(s) { }
long get(const ENUM_SYMBOL_INFO_INTEGER property) const
{
return SymbolInfoInteger(name, property);
}
double get(const ENUM_SYMBOL_INFO_DOUBLE property) const
{
return SymbolInfoDouble(name, property);
}
string get(const ENUM_SYMBOL_INFO_STRING property) const
{
return SymbolInfoString(name, property);
}
...
另外三个类似的方法使得可以在第一个参数中省略枚举类型,并让编译器根据第二个虚拟参数(其类型始终与结果类型匹配)来选择必要的重载。我们将在未来的模板类中使用这一点。
c
long get(const int property, const long) const
{
return SymbolInfoInteger(name, (ENUM_SYMBOL_INFO_INTEGER)property);
}
double get(const int property, const double) const
{
return SymbolInfoDouble(name, (ENUM_SYMBOL_INFO_DOUBLE)property);
}
string get(const int property, const string) const
{
return SymbolInfoString(name, (ENUM_SYMBOL_INFO_STRING)property);
}
...
因此,通过创建一个具有所需交易品种名称的对象,就可以统一查询该交易品种的任何类型的属性。为了查询并记录同一类型的所有属性,我们可以实现类似以下的功能:
c
// 项目(草案)
template<typename E,typename R>
void list2log()
{
E e = (E)0;
int array[];
const int n = EnumToArray(e, array, 0, USHORT_MAX);
for(int i = 0; i < n; ++i)
{
e = (E)array[i];
R r = get(e);
PrintFormat("% 3d %s=%s", i, EnumToString(e), (string)r);
}
}
然而,由于在 long
类型的属性中实际上“隐藏”了其他类型的值,这些值应该以特定的方式显示(例如,对于枚举类型调用 EnumToString
,对于日期和时间类型调用 TimeToString
等),因此定义另外三个重载方法来返回属性的字符串表示形式是有意义的。我们将它们命名为 stringify
。然后,在上述 list2log
草案中,可以使用 stringify
代替将值强制转换为 (string)
,并且该方法本身将消除一个模板参数。
c
template<typename E>
void list2log()
{
E e = (E)0;
int array[];
const int n = EnumToArray(e, array, 0, USHORT_MAX);
for(int i = 0; i < n; ++i)
{
e = (E)array[i];
PrintFormat("% 3d %s=%s", i, EnumToString(e), stringify(e));
}
}
对于实数和字符串类型,stringify
的实现相当直接明了:
c
string stringify(const ENUM_SYMBOL_INFO_DOUBLE property, const string format = NULL) const
{
if(format == NULL) return (string)SymbolInfoDouble(name, property);
return StringFormat(format, SymbolInfoDouble(name, property));
}
string stringify(const ENUM_SYMBOL_INFO_STRING property) const
{
return SymbolInfoString(name, property);
}
但对于 ENUM_SYMBOL_INFO_INTEGER
类型,情况稍微复杂一些。当然,当属性为 long
或 int
类型时,将其强制转换为 (string)
就足够了。所有其他情况需要在 switch
操作符中单独分析和转换。
c
string stringify(const ENUM_SYMBOL_INFO_INTEGER property) const
{
const long v = SymbolInfoInteger(name, property);
switch(property)
{
...
}
return (string)v;
}
例如,如果一个属性是 bool
类型,用字符串 "true"
或 "false"
来表示它会很方便(这样它在视觉上就会与简单的数字 1
和 0
区分开来)。提前举例来说,在这些属性中有 SYMBOL_EXIST
,它等同于 SymbolExist
函数,即返回一个布尔值表示指定的交易品种是否存在。对于处理该属性以及其他逻辑属性,实现一个辅助方法 boolean
是有意义的。
c
static string boolean(const long v)
{
return v ? "true" : "false";
}
string stringify(const ENUM_SYMBOL_INFO_INTEGER property) const
{
const long v = SymbolInfoInteger(name, property);
switch(property)
{
case SYMBOL_EXIST:
return boolean(v);
...
}
return (string)v;
}
对于枚举类型的属性,最合适的解决方案是使用 EnumToString
函数的模板方法。
c
template<typename E>
static string enumstr(const long v)
{
return EnumToString((E)v);
}
例如,SYMBOL_SWAP_ROLLOVER3DAYS
属性确定对于某个交易品种,在一周中的哪一天对未平仓头寸收取三倍的掉期费用,并且该属性的类型为 ENUM_DAY_OF_WEEK
。因此,为了处理它,我们可以在 switch
中编写如下代码:
c
case SYMBOL_SWAP_ROLLOVER3DAYS:
return enumstr<ENUM_DAY_OF_WEEK>(v);
一个特殊情况是属性值为位标志组合的属性。特别是,对于每个交易品种,经纪商会设置特定类型订单的权限,如市价单、限价单、止损单、止盈单等(我们将单独讨论这些权限)。每种订单类型都由一个只有一位被设置为 1
的常量表示,所以它们的叠加(通过按位或运算符 |
组合)存储在 SYMBOL_ORDER_MODE
属性中,并且在没有限制的情况下,所有位同时被设置为 1
。对于这样的属性,我们将在头文件中定义自己的枚举,例如:
c
enum SYMBOL_ORDER
{
_SYMBOL_ORDER_MARKET = 1,
_SYMBOL_ORDER_LIMIT = 2,
_SYMBOL_ORDER_STOP = 4,
_SYMBOL_ORDER_STOP_LIMIT = 8,
_SYMBOL_ORDER_SL = 16,
_SYMBOL_ORDER_TP = 32,
_SYMBOL_ORDER_CLOSEBY = 64,
};
这里,对于每个内置常量,如 SYMBOL_ORDER_MARKET
,都声明了一个相应的元素,其标识符与常量相同,但前面有一个下划线以避免命名冲突。
为了将这种枚举中的标志组合以字符串形式表示,我们实现了另一个模板方法 maskstr
:
c
template<typename E>
static string maskstr(const long v)
{
string text = "";
for(int i = 0; ; ++i)
{
ResetLastError();
const string s = EnumToString((E)(1 << i));
if(_LastError != 0)
{
break;
}
if((v & (1 << i)) != 0)
{
text += s + " ";
}
}
return text;
}
它的功能类似于 enumstr
,但 EnumToString
函数会针对属性值中每个被设置为 1
的位进行调用,然后将得到的字符串“拼接”起来。
现在可以以类似的方式在 switch
语句中处理 SYMBOL_ORDER_MODE
:
c
case SYMBOL_ORDER_MODE:
return maskstr<SYMBOL_ORDER>(v);
以下是 ENUM_SYMBOL_INFO_INTEGER
类型的 stringify
方法的完整代码。我们将在后续章节中逐步熟悉所有的属性和枚举。
c
string stringify(const ENUM_SYMBOL_INFO_INTEGER property) const
{
const long v = SymbolInfoInteger(name, property);
switch(property)
{
case SYMBOL_SELECT:
case SYMBOL_SPREAD_FLOAT:
case SYMBOL_VISIBLE:
case SYMBOL_CUSTOM:
case SYMBOL_MARGIN_HEDGED_USE_LEG:
case SYMBOL_EXIST:
return boolean(v);
case SYMBOL_TIME:
return TimeToString(v, TIME_DATE|TIME_SECONDS);
case SYMBOL_TRADE_CALC_MODE:
return enumstr<ENUM_SYMBOL_CALC_MODE>(v);
case SYMBOL_TRADE_MODE:
return enumstr<ENUM_SYMBOL_TRADE_MODE>(v);
case SYMBOL_TRADE_EXEMODE:
return enumstr<ENUM_SYMBOL_TRADE_EXECUTION>(v);
case SYMBOL_SWAP_MODE:
return enumstr<ENUM_SYMBOL_SWAP_MODE>(v);
case SYMBOL_SWAP_ROLLOVER3DAYS:
return enumstr<ENUM_DAY_OF_WEEK>(v);
case SYMBOL_EXPIRATION_MODE:
return maskstr<SYMBOL_EXPIRATION>(v);
case SYMBOL_FILLING_MODE:
return maskstr<SYMBOL_FILLING>(v);
case SYMBOL_START_TIME:
case SYMBOL_EXPIRATION_TIME:
return TimeToString(v);
case SYMBOL_ORDER_MODE:
return maskstr<SYMBOL_ORDER>(v);
case SYMBOL_OPTION_RIGHT:
return enumstr<ENUM_SYMBOL_OPTION_RIGHT>(v);
case SYMBOL_OPTION_MODE:
return enumstr<ENUM_SYMBOL_OPTION_MODE>(v);
case SYMBOL_CHART_MODE:
return enumstr<ENUM_SYMBOL_CHART_MODE>(v);
case SYMBOL_ORDER_GTC_MODE:
return enumstr<ENUM_SYMBOL_ORDER_GTC_MODE>(v);
case SYMBOL_SECTOR:
return enumstr<ENUM_SYMBOL_SECTOR>(v);
case SYMBOL_INDUSTRY:
return enumstr<ENUM_SYMBOL_INDUSTRY>(v);
case SYMBOL_BACKGROUND_COLOR: // Bytes: Transparency Blue Green Red
return StringFormat("TBGR(0x%08X)", v);
}
return (string)v;
}
为了测试 SymbolMonitor
类,我们创建了一个简单的脚本 SymbolMonitor.mq5
。它会记录工作图表上交易品种的所有属性。
c
#include <MQL5Book/SymbolMonitor.mqh>
void OnStart()
{
SymbolMonitor m;
m.list2log<ENUM_SYMBOL_INFO_INTEGER>();
m.list2log<ENUM_SYMBOL_INFO_DOUBLE>();
m.list2log<ENUM_SYMBOL_INFO_STRING>();
}
例如,如果我们在 EURUSD 图表上运行该脚本,可能会得到以下记录(以缩短形式给出):
ENUM_SYMBOL_INFO_INTEGER Count=36
0 SYMBOL_SELECT=true
...
4 SYMBOL_TIME=2022.01.12 10:52:22
5 SYMBOL_DIGITS=5
6 SYMBOL_SPREAD=0
7 SYMBOL_TICKS_BOOKDEPTH=10
8 SYMBOL_TRADE_CALC_MODE=SYMBOL_CALC_MODE_FOREX
9 SYMBOL_TRADE_MODE=SYMBOL_TRADE_MODE_FULL
10 SYMBOL_TRADE_STOPS_LEVEL=0
11 SYMBOL_TRADE_FREEZE_LEVEL=0
12 SYMBOL_TRADE_EXEMODE=SYMBOL_TRADE_EXECUTION_INSTANT
13 SYMBOL_SWAP_MODE=SYMBOL_SWAP_MODE_POINTS
14 SYMBOL_SWAP_ROLLOVER3DAYS=WEDNESDAY
15 SYMBOL_SPREAD_FLOAT=true
16 SYMBOL_EXPIRATION_MODE=_SYMBOL_EXPIRATION_GTC _SYMBOL_EXPIRATION_DAY »
_SYMBOL_EXPIRATION_SPECIFIED _SYMBOL_EXPIRATION_SPECIFIED_DAY
17 SYMBOL_FILLING_MODE=_SYMBOL_FILLING_FOK
...
23 SYMBOL_ORDER_MODE=_SYMBOL_ORDER_MARKET _SYMBOL_ORDER_LIMIT _SYMBOL_ORDER_STOP »
_SYMBOL_ORDER_STOP_LIMIT _SYMBOL_ORDER_SL _SYMBOL_ORDER_TP _SYMBOL_ORDER_CLOSEBY
...
26 SYMBOL_VISIBLE=true
27 SYMBOL_CUSTOM=false
28 SYMBOL_BACKGROUND_COLOR=TBGR(0xFF000000)
29 SYMBOL_CHART_MODE=SYMBOL_CHART_MODE_BID
30 SYMBOL_ORDER_GTC_MODE=SYMBOL_ORDERS_GTC
31 SYMBOL_MARGIN_HEDGED_USE_LEG=false
32 SYMBOL_EXIST=true
33 SYMBOL_TIME_MSC=1641984742149
34 SYMBOL_SECTOR=SECTOR_CURRENCY
35 SYMBOL_INDUSTRY=INDUSTRY_UNDEFINED
ENUM_SYMBOL_INFO_DOUBLE Count=57
0 SYMBOL_BID=1.13681
1 SYMBOL_BIDHIGH=1.13781
2 SYMBOL_BIDLOW=1.13552
3 SYMBOL_ASK=1.13681
4 SYMBOL_ASKHIGH=1.13781
5 SYMBOL_ASKLOW=1.13552
...
12 SYMBOL_POINT=1e-05
13 SYMBOL_TRADE_TICK_VALUE=1.0
特别是,你可以看到该交易品种的价格播报有 5 位小数(SYMBOL_DIGITS
),该交易品种确实存在(SYMBOL_EXIST
),合约规模为 100000.0(SYMBOL_TRADE_CONTRACT_SIZE
)等等。所有信息都与规格相符。
检查交易品种状态
之前我们研究了几个与交易品种状态相关的函数。回想一下,SymbolExist
用于检查某个交易品种是否存在,而 SymbolSelect
用于检查该交易品种是否被列入或排除在市场报价列表(Market Watch)之外。在交易品种的属性中,有几个用途类似的标志位,与上述函数相比,使用这些标志位既有优点也有缺点。
特别是,SYMBOL_SELECT
属性可让您了解指定的交易品种是否在市场报价列表中被选中,而 SymbolSelect
函数则可以更改此属性。
与类似的 SYMBOL_EXIST
属性不同,SymbolExist
函数还会在输出变量中填充一个指示信息,表明该交易品种是用户自定义的。在查询属性时,有必要分别分析这两个属性,因为自定义交易品种的属性存储在另一个属性 SYMBOL_CUSTOM
中。然而,在某些情况下,程序可能只需要其中一个属性,那么能够单独进行查询就成为了一个优点。
所有标志位都是通过 SymbolInfoInteger
函数获取的布尔值。
标识符 | 描述 |
---|---|
SYMBOL_EXIST | 指示具有给定名称的交易品种是否存在 |
SYMBOL_SELECT | 指示该交易品种是否在市场报价列表中被选中 |
SYMBOL_VISIBLE | 指示指定的交易品种是否在市场报价列表中显示 |
SYMBOL_VISIBLE
特别值得关注。事实上,一些交易品种(通常是用于计算保证金要求和以存款货币计算利润的交叉汇率)会自动在市场报价列表中被选中,但不会显示在用户可见的列表中。此类交易品种必须被明确选择(由用户手动或通过编程方式)才能显示出来。因此,正是 SYMBOL_VISIBLE
属性可以让您确定某个交易品种在窗口中是否可见:对于使用一对函数 SymbolsTotal
和 SymbolName
(其中 selected
参数等于 true
)获取的列表中的某些元素,该属性的值可能为 false
。
考虑一个简单的脚本(SymbolInvisible.mq5
),它在终端中搜索隐式选中的交易品种,即那些在市场报价列表中不显示(SYMBOL_VISIBLE
被重置为 false
)但 SYMBOL_SELECT
为 true
的交易品种。
c
#define PUSH(A,V) (A[ArrayResize(A, ArraySize(A) + 1) - 1] = V)
void OnStart()
{
const int n = SymbolsTotal(false);
int selected = 0;
string invisible[];
// 遍历所有可用的交易品种
for(int i = 0; i < n; ++i)
{
const string s = SymbolName(i, false);
if(SymbolInfoInteger(s, SYMBOL_SELECT))
{
selected++;
if(!SymbolInfoInteger(s, SYMBOL_VISIBLE))
{
// 将已选中但不可见的交易品种收集到数组中
PUSH(invisible, s);
}
}
}
PrintFormat("Symbols: total=%d, selected=%d, implicit=%d",
n, selected, ArraySize(invisible));
if(ArraySize(invisible))
{
ArrayPrint(invisible);
}
}
尝试在不同的账户上编译并运行此脚本。并非总是会遇到交易品种被隐式选中的情况。例如,如果在市场报价列表中选中了以卢布报价的俄罗斯蓝筹股的行情代码,而交易账户使用的是另一种货币(例如美元或欧元,而不是卢布),那么 USDRUB
交易品种将被自动选中。当然,这是假设它之前没有被明确添加到市场报价列表中。然后我们会在日志中得到以下结果:
Symbols: total=50681, selected=49, implicit=1
"USDRUB"
构建交易品种图表的价格类型
MetaTrader 5 价格图表上的柱线可以基于买价(Bid)或最新价(Last)来绘制,并且每个金融工具的规格中会标明绘制类型。MQL 程序可以通过调用 SymbolInfoInteger
函数来查询 SYMBOL_CHART_MODE
属性,从而获取这一特征。返回值是 ENUM_SYMBOL_CHART_MODE
枚举的一个成员。
标识符 | 描述 |
---|---|
SYMBOL_CHART_MODE_BID | 柱线基于买价绘制 |
SYMBOL_CHART_MODE_LAST | 柱线基于最新价绘制 |
最新价模式用于在交易所交易的交易品种(与分散的外汇市场不同),并且对于此类交易品种可以获取市场深度。市场深度可以根据 SYMBOL_TICKS_BOOKDEPTH
属性来查找。
SYMBOL_CHART_MODE
属性对于调整指标或策略的信号很有用。例如,有些指标或策略是基于图表的最新价构建的,而订单将 “按市场价格” 执行,也就是根据交易方向以卖价(Ask)或买价(Bid)执行。
此外,在计算自定义工具的柱线时也需要价格类型:如果自定义工具依赖于标准交易品种,那么根据价格类型考虑这些标准交易品种的设置可能是有意义的。当用户在 “自定义交易品种” 窗口(通过在 “交易品种” 对话框中选择 “创建交易品种” 打开)中输入合成工具的公式时,可以根据所使用的相应标准交易品种的规格选择价格类型。然而,当在 MQL 程序中形成计算算法时,程序本身需要负责正确选择价格类型。
首先,让我们收集关于在特定账户上使用买价和最新价来构建图表的统计信息。这正是脚本 SymbolStatsByPriceType.mq5
要做的事情。
c
const bool MarketWatchOnly = false;
void OnStart()
{
const int n = SymbolsTotal(MarketWatchOnly);
int k = 0;
// 遍历所有可用的交易品种
for(int i = 0; i < n; ++i)
{
if(SymbolInfoInteger(SymbolName(i, MarketWatchOnly), SYMBOL_CHART_MODE)
== SYMBOL_CHART_MODE_LAST)
{
k++;
}
}
PrintFormat("Symbols in total: %d", n);
PrintFormat("Symbols using price types: Bid=%d, Last=%d", n - k, k);
}
在不同的账户上尝试运行该脚本(有些账户可能没有股票交易品种)。结果可能如下所示:
Symbols in total: 52304
Symbols using price types: Bid=229, Last=52075
一个更实际的例子是指标 SymbolBidAskChart.mq5
,它旨在绘制基于指定价格类型形成的柱线图表。这将使你能够将使用 SYMBOL_CHART_MODE
属性中的价格构建的图表中的烛台图与基于替代价格类型的柱线进行比较。例如,你可以在基于最新价的交易品种图表上查看基于买价的柱线,或者获取基于卖价的柱线,而标准终端图表不支持基于卖价绘制柱线。
我们将以 “等待数据和管理可见性” 部分中提供的现成指标 IndDeltaVolume.mq5
作为新指标的基础。在那个指标中,我们下载了一定数量(BarCount
)的柱线的报价历史记录,并计算了成交量的差值,即分别计算了买入和卖出的成交量。在新指标中,我们只需要将计算算法替换为基于每个柱线内的报价来搜索开盘价(Open)、最高价(High)、最低价(Low)和收盘价(Close)。
指标设置包括四个缓冲区和一个显示在主窗口中的柱线图(DRAW_BARS
)。
c
#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots 1
#property indicator_type1 DRAW_BARS
#property indicator_color1 clrDodgerBlue
#property indicator_width1 2
#property indicator_label1 "Open;High;Low;Close;"
选择以柱线形式显示是为了在主图表的烛台图上运行时更易于读取,这样每个柱线的两种版本都可以看到。
新的 ChartMode
输入参数允许用户从三种价格类型中选择一种(请注意,Ask
是我们在 ENUM_SYMBOL_CHART_MODE
的标准元素集基础上添加的)。
c
enum ENUM_SYMBOL_CHART_MODE_EXTENDED
{
_SYMBOL_CHART_MODE_BID, // SYMBOL_CHART_MODE_BID
_SYMBOL_CHART_MODE_LAST, // SYMBOL_CHART_MODE_LAST
_SYMBOL_CHART_MODE_ASK, // SYMBOL_CHART_MODE_ASK*
};
input int BarCount = 100;
input COPY_TICKS TickType = INFO_TICKS;
input ENUM_SYMBOL_CHART_MODE_EXTENDED ChartMode = _SYMBOL_CHART_MODE_BID;
以前的 CalcDeltaVolume
类更名为 CalcCustomBars
,但几乎没有变化。不同之处包括一组新的四个缓冲区以及 chartMode
字段,该字段在构造函数中根据输入变量 ChartMode
进行初始化。
c
class CalcCustomBars
{
const int limit;
const COPY_TICKS tickType;
const ENUM_SYMBOL_CHART_MODE_EXTENDED chartMode;
double open[];
double high[];
double low[];
double close[];
...
public:
CalcCustomBars(
const int bars,
const COPY_TICKS type,
const ENUM_SYMBOL_CHART_MODE_EXTENDED mode)
: limit(bars), tickType(type), chartMode(mode) ...
{
// 将数组注册为指标缓冲区
SetIndexBuffer(0, open);
SetIndexBuffer(1, high);
SetIndexBuffer(2, low);
SetIndexBuffer(3, close);
const static string defTitle[] = {"Open;High;Low;Close;"};
const static string types[] = {"Bid", "Last", "Ask"};
string name = defTitle[0];
StringReplace(name, ";", types[chartMode] + ";");
PlotIndexSetString(0, PLOT_LABEL, name);
IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
}
...
根据 chartMode
的模式,辅助方法 price
从每个报价中返回特定的价格类型。
c
protected:
double price(const MqlTick &t) const
{
switch(chartMode)
{
case _SYMBOL_CHART_MODE_BID:
return t.bid;
case _SYMBOL_CHART_MODE_LAST:
return t.last;
case _SYMBOL_CHART_MODE_ASK:
return t.ask;
}
return 0; // 错误
}
...
使用 price
方法,我们可以轻松实现对主要计算方法 calc
的修改,该方法根据某一柱线的报价数组为编号为 i
的柱线填充缓冲区。
c
void calc(const int i, const MqlTick &ticks[], const int skip = 0)
{
const int n = ArraySize(ticks);
for(int j = skip; j < n; ++j)
{
const double p = price(ticks[j]);
if(open[i] == EMPTY_VALUE)
{
open[i] = p;
}
if(p > high[i] || high[i] == EMPTY_VALUE)
{
high[i] = p;
}
if(p < low[i])
{
low[i] = p;
}
close[i] = p;
}
}
源代码的其余部分及其工作原理与 IndDeltaVolume.mq5
的描述一致。
在 OnInit
处理程序中,我们额外显示图表的当前价格类型,如果用户决定为不存在最新价的交易品种基于最新价类型构建指标,则返回警告。
c
int OnInit()
{
...
ENUM_SYMBOL_CHART_MODE mode =
(ENUM_SYMBOL_CHART_MODE)SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE);
Print("Chart mode: ", EnumToString(mode));
if(mode == SYMBOL_CHART_MODE_BID
&& ChartMode == _SYMBOL_CHART_MODE_LAST)
{
Alert("Last price is not available for ", _Symbol);
}
return INIT_SUCCEEDED;
}
以下是一张基于最新价绘制图表模式的交易品种的截图;一个基于买价类型的指标覆盖在该图表上。
基于最新价的图表上显示基于买价的柱线的指标
基于最新价的图表上显示基于买价的柱线的指标
在常规的基于买价的图表上查看基于卖价的柱线也很有趣。
基于买价的图表上显示基于卖价的柱线的指标
基于买价的图表上显示基于卖价的柱线的指标
在流动性较低的时段,当点差扩大时,你可以看到买价和卖价图表之间的显著差异。
交易工具的基础货币、报价货币和保证金货币
每个金融交易工具最重要的属性之一是其交易中涉及的货币:
- 对于外汇交易工具,表示所买卖资产的基础货币。
- 用于计算利润的(报价)货币。
- 用于计算保证金的货币。
MQL 程序可以使用 SymbolInfoString
函数以及下表中的三个属性来获取这些货币的名称。
标识符 | 描述 |
---|---|
SYMBOL_CURRENCY_BASE | 基础货币 |
SYMBOL_CURRENCY_PROFIT | 利润货币 |
SYMBOL_CURRENCY_MARGIN | 保证金货币 |
这些属性有助于分析外汇交易工具(许多经纪商在其名称中添加了各种前缀和后缀)以及交易所交易工具。特别是,算法将能够找到一个品种,以获取两种给定货币的交叉汇率,或者选择具有给定通用报价货币的指数投资组合。
由于根据特定要求搜索交易工具是一项非常常见的任务,我们创建一个 SymbolFilter
类(SymbolFilter.mqh
)来构建符合条件的品种列表及其选定属性。将来,我们不仅会使用这个类来分析货币,还会用于分析其他特征。
首先,我们将考虑一个简化版本,然后为其补充一些实用的功能。
在开发过程中,我们将使用现成的辅助工具:一个关联映射数组(MapArray.mqh
)用于存储选定类型的键值对,以及一个品种属性监视器(SymbolMonitor.mqh
)。
c
#include <MQL5Book/MapArray.mqh>
#include <MQL5Book/SymbolMonitor.mqh>
为了简化在数组中积累工作结果的语句,我们使用一个改进版本的 PUSH
宏(我们在之前的示例中已经见过),以及用于多维数组的 EXPAND
版本(在这种情况下简单赋值是不可能的)。
c
#define PUSH(A,V) (A[ArrayResize(A, ArraySize(A) + 1, ArraySize(A) * 2) - 1] = V)
#define EXPAND(A) (ArrayResize(A, ArrayRange(A, 0) + 1, ArrayRange(A, 0) * 2) - 1)
SymbolFilter
类的对象必须有一个用于存储属性值的存储区,这些属性值将用于筛选品种。因此,我们在类中描述三个 MapArray
数组,分别用于整数、实数和字符串属性。
c
class SymbolFilter
{
MapArray<ENUM_SYMBOL_INFO_INTEGER,long> longs;
MapArray<ENUM_SYMBOL_INFO_DOUBLE,double> doubles;
MapArray<ENUM_SYMBOL_INFO_STRING,string> strings;
...
设置所需的筛选属性是通过重载 let
方法来完成的。
c
public:
SymbolFilter *let(const ENUM_SYMBOL_INFO_INTEGER property, const long value)
{
longs.put(property, value);
return &this;
}
SymbolFilter *let(const ENUM_SYMBOL_INFO_DOUBLE property, const double value)
{
doubles.put(property, value);
return &this;
}
SymbolFilter *let(const ENUM_SYMBOL_INFO_STRING property, const string value)
{
strings.put(property, value);
return &this;
}
...
请注意,这些方法返回一个指向筛选器的指针,这允许你以链式的方式编写条件:例如,如果在代码前面已经定义了一个 SymbolFilter
类型的对象 f
,那么你可以对价格类型和利润货币名称施加两个条件,如下所示:
c
f.let(SYMBOL_CHART_MODE, SYMBOL_CHART_MODE_LAST).let(SYMBOL_CURRENCY_PROFIT, "USD");
筛选器对象通过 select
方法的几种变体来形成满足条件的品种数组,其中最简单的一个如下所示(其他变体将在后面讨论)。
watch
参数定义了品种的搜索上下文:在市场报价窗口中选定的品种中搜索(true
),或者在所有可用品种中搜索(false
)。输出数组 symbols
将被填充上匹配的品种名称。我们已经知道该方法内部的代码结构:它有一个遍历品种的循环,对于每个品种,都会创建一个监视器对象 m
。
c
void select(const bool watch, string &symbols[]) const
{
const int n = SymbolsTotal(watch);
for(int i = 0; i < n; ++i)
{
const string s = SymbolName(i, watch);
SymbolMonitor m(s);
if(match<ENUM_SYMBOL_INFO_INTEGER,long>(m, longs)
&& match<ENUM_SYMBOL_INFO_DOUBLE,double>(m, doubles)
&& match<ENUM_SYMBOL_INFO_STRING,string>(m, strings))
{
PUSH(symbols, s);
}
}
}
正是借助监视器,我们可以以统一的方式获取任何属性的值。检查当前品种的属性是否与存储在 longs
、doubles
和 strings
数组中的条件集匹配是由辅助方法 match
实现的。只有当所有请求的属性都匹配时,品种名称才会被保存到 symbols
输出数组中。
在最简单的情况下,match
方法的实现如下(随后会对其进行修改)。
c
protected:
template<typename K,typename V>
bool match(const SymbolMonitor &m, const MapArray<K,V> &data) const
{
for(int i = 0; i < 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
类型的属性,其实现并不像想象的那么简单。
c
template<typename V>
static bool equal(const V v1, const V v2)
{
return v1 == v2 || eps(v1, v2);
}
对于 double
类型,表达式 v1 == v2
对于相近的数字可能不起作用,因此应该考虑实数 DBL_EPSILON
类型的精度。这是在一个单独的方法 eps
中完成的,由于模板的原因,该方法分别针对 double
类型和所有其他类型进行了重载。
c
static bool eps(const double v1, const double v2)
{
return fabs(v1 - v2) < DBL_EPSILON * fmax(v1, v2);
}
template<typename V>
static bool eps(const V v1, const V v2)
{
return false;
}
当任何除 double
类型之外的值相等时,模板方法 eps
根本不会被调用,而在所有其他情况下(包括值不相等时),它会根据需要返回 false
(因此,只有条件 v1 == v2
起作用)。
上面描述的筛选器选项仅允许检查属性是否相等。然而,在实际应用中,经常需要分析不相等的条件以及大于/小于的条件。因此,SymbolFilter
类有一个 IS
枚举,其中包含基本的比较操作(如果需要,可以进行补充)。
c
class SymbolFilter
{
...
enum IS
{
EQUAL,
GREATER,
NOT_EQUAL,
LESS
};
...
对于 ENUM_SYMBOL_INFO_INTEGER
、ENUM_SYMBOL_INFO_DOUBLE
和 ENUM_SYMBOL_INFO_STRING
枚举中的每个属性,不仅需要保存所需的属性值(回想一下关联数组 longs
、doubles
、strings
),还需要保存来自新 IS
枚举的比较方法。
由于标准枚举的元素具有不重叠的值(有一个与交易量相关的例外情况,但这并不关键),为比较方法保留一个通用的映射数组 conditions
是有意义的。这就引出了一个问题,即选择哪种类型作为映射键,以便在技术上“组合”不同的枚举。为此,我们不得不描述一个虚拟枚举 ENUM_ANY
,它仅表示某种类型的通用枚举。回想一下,所有枚举都有一个等效于整数 int
的内部表示,因此可以相互转换。
c
enum ENUM_ANY
{
};
MapArray<ENUM_ANY,IS> conditions;
MapArray<ENUM_ANY,long> longs;
MapArray<ENUM_ANY,double> doubles;
MapArray<ENUM_ANY,string> strings;
...
现在,我们可以完善所有设置属性所需值的 let
方法,添加指定比较方法的 cmp
输入参数。默认情况下,它设置为检查相等性(EQUAL
)。
c
SymbolFilter *let(const ENUM_SYMBOL_INFO_INTEGER property, const long value,
const IS cmp = EQUAL)
{
longs.put((ENUM_ANY)property, value);
conditions.put((ENUM_ANY)property, cmp);
return &this;
}
这是针对整数属性的一个变体。另外两个重载方法也以相同的方式进行更改。
考虑到关于不同比较方式的新信息,同时消除映射数组中不同类型的键,我们修改 match
方法。在该方法中,对于每个指定的属性,我们根据 data
映射数组中的键从 conditions
数组中检索一个条件,并使用 switch
操作符进行适当的检查。
c
template<typename V>
bool match(const SymbolMonitor &m, const MapArray<ENUM_ANY,V> &data) const
{
// 虚拟变量,用于选择下面的 m.get 方法重载
static const V type = (V)NULL;
// 遍历对品种属性施加的条件
for(int i = 0; i < data.getSize(); ++i)
{
const ENUM_ANY key = data.getKey(i);
// 选择条件中的比较方法
switch(conditions[key])
{
case EQUAL:
if(!equal(m.get(key, type), data.getValue(i))) return false;
break;
case NOT_EQUAL:
if(equal(m.get(key, type), data.getValue(i))) return false;
break;
case GREATER:
if(!greater(m.get(key, type), data.getValue(i))) return false;
break;
case LESS:
if(greater(m.get(key, type), data.getValue(i))) return false;
break;
}
}
return true;
}
新的模板 greater
方法实现得比较简单。
c
template<typename V>
static bool greater(const V v1, const V v2)
{
return v1 > v2;
}
现在,match
方法的调用可以以更短的形式编写,因为模板 V
仅剩下的一种类型会由传递的 data
参数自动确定(并且这是 longs
、doubles
或 strings
数组之一)。
c
void select(const bool watch, string &symbols[]) const
{
const int n = SymbolsTotal(watch);
for(int i = 0; i < n; ++i)
{
const string s = SymbolName(i, watch);
SymbolMonitor m(s);
if(match(m, longs)
&& match(m, doubles)
&& match(m, strings))
{
PUSH(symbols, s);
}
}
}
这还不是 SymbolFilter
类的最终版本,但我们已经可以对其进行实际测试了。
让我们创建一个脚本 SymbolFilterCurrency.mq5
,它可以根据基础货币和利润货币的属性筛选品种;在这种情况下,货币是美元(USD)。MarketWatchOnly
参数默认仅在市场报价窗口中进行搜索。
c
#include <MQL5Book/SymbolFilter.mqh>
input bool MarketWatchOnly = true;
void OnStart()
{
SymbolFilter f; // 筛选器对象
string symbols[]; // 结果数组
...
假设我们想要找到具有直接报价的外汇交易工具,也就是说,“USD”出现在它们的名称开头。为了不依赖于特定经纪商形成名称的特殊性,我们将使用 SYMBOL_CURRENCY_BASE
属性,它包含了第一种货币。
让我们写下品种的基础货币等于“USD”的条件,并应用筛选器。
c
f.let(SYMBOL_CURRENCY_BASE, "USD")
.select(MarketWatchOnly, symbols);
Print("===== Base is USD =====");
ArrayPrint(symbols);
...
得到的数组将输出到日志中。
plaintext
===== Base is USD =====
"USDCHF" "USDJPY" "USDCNH" "USDRUB" "USDCAD" "USDSEK" "SP500m" "Brent"
如你所见,该数组不仅包括报价代码开头是“USD”的外汇品种,还包括标准普尔 500 指数(S&P500)和商品(石油)。最后两个品种以美元报价,但它们也有相同的基础货币。同时,外汇品种的报价货币(也是利润货币)是第二个,并且与美元不同。这使得我们可以补充筛选器,使非外汇品种不再匹配。
让我们清空数组,添加利润货币不等于“USD”的条件,然后再次请求合适的品种(之前的条件已保存在 f
对象中)。
c
...
ArrayResize(symbols, 0);
f.let(SYMBOL_CURRENCY_PROFIT, "USD", SymbolFilter::IS::NOT_EQUAL)
.select(MarketWatchOnly, symbols);
Print("===== Base is USD and Profit is not USD =====");
ArrayPrint(symbols);
}
这次,日志中实际显示的只有你正在寻找的品种。
plaintext
===== Base is USD and Profit is not USD =====
"USDCHF" "USDJPY" "USDCNH" "USDRUB" "USDCAD" "USDSEK"
价格表示精度和变化步长
之前,我们已经了解了图表工作交易品种的两个相互关联的属性:最小价格变化步长(Point)以及以小数位数表示的价格表示精度(Digits)。它们也存在于预定义变量中。要获取任意交易品种的类似属性,则应分别查询 SYMBOL_POINT
和 SYMBOL_DIGITS
属性。SYMBOL_POINT
属性与最小价格变化(在 MQL 程序中称为 SYMBOL_TRADE_TICK_SIZE
属性)及其值(SYMBOL_TRADE_TICK_VALUE
,通常以交易账户的货币表示,但有些交易品种可配置为使用基础货币;如有必要,您可联系您的经纪商了解详细信息)密切相关。下表列出了这一组相关属性。
标识符 | 描述 |
---|---|
SYMBOL_DIGITS | 小数位数 |
SYMBOL_POINT | 报价货币中一个点的值 |
SYMBOL_TRADE_TICK_VALUE | SYMBOL_TRADE_TICK_VALUE_PROFIT 的值 |
SYMBOL_TRADE_TICK_VALUE_PROFIT | 盈利头寸的当前报价点值 |
SYMBOL_TRADE_TICK_VALUE_LOSS | 亏损头寸的当前报价点值 |
SYMBOL_TRADE_TICK_SIZE | 报价货币中的最小价格变化 |
除 SYMBOL_DIGITS
外的所有属性均为实数,需使用 SymbolInfoDouble
函数进行查询。SYMBOL_DIGITS
属性可通过 SymbolInfoInteger
函数获取。为了测试这些属性的使用情况,我们将使用现成的 SymbolFilter
和 SymbolMonitor
类,它们会针对任何属性自动调用所需的函数。
我们还将对 SymbolFilter
类进行改进,添加 select
方法的一个新重载,该重载不仅能够用合适交易品种的名称填充一个数组,还能用这些交易品种特定属性的值填充另一个数组。
在更一般的情况下,我们可能会同时对每个交易品种的多个属性感兴趣,因此建议对于输出数组,不要使用内置数据类型之一,而是使用具有不同字段的特殊复合类型。
在编程中,这种类型被称为元组,在某种程度上等同于 MQL5 中的结构体。
c
template<typename T1,typename T2,typename T3> // 我们最多可以描述 64 个字段
struct Tuple3 // MQL5 允许 64 个模板参数
{
T1 _1;
T2 _2;
T3 _3;
};
然而,结构体需要预先描述所有字段,而我们事先并不知道所请求的交易品种属性的数量和列表。因此,为了简化代码,我们将把元组表示为动态数组第二维中的向量,该向量用于接收查询结果。
c
T array[][S];
作为数据类型 T
,我们可以使用用于属性的任何内置类型和枚举。大小 S
必须与请求的属性数量相匹配。
说实话,这样的简化限制了我们在一次查询中只能获取相同类型的值,即只能是整数、实数或字符串。不过,过滤条件可以包含任何属性。我们稍后将以其他交易实体(订单、交易和头寸)的过滤器为例,实现使用元组的方法。
因此,SymbolFilter::select
方法的新版本接受一个对属性数组的引用作为输入,该属性数组包含要从过滤后的交易品种中读取的属性标识符。交易品种名称本身以及这些属性的值将被写入 symbols
和 data
输出数组中。
c
template<typename E,typename V>
bool select(const bool watch, const E &property[], string &symbols[],
V &data[][], const bool sort = false) const
{
// 请求的属性数组大小必须与输出元组匹配
const int q = ArrayRange(data, 1);
if(ArraySize(property) != q) return false;
const int n = SymbolsTotal(watch);
// 遍历交易品种
for(int i = 0; i < n; ++i)
{
const string s = SymbolName(i, watch);
// 通过监视器访问交易品种属性
SymbolMonitor m(s);
// 检查所有过滤条件
if(match(m, longs)
&& match(m, doubles)
&& match(m, strings))
{
// 将合适交易品种的属性写入数组
const int k = EXPAND(data);
for(int j = 0; j < q; ++j)
{
data[k][j] = m.get(property[j]);
}
PUSH(symbols, s);
}
}
if(sort)
{
...
}
return true;
}
此外,新方法可以按第一维(第一个请求的属性)对输出数组进行排序:此功能留给读者通过源代码自行研究。要启用排序,将 sort
参数设置为 true
。交易品种名称数组和数据数组将一致排序。
当只需要从过滤后的交易品种中请求一个属性时,为了在调用代码中避免使用元组,SymbolFilter
中实现了以下 select
选项:在该选项内部,我们定义了属性的中间数组(properties
)和值的中间数组(tuples
),它们在第二维的大小为 1,用于调用上述完整版本的 select
方法。
c
template<typename E,typename V>
bool select(const bool watch, const E property, string &symbols[], V &data[],
const bool sort = false) const
{
E properties[1] = {property};
V tuples[][1];
const bool result = select(watch, properties, symbols, tuples, sort);
ArrayCopy(data, tuples);
return result;
}
使用改进后的过滤器,让我们尝试构建一个按报价点值 SYMBOL_TRADE_TICK_VALUE
排序的交易品种列表(请参阅文件 SymbolFilterTickValue.mq5
)。假设存款货币是美元,对于以美元报价的外汇交易品种(XXXUSD
类型),我们应该得到等于 1.0 的值。对于其他资产,我们将看到非零的值。
c
#include <MQL5Book/SymbolFilter.mqh>
input bool MarketWatchOnly = true;
void OnStart()
{
SymbolFilter f; // 过滤器对象
string symbols[]; // 存储交易品种名称的数组
double tickValues[]; // 存储结果的数组
// 应用无过滤条件的过滤器,填充并排序数组
f.select(MarketWatchOnly, SYMBOL_TRADE_TICK_VALUE, symbols, tickValues, true);
PrintFormat("===== Tick values of the symbols (%d) =====",
ArraySize(tickValues));
ArrayPrint(symbols);
ArrayPrint(tickValues, 5);
}
以下是运行该脚本的结果。
===== Tick values of the symbols (13) =====
"BTCUSD" "USDRUB" "XAUUSD" "USDSEK" "USDCNH" "USDCAD" "USDJPY" "NZDUSD" "AUDUSD" "EURUSD" "GBPUSD" "USDCHF" "SP500m"
0.00100 0.01309 0.10000 0.10955 0.15744 0.80163 0.87319 1.00000 1.00000 1.00000 1.00000 1.09212 10.00000
交易操作允许的交易量
在后续章节中,当我们学习如何编写智能交易系统(Expert Advisors)时,需要控制许多决定交易订单发送成功与否的交易品种特征。特别是,这涉及到交易品种规格中规定允许操作范围的部分。MQL5 中也可以获取这些相应的属性,它们均为 double
类型,可通过 SymbolInfoDouble
函数来查询。
标识符 | 描述 |
---|---|
SYMBOL_VOLUME_MIN | 交易的最小手数 |
SYMBOL_VOLUME_MAX | 交易的最大手数 |
SYMBOL_VOLUME_STEP | 交易手数变化的最小步长 |
SYMBOL_VOLUME_LIMIT | 同一方向(买入或卖出)上开仓头寸和挂单的最大允许总手数 |
SYMBOL_TRADE_CONTRACT_SIZE | 交易合约规模,即 1 手的大小 |
若尝试以小于最小手数、大于最大手数,或者不是步长整数倍的手数买卖金融工具,将会导致错误。在与交易 API 相关的章节中,我们会实现一段代码,在调用 MQL5 API 交易函数之前统一进行必要的检查并规范交易量。
此外,MQL 程序还应检查 SYMBOL_VOLUME_LIMIT
。例如,若限制为 5 手,你可以持有一个 5 手的开仓买单,同时可以下达一个 5 手的卖出限价挂单。然而,你不能下达买入限价挂单(因为同一方向的累计手数会超过限制),也不能设置超过 5 手的卖出限价单。
作为一个入门示例,我们来看脚本 SymbolFilterVolumes.mq5
,它会记录所选交易品种上述属性的值。我们在输入参数中添加 MinimalContractSize
变量,以便能够根据 SYMBOL_TRADE_CONTRACT_SIZE
属性过滤交易品种:只显示合约规模大于指定值(默认值为 0,即所有交易品种都满足条件)的交易品种。
c
#include <MQL5Book/SymbolFilter.mqh>
input bool MarketWatchOnly = true;
input double MinimalContractSize = 0;
在 OnStart
函数的开头,我们定义一个过滤对象和输出数组,以便将属性名称列表和四个字段的值以 double
向量的形式获取。四个所需属性的列表在 volumeIds
数组中指定。
c
void OnStart()
{
SymbolFilter f; // 过滤对象
string symbols[]; // 接收名称的数组
double volumeLimits[][4]; // 接收数据向量的数组
// 请求的交易品种属性
ENUM_SYMBOL_INFO_DOUBLE volumeIds[] =
{
SYMBOL_VOLUME_MIN,
SYMBOL_VOLUME_STEP,
SYMBOL_VOLUME_MAX,
SYMBOL_VOLUME_LIMIT
};
...
接下来,我们根据合约规模进行过滤(应大于指定值),并获取与交易量相关的匹配交易品种的规格字段。
c
f.let(SYMBOL_TRADE_CONTRACT_SIZE, MinimalContractSize, SymbolFilter::IS::GREATER)
.select(MarketWatchOnly, volumeIds, symbols, volumeLimits);
const int n = ArraySize(volumeLimits);
PrintFormat("===== Volume limits of the symbols (%d) =====", n);
string title = "";
for(int i = 0; i < ArraySize(volumeIds); ++i)
{
title += "\t" + EnumToString(volumeIds[i]);
}
Print(title);
for(int i = 0; i < n; ++i)
{
Print(symbols[i]);
ArrayPrint(volumeLimits, 3, NULL, i, 1, 0);
}
}
对于默认设置,该脚本可能会显示如下结果(有缩写):
===== Volume limits of the symbols (13) =====
SYMBOL_VOLUME_MIN SYMBOL_VOLUME_STEP SYMBOL_VOLUME_MAX SYMBOL_VOLUME_LIMIT
EURUSD
0.010 0.010 500.000 0.000
GBPUSD
0.010 0.010 500.000 0.000
USDCHF
0.010 0.010 500.000 0.000
USDJPY
0.010 0.010 500.000 0.000
USDCNH
0.010 0.010 1000.000 0.000
USDRUB
0.010 0.010 1000.000 0.000
...
XAUUSD
0.010 0.010 100.000 0.000
BTCUSD
0.010 0.010 1000.000 0.000
SP500m
0.100 0.100 5.000 15.000
某些交易品种可能不受 SYMBOL_VOLUME_LIMIT
限制(值为 0)。你可以将这些结果与交易品种规格进行比较,它们必须一致。
交易权限
作为上一节开始的有关正确准备交易订单主题的延续,我们来关注下面这组在智能交易系统(EA)开发中起着非常重要作用的属性。
标识符 | 描述 |
---|---|
SYMBOL_TRADE_MODE | 该品种在不同交易模式下的权限(请参阅 ENUM_SYMBOL_TRADE_MODE ) |
SYMBOL_ORDER_MODE | 允许的订单类型标志,位掩码(详见下文) |
这两个属性均为整数类型,可通过 SymbolInfoInteger
函数获取。
我们已经在脚本 SymbolPermissions.mq5
中使用过 SYMBOL_TRADE_MODE
属性。它的值是 ENUM_SYMBOL_TRADE_MODE
枚举中的元素之一。
标识符 | 值 | 描述 |
---|---|---|
SYMBOL_TRADE_MODE_DISABLED | 0 | 该品种的交易被禁用 |
SYMBOL_TRADE_MODE_LONGONLY | 1 | 仅允许买入交易 |
SYMBOL_TRADE_MODE_SHORTONLY | 2 | 仅允许卖出交易 |
SYMBOL_TRADE_MODE_CLOSEONLY | 3 | 仅允许平仓操作 |
SYMBOL_TRADE_MODE_FULL | 4 | 对交易操作无限制 |
回顾一下,Permissions
类包含 isTradeOnSymbolEnabled
方法,该方法会检查影响品种交易可用性的几个方面,其中之一就是 SYMBOL_TRADE_MODE
属性。默认情况下,我们认为我们感兴趣的是对交易的完全访问权限,即买卖操作:SYMBOL_TRADE_MODE_FULL
。根据交易策略的不同,MQL 程序可能认为仅允许买入、仅允许卖出或仅允许平仓操作的权限就已足够。
c
static bool isTradeOnSymbolEnabled(string symbol, const datetime now = 0,
const ENUM_SYMBOL_TRADE_MODE mode = SYMBOL_TRADE_MODE_FULL)
{
// 检查交易时段
bool found = now == 0;
...
// 检查该品种的交易模式
return found && (SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE) == mode);
}
除了交易模式之外,将来我们还需要分析不同类型订单的权限:它们由 SYMBOL_ORDER_MODE
属性中的单独位来表示,并且可以通过逻辑或(|
)任意组合。例如,值 127(0x7F
)对应于所有位都被设置,即所有类型的订单都可用。
标识符 | 值 | 描述 |
---|---|---|
SYMBOL_ORDER_MARKET | 1 | 允许市价订单(买入和卖出) |
SYMBOL_ORDER_LIMIT | 2 | 允许限价订单(买入限价和卖出限价) |
SYMBOL_ORDER_STOP | 4 | 允许止损订单(买入止损和卖出止损) |
SYMBOL_ORDER_STOP_LIMIT | 8 | 允许止损限价订单(买入止损限价和卖出止损限价) |
SYMBOL_ORDER_SL | 16 | 允许设置止损(Stop Loss)水平 |
SYMBOL_ORDER_TP | 32 | 允许设置止盈(Take Profit)水平 |
SYMBOL_ORDER_CLOSEBY | 64 | 允许通过同一品种的反向订单平仓,即平仓(Close By)操作 |
SYMBOL_ORDER_CLOSEBY
属性仅针对采用对冲账户(ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
,请参阅账户类型)设置。
在测试脚本 SymbolFilterTradeMode.mq5
中,我们将请求市场报价窗口中可见品种的上述几个属性。将位及其组合作为数字输出并不是很直观,所以我们将利用 SymbolMonitor
类中一个方便的方法 stringify
来打印枚举成员和所有属性的位掩码。
c
void OnStart()
{
SymbolFilter f; // 筛选器对象
string symbols[]; // 用于存储品种名称的数组
long permissions[][2]; // 用于存储数据(属性值)的数组
// 请求的品种属性列表
ENUM_SYMBOL_INFO_INTEGER modes[] =
{
SYMBOL_TRADE_MODE,
SYMBOL_ORDER_MODE
};
// 应用筛选器,获取包含结果的数组
f.let(SYMBOL_VISIBLE, true).select(true, modes, symbols, permissions);
const int n = ArraySize(symbols);
PrintFormat("===== Trade permissions for the symbols (%d) =====", n);
for(int i = 0; i < n; ++i)
{
Print(symbols[i] + ":");
for(int j = 0; j < ArraySize(modes); ++j)
{
// 按原样显示位和数字描述
PrintFormat(" %s (%d)",
SymbolMonitor::stringify(permissions[i][j], modes[j]),
permissions[i][j]);
}
}
}
以下是运行该脚本后生成的部分日志内容。
plaintext
===== Trade permissions for the symbols (13) =====
EURUSD:
SYMBOL_TRADE_MODE_FULL (4)
[ _SYMBOL_ORDER_MARKET _SYMBOL_ORDER_LIMIT _SYMBOL_ORDER_STOP
_SYMBOL_ORDER_STOP_LIMIT _SYMBOL_ORDER_SL _SYMBOL_ORDER_TP
_SYMBOL_ORDER_CLOSEBY ] (127)
GBPUSD:
SYMBOL_TRADE_MODE_FULL (4)
[ _SYMBOL_ORDER_MARKET _SYMBOL_ORDER_LIMIT _SYMBOL_ORDER_STOP
_SYMBOL_ORDER_STOP_LIMIT _SYMBOL_ORDER_SL _SYMBOL_ORDER_TP
_SYMBOL_ORDER_CLOSEBY ] (127)
...
SP500m:
SYMBOL_TRADE_MODE_DISABLED (0)
[ _SYMBOL_ORDER_MARKET _SYMBOL_ORDER_LIMIT _SYMBOL_ORDER_STOP
_SYMBOL_ORDER_STOP_LIMIT _SYMBOL_ORDER_SL _SYMBOL_ORDER_TP ] (63)
请注意,最后一个品种 SP500m 的交易完全被禁用(其报价仅作为“参考性”报价提供)。同时,它的订单类型标志集不为 0,但这没有实际意义。
根据市场情况,经纪商可以自行决定更改品种的属性,例如,在一段时间内仅保留平仓的机会,因此一个正确的交易机器人必须在每次操作前检查这些属性。
交易品种的交易条件和订单执行模式
在本节中,我们将更深入地探讨依赖于金融工具设置的交易自动化方面。目前,我们将仅研究相关属性,其实际应用将在后续章节中介绍。假定读者已经熟悉诸如市价单、挂单、交易和头寸等基本术语。
在发送交易请求以执行时,应该考虑到在金融市场中,无法保证在特定时刻,该金融工具在期望价格下有全部请求的交易量可供交易。因此,实时交易由价格和交易量执行模式进行规范。这些模式,换句话说就是执行策略,定义了在价格发生变化或当前时刻无法完全执行请求交易量的情况下的规则。
在 MQL5 API 中,对于每个交易品种,这些模式可作为以下属性获取,通过 SymbolInfoInteger
函数来获取这些属性。
标识符 | 描述 |
---|---|
SYMBOL_TRADE_EXEMODE | 与价格相关的交易执行模式 |
SYMBOL_FILLING_MODE | 与交易量相关的允许订单成交模式标志(位掩码,详见下文) |
SYMBOL_TRADE_EXEMODE
属性的值是 ENUM_SYMBOL_TRADE_EXECUTION
枚举的一个成员。
标识符 | 描述 |
---|---|
SYMBOL_TRADE_EXECUTION_REQUEST | 按请求价格进行交易 |
SYMBOL_TRADE_EXECUTION_INSTANT | 即时执行(按实时流价格进行交易) |
SYMBOL_TRADE_EXECUTION_MARKET | 市价执行 |
SYMBOL_TRADE_EXECUTION_EXCHANGE | 交易所执行 |
终端用户应该从“新订单”对话框(F9)中的“类型”下拉列表中了解到这些模式中的全部或大部分。让我们简要回顾一下它们的含义。如需进一步的详细信息,请参考终端文档。
按请求执行(SYMBOL_TRADE_EXECUTION_REQUEST
)—— 以之前从经纪商处收到的价格执行市价单。在发送市价单之前,交易者向经纪商请求当前价格。之后,该订单按此价格执行可能会被确认或拒绝。
即时执行(SYMBOL_TRADE_EXECUTION_INSTANT
)—— 以当前价格执行市价单。在发送交易请求以执行时,终端会自动将当前价格插入到订单中。如果经纪商接受该价格,订单将被执行。如果经纪商不接受请求的价格,经纪商会返回该订单可以执行的价格,这被称为重新报价。
市价执行(SYMBOL_TRADE_EXECUTION_MARKET
)—— 经纪商在没有交易者额外确认的情况下将执行价格插入到订单中。以这种模式发送市价单意味着事先同意订单将以该价格执行。
交易所执行(SYMBOL_TRADE_EXECUTION_EXCHANGE
)—— 交易操作按当前市场报价的价格执行。
至于 SYMBOL_FILLING_MODE
中的位,可以使用逻辑或运算符 OR
(|
)进行组合,这些位的存在或不存在表示以下操作。
标识符 | 值 | 成交策略 |
---|---|---|
SYMBOL_FILLING_FOK | 1 | 立即成交或取消(FOK);订单必须完全按指定的交易量执行,否则取消 |
SYMBOL_FILLING_IOC | 2 | 立即成交否则取消(IOC);在订单指定的限制范围内,交易市场上可获得的最大交易量,否则取消 |
(无标识符) | (任意值,包括 0) | 返回;在部分成交的情况下,剩余交易量的市价单或限价单不会被取消,而是保持有效 |
是否可以使用 FOK 和 IOC 模式由交易服务器决定。
如果启用了 SYMBOL_FILLING_FOK
模式,那么在使用 OrderSend
函数发送订单时,MQL 程序将能够在 MqlTradeRequest
结构中使用相关的订单成交类型:ORDER_FILLING_FOK
。如果同时市场上该金融工具的交易量不足,订单将不会被执行。应该考虑到,所需的交易量可能由市场上当前可用的多个报价组成,从而导致多笔交易。
如果启用了 SYMBOL_FILLING_IOC
模式,MQL 程序将可以使用同名的 ORDER_FILLING_IOC
订单成交方法(在将订单发送到 OrderSend
函数之前,也在 MqlTradeRequest
结构的特殊“成交”字段(type_filling
)中指定)。使用此模式时,如果无法完全执行订单,订单将按可用交易量执行,订单的剩余交易量将被取消。
最后一个没有标识符的策略是默认模式,无论其他模式如何都可用(这就是为什么它与零或任何其他值匹配)。换句话说,即使我们得到 SYMBOL_FILLING_MODE
属性的值为 1(SYMBOL_FILLING_FOK
)、2(SYMBOL_FILLING_IOC
)或 3(SYMBOL_FILLING_FOK | SYMBOL_FILLING_IOC
),也会隐含返回模式。要使用此策略,在创建订单(填充 MqlTradeRequest
结构)时,我们应该指定成交类型 ORDER_FILLING_RETURN
。
在所有 SYMBOL_TRADE_EXEMODE
模式中,关于市价执行(SYMBOL_TRADE_EXECUTION_MARKET
)有一个特殊性:在市价执行模式下,始终禁止返回订单。
由于 ORDER_FILLING_FOK
对应常量 0,在交易请求中没有明确指示成交类型将意味着使用此特定模式。
我们将在开发智能交易系统时在实践中考虑所有这些细节,但现在,让我们在一个简单的脚本 SymbolFilterExecMode.mq5
中检查属性的读取情况。
c
#include <MQL5Book/SymbolFilter.mqh>
void OnStart()
{
SymbolFilter f; // 过滤器对象
string symbols[]; // 交易品种名称数组
long permissions[][2]; // 包含属性值向量的数组
// 要读取的属性
ENUM_SYMBOL_INFO_INTEGER modes[] =
{
SYMBOL_TRADE_EXEMODE,
SYMBOL_FILLING_MODE
};
// 应用过滤器 - 填充数组
f.select(true, modes, symbols, permissions);
const int n = ArraySize(symbols);
PrintFormat("===== Trade execution and filling modes for the symbols (%d) =====", n);
for(int i = 0; i < n; ++i)
{
Print(symbols[i] + ":");
for(int j = 0; j < ArraySize(modes); ++j)
{
// 以描述和数字形式输出属性
PrintFormat(" %s (%d)",
SymbolMonitor::stringify(permissions[i][j], modes[j]),
permissions[i][j]);
}
}
}
下面是带有脚本执行结果的日志片段。除了最后一个 SP500m
(SYMBOL_TRADE_EXECUTION_MARKET
)之外,这里几乎所有的交易品种都具有按价格即时执行的模式(SYMBOL_TRADE_EXECUTION_INSTANT
)。在这里,我们可以找到各种交易量成交模式,既有单独的 SYMBOL_FILLING_FOK
、SYMBOL_FILLING_IOC
,也有它们的组合。只有 BTCUSD
指定了 SYMBOL_FILLING_RETURN
,即接收到的值为 0(没有 FOK 和 IOC 位)。
===== Trade execution and filling modes for the symbols (13) =====
EURUSD:
SYMBOL_TRADE_EXECUTION_INSTANT (1)
[ _SYMBOL_FILLING_FOK ] (1)
GBPUSD:
SYMBOL_TRADE_EXECUTION_INSTANT (1)
[ _SYMBOL_FILLING_FOK ] (1)
...
USDCNH:
SYMBOL_TRADE_EXECUTION_INSTANT (1)
[ _SYMBOL_FILLING_FOK _SYMBOL_FILLING_IOC ] (3)
USDRUB:
SYMBOL_TRADE_EXECUTION_INSTANT (1)
[ _SYMBOL_FILLING_IOC ] (2)
AUDUSD:
SYMBOL_TRADE_EXECUTION_INSTANT (1)
[ _SYMBOL_FILLING_FOK ] (1)
NZDUSD:
SYMBOL_TRADE_EXECUTION_INSTANT (1)
[ _SYMBOL_FILLING_FOK _SYMBOL_FILLING_IOC ] (3)
...
XAUUSD:
SYMBOL_TRADE_EXECUTION_INSTANT (1)
[ _SYMBOL_FILLING_FOK _SYMBOL_FILLING_IOC ] (3)
BTCUSD:
SYMBOL_TRADE_EXECUTION_INSTANT (1)
[(_SYMBOL_FILLING_RETURN)] (0)
SP500m:
SYMBOL_TRADE_EXECUTION_MARKET (2)
[ _SYMBOL_FILLING_FOK ] (1)
回想一下,成交模式标识符中的下划线是因为我们必须定义自己的枚举 SYMBOL_FILLING
(SymbolMonitor.mqh
),其中的元素具有常量值。这样做是因为 MQL5 没有这样的内置枚举,但同时我们不能将我们枚举的元素命名得与内置常量完全一样,因为这会导致名称冲突。
保证金要求
对于交易者而言,关于金融工具的最重要信息之一就是开仓所需的资金数额。如果不知道买卖一定手数的金融工具需要多少资金,就无法在智能交易系统中实施资金管理系统,也无法控制账户余额。
由于 MetaTrader 5 可用于交易各种工具(货币、商品、股票、债券、期权和期货),保证金的计算原则差异很大。相关文档提供了详细信息,特别是针对外汇和期货以及交易所交易的情况。
MQL5 API 的几个属性可用于确定市场类型以及特定金融工具的保证金计算方法。
提前说明一下,对于给定的交易操作类型、金融工具、交易量和价格等参数组合,MQL5 允许使用 OrderCalcMargin
函数来计算保证金。这是最简单的方法,但它有一个显著的限制:该函数不考虑当前的开仓头寸和挂单。这尤其忽略了在账户允许反向头寸时,可能因重叠交易量而进行的调整。
因此,为了获得当前用作开仓头寸和订单保证金的账户资金明细,MQL 程序可能需要分析以下属性,并使用公式进行计算。此外,指标中禁止使用 OrderCalcMargin
函数。你可以使用 OrderCheck
提前估算在拟进行的交易完成后的可用保证金。
标识符 | 描述 |
---|---|
SYMBOL_TRADE_CALC_MODE | 计算保证金和利润的方法(见 ENUM_SYMBOL_CALC_MODE ) |
SYMBOL_MARGIN_HEDGED_USE_LEG | 布尔标志,用于启用(true )或禁用(false )对最大重叠头寸(买入和卖出)的对冲保证金计算模式 |
SYMBOL_MARGIN_INITIAL | 交易所工具的初始保证金 |
SYMBOL_MARGIN_MAINTENANCE | 交易所工具的维持保证金 |
SYMBOL_MARGIN_HEDGED | 已对冲头寸(单个交易品种的反向头寸)每手的合约规模或保证金 |
前两个属性包含在 ENUM_SYMBOL_INFO_INTEGER
枚举中,后三个属性包含在 ENUM_SYMBOL_INFO_DOUBLE
枚举中,可分别通过 SymbolInfoInteger
和 SymbolInfoDouble
函数读取。
具体的保证金计算公式取决于 SYMBOL_TRADE_CALC_MODE
属性,如下表所示。更完整的信息可在 MQL5 文档中找到。
请注意,外汇工具不使用初始保证金和维持保证金,对于外汇工具,这些属性始终为 0。
初始保证金表示开仓一手头寸所需的以保证金货币计价的保证金金额。在进入市场前检查客户资金是否充足时会用到它。要根据订单的类型和方向获取最终收取的保证金金额,请使用 SymbolInfoMarginRate
函数检查保证金比率。因此,经纪商可以为每种工具设置单独的杠杆或折扣。
维持保证金表示维持一手开仓头寸所需的以该工具保证金货币计价的资金最小值。在账户状态(交易条件)变化时检查客户资金是否充足时会用到它。如果资金水平降至所有头寸的维持保证金金额以下,经纪商将开始强制平仓。
如果维持保证金属性为 0,则使用初始保证金。与初始保证金的情况一样,要根据订单的类型和方向获取最终收取的保证金金额,应使用 SymbolInfoMarginRate
函数检查保证金比率。
对冲头寸,即同一交易品种的多方向头寸,只能存在于对冲交易账户中。显然,只有在这样的账户上,结合 SYMBOL_MARGIN_HEDGED_USE_LEG
和 SYMBOL_MARGIN_HEDGED
属性来计算对冲保证金才有意义。对冲保证金适用于已对冲的交易量。
经纪商可以为每种工具选择两种现有方法之一来计算已对冲头寸的保证金:
- 当最长边计算模式禁用时,即
SYMBOL_MARGIN_HEDGED_USE_LEG
属性等于false
时,应用基础计算。在这种情况下,保证金由三个部分组成:现有头寸未对冲交易量的保证金、已对冲交易量的保证金(如果存在反向头寸且SYMBOL_MARGIN_HEDGED
属性不为 0)、挂单的保证金。如果为该工具设置了初始保证金(SYMBOL_MARGIN_INITIAL
属性不为 0),则对冲保证金指定为绝对值(以货币计)。如果未设置初始保证金(等于 0),则SYMBOL_MARGIN_HEDGED
指定合约规模,将根据与交易工具类型对应的公式(SYMBOL_TRADE_CALC_MODE
)计算保证金时使用该合约规模。 - 当
SYMBOL_MARGIN_HEDGED_USE_LEG
属性等于true
时,应用最高头寸计算。在这种情况下,将忽略SYMBOL_MARGIN_HEDGED
的值。相反,计算该工具上所有空头和多头头寸的交易量,并为每一方计算加权平均开仓价格。然后,使用与工具类型对应的公式(SYMBOL_TRADE_CALC_MODE
),计算空头方和多头方的保证金。将最大值用作最终值。
下表列出了 ENUM_SYMBOL_CALC_MODE
枚举元素及其各自的保证金计算方法。相同的属性(SYMBOL_TRADE_CALC_MODE
)也负责计算头寸的盈亏,但我们将在后续关于 MQL5 交易函数的章节中讨论这一方面。
标识符 | 公式 |
---|---|
SYMBOL_CALC_MODE_FOREX | 外汇:手数 * 合约规模 * 保证金比率 / 杠杆 |
SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE | 无杠杆外汇:手数 * 合约规模 * 保证金比率 |
SYMBOL_CALC_MODE_CFD | 差价合约(CFD):手数 * 合约规模 * 市场价格 * 保证金比率 |
SYMBOL_CALC_MODE_CFDLEVERAGE | 有杠杆差价合约:手数 * 合约规模 * 市场价格 * 保证金比率 / 杠杆 |
SYMBOL_CALC_MODE_CFDINDEX | 指数差价合约:手数 * 合约规模 * 市场价格 * 最小变动价位 / 最小变动价位单位 * 保证金比率 |
SYMBOL_CALC_MODE_EXCH_STOCKS | 证券交易所的证券:手数 * 合约规模 * 最新价格 * 保证金比率 |
SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX | 莫斯科交易所(MOEX)的证券:手数 * 合约规模 * 最新价格 * 保证金比率 |
SYMBOL_CALC_MODE_FUTURES | 期货:手数 * 初始保证金 * 保证金比率 |
SYMBOL_CALC_MODE_EXCH_FUTURES | 证券交易所的期货:手数 * 初始保证金 * 保证金比率 或 手数 * 维持保证金 * 保证金比率 |
SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS | 俄罗斯期货交易所(FORTS)的期货:手数 * 初始保证金 * 保证金比率 或 手数 * 维持保证金 * 保证金比率 |
SYMBOL_CALC_MODE_EXCH_BONDS | 证券交易所的债券:手数 * 合约规模 * 债券面值 * 开盘价格 / 100 |
SYMBOL_CALC_MODE_EXCH_BONDS_MOEX | 莫斯科交易所的债券:手数 * 合约规模 * 债券面值 * 开盘价格 / 100 |
SYMBOL_CALC_MODE_SERV_COLLATERAL | 非交易资产(不适用保证金) |
公式中使用了以下符号:
手数
:头寸或订单的手数(合约份额)合约规模
:合约规模(一手,SYMBOL_TRADE_CONTRACT_SIZE
)杠杆
:交易账户杠杆(ACCOUNT_LEVERAGE
)初始保证金
:初始保证金(SYMBOL_MARGIN_INITIAL
)维持保证金
:维持保证金(SYMBOL_MARGIN_MAINTENANCE
)最小变动价位
:最小变动价位(SYMBOL_TRADE_TICK_VALUE
)最小变动价位单位
:最小变动价位单位(SYMBOL_TRADE_TICK_SIZE
)市场价格
:根据交易类型确定的最新已知买价/卖价最新价格
:最新已知的最新价开盘价格
:头寸或订单开仓的加权平均价格债券面值
:债券的面值保证金比率
:根据SymbolInfoMarginRate
函数确定的保证金比率,也可以有两种不同的值:初始保证金和维持保证金
在文件 MarginProfitMeter.mqh
中给出了大多数类型交易品种公式计算的另一种实现方式(见 “估算交易操作的利润” 部分)。它也可以在指标中使用。
让我们对一些模式做几点说明:
- 在上表中,只有三个期货公式使用了初始保证金(
SYMBOL_MARGIN_INITIAL
)。然而,如果任何其他交易品种的规格中该属性的值不为 0,则它将决定保证金。 - 一些交易所可能会对保证金调整施加自己的特殊规定,例如俄罗斯期货交易所(FORTS)的折扣系统(
SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS
)。有关详细信息,请参阅 MQL5 文档和你的经纪商。 - 在
SYMBOL_CALC_MODE_SERV_COLLATERAL
模式下,工具的值会在 “资产” 中考虑,这些资产会添加到 “净值” 中。因此,此类工具的开仓头寸会增加可用保证金的金额,并作为交易工具开仓头寸的额外抵押品。开仓头寸的市场价值根据交易量、合约规模、当前市场价格和流动性比率计算:手数 * 合约规模 * 市场价格 * 流动性比率
(后者的值可作为SYMBOL_TRADE_LIQUIDITY_RATE
属性获取)。
作为使用与保证金相关属性的示例,我们来看脚本 SymbolFilterMarginStats.mq5
。其目的是计算所选交易品种列表中保证金计算方法的统计信息,并可选择记录每个交易品种的这些属性。我们将使用已熟知的过滤器类 SymbolFilter
和从输入变量提供的条件来选择要分析的交易品种。
c
#include <MQL5Book/SymbolFilter.mqh>
input bool UseMarketWatch = false;
input bool ShowPerSymbolDetails = false;
input bool ExcludeZeroInitMargin = false;
input bool ExcludeZeroMainMargin = false;
input bool ExcludeZeroHedgeMargin = false;
默认情况下,会请求所有可用交易品种的信息。若要将范围限制为仅市场报价,应将 UseMarketWatch
设置为 true
。
参数 ShowPerSymbolDetails
允许启用输出每个交易品种的详细信息(默认情况下,该参数为 false
,仅显示统计信息)。
最后三个参数用于根据零保证金值的条件(分别为初始、维持和对冲保证金)过滤交易品种。
为了在日志中收集并方便显示每个交易品种的完整属性集(当 ShowPerSymbolDetails
启用时),在代码中定义了 MarginSettings
结构体。
c
struct MarginSettings
{
string name;
ENUM_SYMBOL_CALC_MODE calcMode;
bool hedgeLeg;
double initial;
double maintenance;
double hedged;
};
由于某些属性是整数类型(SYMBOL_TRADE_CALC_MODE
、SYMBOL_MARGIN_HEDGED_USE_LEG
),而某些是实数类型(SYMBOL_MARGIN_INITIAL
、SYMBOL_MARGIN_MAINTENANCE
、SYMBOL_MARGIN_HEDGED
),因此必须由过滤器对象分别请求它们。
现在我们直接看 OnStart
中的工作代码。这里,像往常一样,我们定义过滤器对象(f
)、用于交易品种名称的输出数组(symbols
)以及请求属性的值(flags
、values
)。除了它们之外,我们还添加一个 MarginSettings
结构体数组。
c
void OnStart()
{
SymbolFilter f; // 过滤对象
string symbols[]; // 名称数组
long flags[][2]; // 整数向量数组
double values[][3]; // 实数向量数组
MarginSettings margins[]; // 复合输出数组
...
引入了 stats
数组映射,用于计算统计信息,其键为 ENUM_SYMBOL_CALC_MODE
类型,整数值表示每种方法出现的次数。此外,所有零保证金情况和在较长边启用的计算模式都应记录在相应的计数器变量中。
c
MapArray<ENUM_SYMBOL_CALC_MODE,int> stats; // 每种方法/模式的计数器
int hedgeLeg = 0; // 其他计数器
int zeroInit = 0; //...
int zeroMaintenance = 0;
int zeroHedged = 0;
...
接下来,我们指定与保证金相关的感兴趣的属性,这些属性将从交易品种设置中读取。首先是 ints
数组中的整数属性,然后是 doubles
数组中的实数属性。
c
ENUM_SYMBOL_INFO_INTEGER ints[] =
{
SYMBOL_TRADE_CALC_MODE,
SYMBOL_MARGIN_HEDGED_USE_LEG
};
ENUM_SYMBOL_INFO_DOUBLE doubles[] =
{
SYMBOL_MARGIN_INITIAL,
SYMBOL_MARGIN_MAINTENANCE,
SYMBOL_MARGIN_HEDGED
};
...
根据输入参数,我们将设置过滤条件。
c
if(ExcludeZeroInitMargin) f.let(SYMBOL_MARGIN_INITIAL, 0, SymbolFilter::IS::GREATER);
if(ExcludeZeroMainMargin) f.let(SYMBOL_MARGIN_MAINTENANCE, 0, SymbolFilter::IS::GREATER);
if(ExcludeZeroHedgeMargin) f.let(SYMBOL_MARGIN_HEDGED, 0, SymbolFilter::IS::GREATER);
...
现在,一切准备就绪,可以根据条件选择交易品种并将它们的属性获取到数组中。我们分两次进行,分别针对整数属性和实数属性。
c
f.select(UseMarketWatch, ints, symbols, flags);
const int n = ArraySize(symbols);
ArrayResize(symbols, 0, n);
f.select(UseMarketWatch, doubles, symbols, values);
...
在第一次应用过滤器后,交易品种名称数组必须清零,以避免名称重复。尽管进行了两次单独的查询,但所有输出数组(ints
和 doubles
)中的元素顺序是相同的,因为过滤条件没有改变。
如果用户启用了详细日志记录,我们为 margins
结构体数组分配内存。
c
if(ShowPerSymbolDetails) ArrayResize(margins, n);
最后,我们通过迭代结果数组的所有元素来计算统计信息,并可选择填充结构体数组。
c
for(int i = 0; i < n; ++i)
{
stats.inc((ENUM_SYMBOL_CALC_MODE)flags[i].value[0]);
hedgeLeg += (int)flags[i].value[1];
if(values[i].value[0] == 0) zeroInit++;
if(values[i].value[1] == 0) zeroMaintenance++;
if(values[i].value[2] == 0) zeroHedged++;
if(ShowPerSymbolDetails)
{
margins[i].name = symbols[i];
margins[i].calcMode = (ENUM_SYMBOL_CALC_MODE)flags[i][0];
margins[i].hedgeLeg = (bool)flags[i][1];
margins[i].initial = values[i][0];
margins[i].maintenance = values[i][1];
margins[i].hedged = values[i][2];
}
}
...
现在我们在日志中显示统计信息。
c
PrintFormat("===== Margin calculation modes for %s symbols %s=====",
(UseMarketWatch? "Market Watch" : "all available"),
(ExcludeZeroInitMargin || ExcludeZeroMainMargin || ExcludeZeroHedgeMargin
? "(with conditions) " : ""));
PrintFormat("Total symbols: %d", n);
PrintFormat("Hedge leg used in: %d", hedgeLeg);
PrintFormat("Zero margin counts: initial=%d, maintenance=%d, hedged=%d",
zeroInit, zeroMaintenance, zeroHedged);
Print("Stats per calculation mode:");
stats.print();
...
由于 ENUM_SYMBOL_CALC_MODE
枚举的成员显示为整数(这不是很直观),我们还显示一个文本,其中每个值都有一个名称(来自 EnumToString
)。
c
Print("Legend: key=calculation mode, value=count");
for(int i = 0; i < stats.getSize(); ++i)
{
PrintFormat("%d -> %s", stats.getKey(i), EnumToString(stats.getKey(i)));
}
...
如果需要所选交易品种的详细信息,我们输出 margins
结构体数组。
c
if(ShowPerSymbolDetails)
{
Print("Settings per symbol:");
ArrayPrint(margins);
}
}
运行脚本的不同设置情况
第一次运行:使用默认设置
所有可用交易品种的保证金计算模式
- 交易品种总数:131
- 用作对冲腿的数量:14
- 零保证金数量:初始保证金=123,维持保证金=130,对冲保证金=32
- 各计算模式统计: | [键] | [值] | | --- | --- | | [0] | 0 | 101 | | [1] | 4 | 16 | | [2] | 1 | 1 | | [3] | 2 | 11 | | [4] | 5 | 2 |
图例:键 = 计算模式,值 = 数量
- 0 -> 外汇计算模式(SYMBOL_CALC_MODE_FOREX)
- 4 -> 差价合约杠杆计算模式(SYMBOL_CALC_MODE_CFDLEVERAGE)
- 1 -> 期货计算模式(SYMBOL_CALC_MODE_FUTURES)
- 2 -> 差价合约计算模式(SYMBOL_CALC_MODE_CFD)
- 5 -> 无杠杆外汇计算模式(SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE)
第二次运行:将 ShowPerSymbolDetails 和 ExcludeZeroInitMargin 设置为 true
此设置请求获取所有初始保证金不为零的交易品种的详细信息。
所有可用交易品种的保证金计算模式(符合条件)
- 交易品种总数:8
- 用作对冲腿的数量:0
- 零保证金数量:初始保证金=0,维持保证金=7,对冲保证金=0
- 各计算模式统计: | [键] | [值] | | --- | --- | | [0] | 0 | 5 | | [1] | 1 | 1 | | [2] | 5 | 2 |
图例:键 = 计算模式,值 = 数量
- 0 -> 外汇计算模式(SYMBOL_CALC_MODE_FOREX)
- 1 -> 期货计算模式(SYMBOL_CALC_MODE_FUTURES)
- 5 -> 无杠杆外汇计算模式(SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE)
各交易品种的设置
[名称] | [计算模式] | [对冲腿] | [初始保证金] | [维持保证金] | [对冲保证金] |
---|---|---|---|---|---|
[0] | "XAUEUR" | 0 | false | 100.00000 | 0.00000 |
[1] | "XAUAUD" | 0 | false | 100.00000 | 0.00000 |
[2] | "XAGEUR" | 0 | false | 1000.00000 | 0.00000 |
[3] | "USDGEL" | 0 | false | 100000.00000 | 100000.00000 |
[4] | "SP500m" | 1 | false | 6600.00000 | 0.00000 |
[5] | "XBRUSD" | 5 | false | 100.00000 | 0.00000 |
[6] | "XNGUSD" | 0 | false | 10000.00000 | 0.00000 |
[7] | "XTIUSD" | 5 | false | 100.00000 | 0.00000 |
挂单过期规则
在处理挂单(包括止损和止盈水平)时,MQL 程序应检查两个属性,这两个属性定义了挂单的过期规则。这两个属性都可以作为 ENUM_SYMBOL_INFO_INTEGER
枚举的成员,通过 SymbolInfoInteger
函数调用获取。
标识符 | 描述 |
---|---|
SYMBOL_EXPIRATION_MODE | 允许的订单过期模式标志(位掩码) |
SYMBOL_ORDER_GTC_MODE | 有效期由 ENUM_SYMBOL_ORDER_GTC_MODE 枚举中的一个元素定义 |
只有当 SYMBOL_EXPIRATION_MODE
包含 SYMBOL_EXPIRATION_GTC
时,才会考虑 SYMBOL_ORDER_GTC_MODE
属性。GTC 是 “Good Till Canceled” 的缩写。
对于每个金融工具,SYMBOL_EXPIRATION_MODE
属性可以指定挂单的几种有效(过期)模式。每种模式都有一个关联的标志(位)。
标识符(值) | 描述 |
---|---|
SYMBOL_EXPIRATION_GTC (1) | 订单根据 ENUM_SYMBOL_ORDER_GTC_MODE 属性有效 |
SYMBOL_EXPIRATION_DAY (2) | 订单在当前交易日结束前有效 |
SYMBOL_EXPIRATION_SPECIFIED (4) | 订单中指定了过期日期和时间 |
SYMBOL_EXPIRATION_SPECIFIED_DAY (8) | 订单中指定了过期日期 |
这些标志可以通过逻辑或(|
)操作进行组合,例如,SYMBOL_EXPIRATION_GTC | SYMBOL_EXPIRATION_SPECIFIED
等同于 1 | 4
,结果为数字 5。要检查某个工具是否启用了特定模式,可对函数结果和所需模式位进行逻辑与(&
)操作:非零值表示该模式可用。
在 SYMBOL_EXPIRATION_SPECIFIED_DAY
的情况下,订单在指定日期的 23:59:59 前有效。如果这个时间不在交易时段内,过期将发生在最近的下一个交易时间。
ENUM_SYMBOL_ORDER_GTC_MODE
枚举包含以下成员。
标识符 | 描述 |
---|---|
SYMBOL_ORDERS_GTC | 挂单和止损/止盈水平在明确取消之前一直有效 |
SYMBOL_ORDERS_DAILY | 订单仅在一个交易日内有效:交易日结束时,所有挂单以及止损和止盈水平都将被删除 |
SYMBOL_ORDERS_DAILY_EXCLUDING_STOPS | 交易日变更时,仅删除挂单,但保留止损和止盈水平 |
根据 SYMBOL_EXPIRATION_MODE
属性中设置的位,在准备发送订单时,MQL 程序可以选择与这些位对应的模式之一。从技术上讲,这是通过在调用 OrderSend
函数之前,在特殊结构 MqlTradeRequest
中填充 type_time
字段来实现的。该字段的值必须是 ENUM_ORDER_TYPE_TIME
枚举的一个元素(请参阅“挂单过期日期”):正如我们稍后将看到的,它与上述标志集有一定的关联,即每个标志在订单中设置相应的模式:ORDER_TIME_GTC
、ORDER_TIME_DAY
、ORDER_TIME_SPECIFIED
、ORDER_TIME_SPECIFIED_DAY
。过期时间或日期本身必须在同一结构的另一个字段中指定。
脚本 SymbolFilterExpiration.mq5
允许你了解可用品种(在市场报价窗口中或总体上,取决于输入参数 UseMarketWatch
)中每个标志的使用统计信息。第二个参数 ShowPerSymbolDetails
设置为 true
时,将记录每个品种的所有标志,因此请注意:如果同时 UseMarketWatch
模式设置为 false
,将生成大量的日志条目。
c
#property script_show_inputs
#include <MQL5Book/SymbolFilter.mqh>
input bool UseMarketWatch = false;
input bool ShowPerSymbolDetails = false;
void OnStart()
{
SymbolFilter f; // 筛选器对象
string symbols[]; // 用于接收品种名称的数组
long flags[][2]; // 用于接收属性值的数组
MapArray<SYMBOL_EXPIRATION,int> stats; // 模式计数器
MapArray<ENUM_SYMBOL_ORDER_GTC_MODE,int> gtc; // GTC 计数器
ENUM_SYMBOL_INFO_INTEGER ints[] =
{
SYMBOL_EXPIRATION_MODE,
SYMBOL_ORDER_GTC_MODE
};
f.select(UseMarketWatch, ints, symbols, flags);
const int n = ArraySize(symbols);
for(int i = 0; i < n; ++i)
{
if(ShowPerSymbolDetails)
{
Print(symbols[i] + ":");
for(int j = 0; j < ArraySize(ints); ++j)
{
// 以描述和数字的形式显示属性
PrintFormat(" %s (%d)",
SymbolMonitor::stringify(flags[i][j], ints[j]),
flags[i][j]);
}
}
const SYMBOL_EXPIRATION mode = (SYMBOL_EXPIRATION)flags[i][0];
for(int j = 0; j < 4; ++j)
{
const SYMBOL_EXPIRATION bit = (SYMBOL_EXPIRATION)(1 << j);
if((mode & bit) != 0)
{
stats.inc(bit);
}
if(bit == SYMBOL_EXPIRATION_GTC)
{
gtc.inc((ENUM_SYMBOL_ORDER_GTC_MODE)flags[i][1]);
}
}
}
PrintFormat("===== Expiration modes for %s symbols =====",
(UseMarketWatch ? "Market Watch" : "all available"));
PrintFormat("Total symbols: %d", n);
Print("Stats per expiration mode:");
stats.print();
Print("Legend: key=expiration mode, value=count");
for(int i = 0; i < stats.getSize(); ++i)
{
PrintFormat("%d -> %s", stats.getKey(i), EnumToString(stats.getKey(i)));
}
Print("Stats per GTC mode:");
gtc.print();
Print("Legend: key=GTC mode, value=count");
for(int i = 0; i < gtc.getSize(); ++i)
{
PrintFormat("%d -> %s", gtc.getKey(i), EnumToString(gtc.getKey(i)));
}
}
运行脚本示例
第一次运行:使用默认设置
plaintext
===== Expiration modes for all available symbols =====
Total symbols: 52357
Stats per expiration mode:
[key] [value]
[0] 1 52357
[1] 2 52357
[2] 4 52357
[3] 8 52303
Legend: key=expiration mode, value=count
1 -> _SYMBOL_EXPIRATION_GTC
2 -> _SYMBOL_EXPIRATION_DAY
4 -> _SYMBOL_EXPIRATION_SPECIFIED
8 -> _SYMBOL_EXPIRATION_SPECIFIED_DAY
Stats per GTC mode:
[key] [value]
[0] 0 52357
Legend: key=GTC mode, value=count
0 -> SYMBOL_ORDERS_GTC
从这里可以看出,大多数品种几乎允许所有标志,并且对于 SYMBOL_EXPIRATION_GTC
模式,仅使用 SYMBOL_ORDERS_GTC
这一变体。
第二次运行:将 UseMarketWatch
和 ShowPerSymbolDetails
设置为 true
(假设市场报价窗口中选择了有限数量的品种)
plaintext
GBPUSD:
[ _SYMBOL_EXPIRATION_GTC _SYMBOL_EXPIRATION_DAY _SYMBOL_EXPIRATION_SPECIFIED ] (7)
SYMBOL_ORDERS_GTC (0)
USDCHF:
[ _SYMBOL_EXPIRATION_GTC _SYMBOL_EXPIRATION_DAY _SYMBOL_EXPIRATION_SPECIFIED ] (7)
SYMBOL_ORDERS_GTC (0)
USDJPY:
[ _SYMBOL_EXPIRATION_GTC _SYMBOL_EXPIRATION_DAY _SYMBOL_EXPIRATION_SPECIFIED ] (7)
SYMBOL_ORDERS_GTC (0)
...
XAUUSD:
[ _SYMBOL_EXPIRATION_GTC _SYMBOL_EXPIRATION_DAY _SYMBOL_EXPIRATION_SPECIFIED
_SYMBOL_EXPIRATION_SPECIFIED_DAY ] (15)
SYMBOL_ORDERS_GTC (0)
SP500m:
[ _SYMBOL_EXPIRATION_GTC _SYMBOL_EXPIRATION_DAY _SYMBOL_EXPIRATION_SPECIFIED
_SYMBOL_EXPIRATION_SPECIFIED_DAY ] (15)
SYMBOL_ORDERS_GTC (0)
UK100:
[ _SYMBOL_EXPIRATION_GTC _SYMBOL_EXPIRATION_DAY _SYMBOL_EXPIRATION_SPECIFIED
_SYMBOL_EXPIRATION_SPECIFIED_DAY ] (15)
SYMBOL_ORDERS_GTC (0)
===== Expiration modes for Market Watch symbols =====
Total symbols: 15
Stats per expiration mode:
[key] [value]
[0] 1 15
[1] 2 15
[2] 4 15
[3] 8 6
Legend: key=expiration mode, value=count
1 -> _SYMBOL_EXPIRATION_GTC
2 -> _SYMBOL_EXPIRATION_DAY
4 -> _SYMBOL_EXPIRATION_SPECIFIED
8 -> _SYMBOL_EXPIRATION_SPECIFIED_DAY
Stats per GTC mode:
[key] [value]
[0] 0 15
Legend: key=GTC mode, value=count
0 -> SYMBOL_ORDERS_GTC
在选择的 15 个品种中,只有 6 个设置了 SYMBOL_EXPIRATION_SPECIFIED_DAY
标志。每个品种标志的详细信息可以在上面找到。
点差以及订单与当前价格的距离
对于许多交易策略,尤其是基于短期交易的策略而言,有关点差以及允许设置或修改订单的与当前价格的距离信息非常重要。所有这些属性都是 ENUM_SYMBOL_INFO_INTEGER
枚举的一部分,并且可以通过 SymbolInfoInteger
函数获取。
标识符 | 描述 |
---|---|
SYMBOL_SPREAD | 点差大小(以点数计) |
SYMBOL_SPREAD_FLOAT | 浮动点差的布尔标志 |
SYMBOL_TRADE_STOPS_LEVEL | 设置止损(Stop Loss)、止盈(Take Profit)和挂单时,与当前价格的最小允许距离(以点数计) |
SYMBOL_TRADE_FREEZE_LEVEL | 冻结订单和头寸时,与当前价格的距离(以点数计) |
在上述表格中,当前价格是指买入价(Ask)或卖出价(Bid),具体取决于正在执行的操作的性质。
保护性的止损和止盈水平表明应该平仓。这是通过与开仓操作相反的操作来完成的。因此,对于以买入价(Ask)开仓的买入订单,保护性水平指的是卖出价(Bid);对于以卖出价(Bid)开仓的卖出订单,保护性水平指的是买入价(Ask)。在下达挂单时,以标准方式选择开仓价格类型:买入订单(买入止损单(Buy Stop)、买入限价单(Buy Limit)、买入止损限价单(Buy Stop Limit))基于买入价(Ask),卖出订单(卖出止损单(Sell Stop)、卖出限价单(Sell Limit)、卖出止损限价单(Sell Stop Limit))基于卖出价(Bid)。考虑到上述交易操作背景下的这些价格类型,会为 SYMBOL_TRADE_STOPS_LEVEL
和 SYMBOL_TRADE_FREEZE_LEVEL
属性计算以点数表示的距离。
SYMBOL_TRADE_STOPS_LEVEL
属性,如果其值不为零,当新的止损和止盈水平与当前价格的距离比指定距离更近时,将禁止修改未平仓头寸的止损和止盈水平。同样,也不可能将挂单的开仓价格移动到与当前价格的距离小于 SYMBOL_TRADE_STOPS_LEVEL
点数的位置。
SYMBOL_TRADE_FREEZE_LEVEL
属性,如果其值不为零,会在与当前价格的指定距离内限制未平仓订单或未平仓头寸的任何交易操作。对于挂单,当指定的开仓价格与当前价格的距离小于 SYMBOL_TRADE_FREEZE_LEVEL
点数时(同样,当前价格的类型是买入价(Ask)还是卖出价(Bid),取决于操作是买入还是卖出),就会发生冻结。对于头寸,当止损和止盈水平恰好在当前价格附近时会发生冻结,因此对它们的测量是针对“相反”的价格类型进行的。
如果 SYMBOL_SPREAD_FLOAT
属性为 true
,那么 SYMBOL_SPREAD
属性就不是交易品种规格的一部分,而是包含实际的点差,每次调用时会根据市场情况动态变化。也可以通过调用 SymbolInfoTick
函数,在 MqlTick
结构中找到买入价(Ask)和卖出价(Bid)之间的差值来获取点差。
脚本 SymbolFilterSpread.mq5
将允许您分析上述属性。它定义了一个自定义枚举 ENUM_SYMBOL_INFO_INTEGER_PART
,该枚举仅包含 ENUM_SYMBOL_INFO_INTEGER
中在这种情况下我们感兴趣的属性。
c
enum ENUM_SYMBOL_INFO_INTEGER_PART
{
SPREAD_FIXED = SYMBOL_SPREAD,
SPREAD_FLOAT = SYMBOL_SPREAD_FLOAT,
STOPS_LEVEL = SYMBOL_TRADE_STOPS_LEVEL,
FREEZE_LEVEL = SYMBOL_TRADE_FREEZE_LEVEL
};
新的枚举定义了 Property
输入参数,该参数指定将分析四个属性中的哪一个。UseMarketWatch
和 ShowPerSymbolDetails
参数以我们已经熟悉的方式控制该过程,就像在之前的测试脚本中一样。
c
input bool UseMarketWatch = true;
input ENUM_SYMBOL_INFO_INTEGER_PART Property = SPREAD_FIXED;
input bool ShowPerSymbolDetails = true;
为了使用 ArrayPrint
函数方便地显示每个交易品种的信息(每行显示属性名称和值),定义了一个辅助结构 SymbolDistance
(仅在 ShowPerSymbolDetails
等于 true
时使用)。
c
struct SymbolDistance
{
string name;
int value;
};
在 OnStart
处理程序中,我们描述了必要的对象和数组。
c
void OnStart()
{
SymbolFilter f; // 过滤器对象
string symbols[]; // 用于接收名称的数组
long values[]; // 用于接收值的数组
SymbolDistance distances[]; // 用于打印的数组
MapArray<long,int> stats; // 所选属性特定值的计数器
...
然后我们应用过滤器,用指定 Property
的值填充接收数组,同时进行排序。
c
f.select(UseMarketWatch, (ENUM_SYMBOL_INFO_INTEGER)Property, symbols, values, true);
const int n = ArraySize(symbols);
if(ShowPerSymbolDetails) ArrayResize(distances, n);
...
在循环中,我们统计数据并在需要时填充 SymbolDistance
结构。
c
for(int i = 0; i < n; ++i)
{
stats.inc(values[i]);
if(ShowPerSymbolDetails)
{
distances[i].name = symbols[i];
distances[i].value = (int)values[i];
}
}
...
最后,我们将结果输出到日志中。
c
PrintFormat("===== Distances for %s symbols =====",
(UseMarketWatch ? "Market Watch" : "all available"));
PrintFormat("Total symbols: %d", n);
PrintFormat("Stats per %s:", EnumToString((ENUM_SYMBOL_INFO_INTEGER)Property));
stats.print();
if(ShowPerSymbolDetails)
{
Print("Details per symbol:");
ArrayPrint(distances);
}
}
当使用默认设置运行脚本时,会得到如下结果,这与点差分析是一致的。
===== Distances for Market Watch symbols =====
Total symbols: 13
Stats per SYMBOL_SPREAD:
[key] [value]
[0] 0 2
[1] 2 3
[2] 3 1
[3] 6 1
[4] 7 1
[5] 9 1
[6] 151 1
[7] 319 1
[8] 3356 1
[9] 3400 1
Details per symbol:
[name] [value]
[ 0] "USDJPY" 0
[ 1] "EURUSD" 0
[ 2] "USDCHF" 2
[ 3] "USDCAD" 2
[ 4] "GBPUSD" 2
[ 5] "AUDUSD" 3
[ 6] "XAUUSD" 6
[ 7] "SP500m" 7
[ 8] "NZDUSD" 9
[ 9] "USDCNH" 151
[10] "USDSEK" 319
[11] "BTCUSD" 3356
[12] "USDRUB" 3400
为了了解点差是浮动的(动态变化)还是固定的,让我们使用不同的设置运行脚本:Property = SPREAD_FLOAT
,ShowPerSymbolDetails = false
。
===== Distances for Market Watch symbols =====
Total symbols: 13
Stats per SYMBOL_SPREAD_FLOAT:
[key] [value]
[0] 1 13
根据这些数据,市场报价中的所有交易品种都有浮动点差(SYMBOL_SPREAD_FLOAT
中键的值为 1 即 true
)。因此,如果我们在市场开盘时一次又一次地使用默认设置运行该脚本,我们将收到新的值。
获取掉期规模
对于实施中、长期交易策略而言,掉期规模变得非常重要,因为它们通常会对财务结果产生显著的、往往是负面的影响。不过,一些读者可能是 “套息交易” 策略的爱好者,该策略最初就是基于从正掉期收益中获利而构建的。MQL5 有几个交易品种属性,可用于访问与掉期相关的规格字符串。
标识符 | 描述 |
---|---|
SYMBOL_SWAP_MODE | 掉期计算模型(ENUM_SYMBOL_SWAP_MODE ) |
SYMBOL_SWAP_ROLLOVER3DAYS | 三倍掉期信贷的星期几(ENUM_DAY_OF_WEEK ) |
SYMBOL_SWAP_LONG | 多头头寸的掉期规模 |
SYMBOL_SWAP_SHORT | 空头头寸的掉期规模 |
ENUM_SYMBOL_SWAP_MODE
枚举包含指定掉期度量单位和计算原则的元素。与 SYMBOL_SWAP_ROLLOVER3DAYS
一样,它们属于 ENUM_SYMBOL_INFO_INTEGER
的整数属性。
掉期规模在 SYMBOL_SWAP_LONG
和 SYMBOL_SWAP_SHORT
属性中直接指定,作为 ENUM_SYMBOL_INFO_DOUBLE
的一部分,即 double
类型。
以下是 ENUM_SYMBOL_SWAP_MODE
的元素:
标识符 | 描述 |
---|---|
SYMBOL_SWAP_MODE_DISABLED | 无掉期 |
SYMBOL_SWAP_MODE_POINTS | 点数 |
SYMBOL_SWAP_MODE_CURRENCY_SYMBOL | 交易品种的基础货币 |
SYMBOL_SWAP_MODE_CURRENCY_MARGIN | 交易品种的保证金货币 |
SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT | 存款货币 |
SYMBOL_SWAP_MODE_INTEREST_CURRENT | 掉期计算时工具价格的年利率 |
SYMBOL_SWAP_MODE_INTEREST_OPEN | 交易品种头寸开仓价格的年利率 |
SYMBOL_SWAP_MODE_REOPEN_CURRENT | 点数(按收盘价重新开仓) |
SYMBOL_SWAP_MODE_REOPEN_BID | 点数(按新一天的买价重新开仓,在 SYMBOL_SWAP_LONG 和 SYMBOL_SWAP_SHORT 参数中) |
对于 SYMBOL_SWAP_MODE_INTEREST_CURRENT
和 SYMBOL_SWAP_MODE_INTEREST_OPEN
选项,假定一年有 360 个银行工作日。
对于 SYMBOL_SWAP_MODE_REOPEN_CURRENT
和 SYMBOL_SWAP_MODE_REOPEN_BID
选项,头寸在交易日结束时被强制平仓,然后它们的行为有所不同。
使用 SYMBOL_SWAP_MODE_REOPEN_CURRENT
时,头寸会在第二天按昨天的收盘价加上或减去指定的点数重新开仓。使用 SYMBOL_SWAP_MODE_REOPEN_BID
时,头寸会在第二天按当前买价加上或减去指定的点数重新开仓。在这两种情况下,点数都在 SYMBOL_SWAP_LONG
和 SYMBOL_SWAP_SHORT
参数中。
让我们使用脚本 SymbolFilterSwap.mq5
来检查这些属性的运行情况。在输入参数中,我们提供了分析上下文的选择:根据 UseMarketWatch
的值选择市场报价中的交易品种或所有交易品种。当 ShowPerSymbolDetails
参数为 false
时,我们将计算统计信息,即 ENUM_SYMBOL_SWAP_MODE
中的各种模式在交易品种中使用的次数。当 ShowPerSymbolDetails
参数为 true
时,我们将输出包含指定模式(mode
)的所有交易品种的数组,并根据 SYMBOL_SWAP_LONG
和 SYMBOL_SWAP_SHORT
字段中的值按降序对数组进行排序。
c
input bool UseMarketWatch = true;
input bool ShowPerSymbolDetails = false;
input ENUM_SYMBOL_SWAP_MODE Mode = SYMBOL_SWAP_MODE_POINTS;
对于掉期的组合数组元素,我们使用交易品种名称和掉期值来描述 SymbolSwap
结构体。掉期的方向将在 name
字段中用前缀表示:“+” 表示多头头寸的掉期,“-” 表示空头头寸的掉期。
c
struct SymbolSwap
{
string name;
double value;
};
按照惯例,我们在 OnStart
的开头描述过滤器对象。然而,根据 ShowPerSymbolDetails
变量的值,以下代码会有很大差异。
c
void OnStart()
{
SymbolFilter f; // 过滤对象
PrintFormat("===== Swap modes for %s symbols =====",
(UseMarketWatch? "Market Watch" : "all available"));
if(ShowPerSymbolDetails)
{
// 所选模式的掉期汇总表
...
}
else
{
// 模式统计信息的计算
...
}
}
我们先来介绍第二个分支。在这里,我们使用过滤器填充包含交易品种名称的数组(symbols
)以及从 SYMBOL_SWAP_MODE
属性获取的掉期模式数组(values
)。得到的值会累积在 MapArray<ENUM_SYMBOL_SWAP_MODE,int> stats
数组映射中。
c
// 模式统计信息的计算
string symbols[];
long values[];
MapArray<ENUM_SYMBOL_SWAP_MODE,int> stats; // 每种模式的计数器
// 应用过滤器并收集模式值
f.select(UseMarketWatch, SYMBOL_SWAP_MODE, symbols, values);
const int n = ArraySize(symbols);
for(int i = 0; i < n; ++i)
{
stats.inc((ENUM_SYMBOL_SWAP_MODE)values[i]);
}
...
接下来,我们显示收集到的统计信息。
c
PrintFormat("Total symbols: %d", n);
Print("Stats per swap mode:");
stats.print();
Print("Legend: key=swap mode, value=count");
for(int i = 0; i < stats.getSize(); ++i)
{
PrintFormat("%d -> %s", stats.getKey(i), EnumToString(stats.getKey(i)));
}
对于构建掉期值表格的情况,算法如下。多头和空头头寸的掉期是分别请求的,所以我们定义了用于名称的配对数组和用于值的数组。它们将一起组合到 swaps
结构体数组中。
c
// 所选模式的掉期汇总表
string buyers[], sellers[]; // 名称数组
double longs[], shorts[]; // 掉期值数组
SymbolSwap swaps[]; // 要打印的总数组
在过滤器中设置所选掉期模式的条件。这对于比较和排序数组元素是必要的。
c
f.let(SYMBOL_SWAP_MODE, Mode);
然后,我们针对不同的属性(SYMBOL_SWAP_LONG
、SYMBOL_SWAP_SHORT
)两次应用过滤器,并使用它们的值填充不同的数组(longs
、shorts
)。在每次调用中,数组都按升序排序。
c
f.select(UseMarketWatch, SYMBOL_SWAP_LONG, buyers, longs, true);
f.select(UseMarketWatch, SYMBOL_SWAP_SHORT, sellers, shorts, true);
理论上,数组的大小应该相同,因为过滤条件是相同的,但为了清晰起见,我们为每个大小分配一个变量。由于每个交易品种在结果表格中会出现两次,分别对应多头和空头头寸,所以我们为 swaps
数组提供两倍的大小。
c
const int l = ArraySize(longs);
const int s = ArraySize(shorts);
const int n = ArrayResize(swaps, l + s); // 应该 l == s
PrintFormat("Total symbols with %s: %d", EnumToString(Mode), l);
接下来,我们将 longs
和 shorts
两个数组合并,以逆序处理它们,因为我们需要从正值到负值进行排序。
c
if(n > 0)
{
int i = l - 1, j = s - 1, k = 0;
while(k < n)
{
const double swapLong = i >= 0? longs[i] : -DBL_MAX;
const double swapShort = j >= 0? shorts[j] : -DBL_MAX;
if(swapLong >= swapShort)
{
swaps[k].name = "+" + buyers[i];
swaps[k].value = longs[i];
--i;
++k;
}
else
{
swaps[k].name = "-" + sellers[j];
swaps[k].value = shorts[j];
--j;
++k;
}
}
Print("Swaps per symbols (ordered):");
ArrayPrint(swaps);
}
多次使用不同的设置运行该脚本会很有趣。例如,默认情况下,我们可以得到以下结果。
===== Swap modes for Market Watch symbols =====
Total symbols: 13
Stats per swap mode:
[key] [value]
[0] 1 10
[1] 0 2
[2] 2 1
Legend: key=swap mode, value=count
1 -> SYMBOL_SWAP_MODE_POINTS
0 -> SYMBOL_SWAP_MODE_DISABLED
2 -> SYMBOL_SWAP_MODE_CURRENCY_SYMBOL
这些统计信息表明,10 个交易品种的掉期模式为 SYMBOL_SWAP_MODE_POINTS
,2 个交易品种的掉期被禁用(SYMBOL_SWAP_MODE_DISABLED
),1 个交易品种的掉期以基础货币计算(SYMBOL_SWAP_MODE_CURRENCY_SYMBOL
)。
让我们找出哪些交易品种具有 SYMBOL_SWAP_MODE_POINTS
模式,并了解它们的掉期情况。为此,我们将 ShowPerSymbolDetails
设置为 true
(mode
参数已设置为 SYMBOL_SWAP_MODE_POINTS
)。
===== Swap modes for Market Watch symbols =====
Total symbols with SYMBOL_SWAP_MODE_POINTS: 10
Swaps per symbols (ordered):
[name] [value]
[ 0] "+AUDUSD" 6.30000
[ 1] "+NZDUSD" 2.80000
[ 2] "+USDCHF" 0.10000
[ 3] "+USDRUB" 0.00000
[ 4] "-USDRUB" 0.00000
[ 5] "+USDJPY" -0.10000
[ 6] "+GBPUSD" -0.20000
[ 7] "-USDCAD" -0.40000
[ 8] "-USDJPY" -0.60000
[ 9] "+EURUSD" -0.70000
[10] "+USDCAD" -0.80000
[11] "-EURUSD" -1.00000
[12] "-USDCHF" -1.00000
[13] "-GBPUSD" -2.20000
[14] "+USDSEK" -4.50000
[15] "-XAUUSD" -4.60000
[16] "-USDSEK" -4.90000
[17] "-NZDUSD" -6.70000
[18] "+XAUUSD" -12.60000
[19] "-AUDUSD" -14.80000
你可以将这些值与交易品种规格进行比较。
最后,我们将 Mode
更改为 SYMBOL_SWAP_MODE_CURRENCY_SYMBOL
。在我们的例子中,应该得到一个交易品种,但会分成两行显示:名称中带有加号和减号。
===== Swap modes for Market Watch symbols =====
Total symbols with SYMBOL_SWAP_MODE_CURRENCY_SYMBOL: 1
Swaps per symbols (ordered):
[name] [value]
[0] "-SP500m" -35.00000
[1] "+SP500m" -41.41000
从表格中可以看出,两个掉期都是负值。
当前市场信息(报价)
在“获取交易品种的最新报价”这一部分中,我们已经了解了 SymbolInfoTick
函数,它以 MqlTick
结构体的形式提供有关最新报价(价格变动事件)的完整信息。如果有必要,MQL 程序可以分别请求与该结构体字段相对应的价格和交易量的值。所有这些值都由不同类型的属性表示,这些属性是 ENUM_SYMBOL_INFO_INTEGER
和 ENUM_SYMBOL_INFO_DOUBLE
枚举的一部分。
标识符 | 描述 | 属性类型 |
---|---|---|
SYMBOL_TIME | 最新报价时间 | datetime |
SYMBOL_BID | 卖出价;最佳卖出报价 | double |
SYMBOL_ASK | 买入价;最佳买入报价 | double |
SYMBOL_LAST | 最新成交价;最后一笔交易的价格 | double |
SYMBOL_VOLUME | 最后一笔交易的交易量 | long |
SYMBOL_TIME_MSC | 自 1970 年 1 月 1 日以来,最新报价的时间(以毫秒为单位) | long |
SYMBOL_VOLUME_REAL | 最后一笔交易的更高精度交易量 | double |
请注意,与两个交易量相关的属性 SYMBOL_VOLUME
和 SYMBOL_VOLUME_REAL
在两个枚举中的代码是相同的。这是不同枚举的元素标识符重叠的唯一情况。原因是它们本质上返回的是相同的报价属性,但表示精度不同。
与结构体不同,属性没有提供与 uint
类型的 flags
字段类似的内容,该字段用于说明是市场中的何种变化导致了报价的生成。这个字段仅在结构体中才有意义。
让我们尝试分别请求报价属性,并将它们与 SymbolInfoTick
函数调用的结果进行比较。在快速变化的市场中,结果可能会有所不同。在函数调用之间可能会出现新的报价(甚至是几个报价)。
c
void OnStart()
{
PRTF(TimeToString(SymbolInfoInteger(_Symbol, SYMBOL_TIME), TIME_DATE | TIME_SECONDS));
PRTF(SymbolInfoDouble(_Symbol, SYMBOL_BID));
PRTF(SymbolInfoDouble(_Symbol, SYMBOL_ASK));
PRTF(SymbolInfoDouble(_Symbol, SYMBOL_LAST));
PRTF(SymbolInfoInteger(_Symbol, SYMBOL_VOLUME));
PRTF(SymbolInfoInteger(_Symbol, SYMBOL_TIME_MSC));
PRTF(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_REAL));
MqlTick tick[1];
SymbolInfoTick(_Symbol, tick[0]);
ArrayPrint(tick);
}
很容易验证,在特定情况下,信息是一致的。
TimeToString(SymbolInfoInteger(_Symbol,SYMBOL_TIME),TIME_DATE|TIME_SECONDS)
=2022.01.25 13:52:51 / ok
SymbolInfoDouble(_Symbol,SYMBOL_BID)=1838.44 / ok
SymbolInfoDouble(_Symbol,SYMBOL_ASK)=1838.49 / ok
SymbolInfoDouble(_Symbol,SYMBOL_LAST)=0.0 / ok
SymbolInfoInteger(_Symbol,SYMBOL_VOLUME)=0 / ok
SymbolInfoInteger(_Symbol,SYMBOL_TIME_MSC)=1643118771166 / ok
SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_REAL)=0.0 / ok
[time] [bid] [ask] [last] [volume] [time_msc] [flags] [volume_real]
[0] 2022.01.25 13:52:51 1838.44 1838.49 0.00 0 1643118771166 6 0.00
交易品种的描述性属性
该平台为 MQL 程序提供了一组文本属性,用于描述重要的定性特征。例如,在基于一篮子金融工具开发指标或交易策略时,可能需要根据来源国、经济部门或基础资产的名称(如果该工具是衍生工具)来选择交易品种。
标识符 | 描述 |
---|---|
SYMBOL_BASIS | 衍生工具的基础资产名称 |
SYMBOL_CATEGORY | 金融工具所属类别的名称 |
SYMBOL_COUNTRY | 金融工具所属的国家 |
SYMBOL_SECTOR_NAME | 金融工具所属的经济部门 |
SYMBOL_INDUSTRY_NAME | 金融工具所属的经济分支或行业类型 |
SYMBOL_BANK | 当前报价来源 |
SYMBOL_DESCRIPTION | 交易品种的字符串描述 |
SYMBOL_EXCHANGE | 交易品种交易的交易所或市场名称 |
SYMBOL_ISIN | 国际证券识别码(ISIN,International Securities Identification Number)系统中唯一的 12 位字母数字代码 |
SYMBOL_PAGE | 包含该交易品种信息的互联网页面地址 |
SYMBOL_PATH | 交易品种树中的路径 |
程序可以应用这些属性进行分析的另一种情况是,在寻找一种货币到另一种货币的兑换率时。我们已经知道如何找到具有合适的基础货币和报价货币组合的交易品种,但困难在于可能存在多个这样的交易品种。在这种情况下,读取 SYMBOL_SECTOR_NAME
(需要寻找 “Currency” 或其同义词,可参考经纪商的规格)或 SYMBOL_PATH
等属性会有所帮助。
SYMBOL_PATH
包含交易品种目录中包含特定交易品种的所有文件夹层次结构:文件夹名称之间用反斜杠(\
)分隔,就像文件系统一样。路径的最后一个元素是交易品种本身的名称。
一些字符串属性有对应的整数属性。特别是,代替 SYMBOL_SECTOR_NAME
,可以使用 SYMBOL_SECTOR
属性,它返回一个包含所有支持部门的 ENUM_SYMBOL_SECTOR
枚举成员。类似地,对于 SYMBOL_INDUSTRY_NAME
,有一个类似的属性 SYMBOL_INDUSTRY
,其枚举类型为 ENUM_SYMBOL_INDUSTRY
。
如果需要,MQL 程序甚至可以通过简单地读取 SYMBOL_BACKGROUND_COLOR
属性,找到在市场报价中显示交易品种时使用的背景颜色。这将使那些使用图形对象(对话框、列表等)在图表上创建自己界面的程序,能够使其界面与原生终端控件保持一致。
我们来看示例脚本 SymbolFilterDescription.mq5
,它输出市场报价中交易品种的四个预定义文本属性。第一个是 SYMBOL_DESCRIPTION
(不要与交易品种本身的名称混淆),并且结果列表将根据它进行排序。另外三个纯粹是作为参考:SYMBOL_SECTOR_NAME
、SYMBOL_COUNTRY
、SYMBOL_PATH
。对于每个经纪商,所有值都以特定方式填充(对于相同的行情代码可能存在差异)。
我们之前没有提到,但我们的 SymbolFilter
类实现了 equal
方法的特殊重载来比较字符串。它支持搜索带有模式的子字符串,其中通配符 *
代表 0 个或多个任意字符。例如,*ian*
将找到所有包含子字符串 “ian”(在任何位置)的交易品种,而 *Index
将只找到以 “Index” 结尾的字符串。
这个功能类似于用户可用的 “交易品种” 对话框中的子字符串搜索。然而,不需要指定通配符,因为总是搜索子字符串。在源代码(SymbolFilter.mqh
)中的算法中,我们保留了搜索完全匹配(没有 *
字符)或子字符串(至少有一个星号)的可能性。
比较是区分大小写的。如果需要,很容易修改代码以实现不区分大小写的比较。
考虑到这个新功能,我们在交易品种描述的搜索字符串中定义一个输入变量。如果该变量为空,将显示市场报价窗口中的所有交易品种。
c
input string SearchPattern = "";
接下来,一切都和往常一样。
c
void OnStart()
{
SymbolFilter f; // 过滤对象
string symbols[]; // 名称数组
string text[][4]; // 包含数据向量的数组
// 要读取的属性
ENUM_SYMBOL_INFO_STRING fields[] =
{
SYMBOL_DESCRIPTION,
SYMBOL_SECTOR_NAME,
SYMBOL_COUNTRY,
SYMBOL_PATH
};
if(SearchPattern != "")
{
f.let(SYMBOL_DESCRIPTION, SearchPattern);
}
// 应用过滤器并获取按描述排序的数组
f.select(true, fields, symbols, text, true);
const int n = ArraySize(symbols);
PrintFormat("===== Text fields for symbols (%d) =====", n);
for(int i = 0; i < n; ++i)
{
Print(symbols[i] + ":");
ArrayPrint(text, 0, NULL, i, 1, 0);
}
}
以下是可能的列表版本(有缩写)。
===== Text fields for symbols (16) =====
AUDUSD:
"Australian Dollar vs US Dollar" "Currency" "" "Forex\AUDUSD"
EURUSD:
"Euro vs US Dollar" "Currency" "" "Forex\EURUSD"
UK100:
"FTSE 100 Index" "Undefined" "" "Indexes\UK100"
XAUUSD:
"Gold vs US Dollar" "Commodities" "" "Metals\XAUUSD"
JAGG:
"JPMorgan U.S. Aggregate Bond ETF" "Financial"
"USA" "ETF\United States\NYSE\JPMorgan\JAGG"
NZDUSD:
"New Zealand Dollar vs US Dollar" "Currency" "" "Forex\NZDUSD"
GBPUSD:
"Pound Sterling vs US Dollar" "Currency" "" "Forex\GBPUSD"
SP500m:
"Standard & Poor's 500" "Undefined" "" "Indexes\SP500m"
FIHD:
"UBS AG FI Enhanced Global High Yield ETN" "Financial"
"USA" "ETF\United States\NYSE\UBS\FIHD"
...
如果我们在输入变量 SearchPattern
中输入搜索字符串 *ian*
,我们会得到以下结果。
===== Text fields for symbols (3) =====
AUDUSD:
"Australian Dollar vs US Dollar" "Currency" "" "Forex\AUDUSD"
USDCAD:
"US Dollar vs Canadian Dollar" "Currency" "" "Forex\USDCAD"
USDRUB:
"US Dollar vs Russian Ruble" "Currency" "" "Forex\USDRUB"
市场深度
当涉及到交易所交易工具时,MetaTrader 5 不仅允许获取打包在报价(tick)中的价格和成交量信息,还能获取市场深度(订单簿,二级价格),也就是在当前价格附近几个最近价位上已挂出的买入和卖出订单的成交量分布情况。品种的整数属性之一 SYMBOL_TICKS_BOOKDEPTH
包含了市场深度中显示的最大价位数量。这个数量对买卖双方各自有效,也就是说订单簿的总规模可能是该数值的两倍(并且这还不包括未广播的成交量为零的价位)。
根据市场情况,实际传输的订单簿规模可能会小于该属性中指示的规模。对于非交易所交易工具,这个属性通常等于 0,不过一些经纪商也可能会广播外汇品种的订单簿,只是受限于其客户的订单情况。
订单簿本身以及关于其更新的通知必须由感兴趣的 MQL 程序使用特殊的 API 来请求,我们将在下一章讨论这个 API。
需要注意的是,由于平台的架构特点,这个属性与订单簿的传输没有直接关系,也就是说它只是经纪商填写的一个规格字段。换句话说,该属性的非零值并不意味着在公开市场上订单簿一定会传输到终端。这取决于其他服务器设置以及是否与数据提供商建立了有效的连接。
让我们尝试使用脚本 SymbolFilterBookDepth.mq5
来获取所有或选定品种的市场深度统计信息。
c
input bool UseMarketWatch = false;
input int ShowSymbolsWithDepth = -1;
参数 ShowSymbolsWithDepth
默认值为 -1,它指示在所有品种中收集不同市场深度设置的统计信息。如果你将该参数设置为其他值,程序将尝试查找所有具有指定订单簿深度的品种。
c
void OnStart()
{
SymbolFilter f; // 筛选器对象
string symbols[]; // 用于存储品种名称的数组
long depths[]; // 属性值数组
MapArray<long,int> stats; // 每个深度出现次数的计数器
if(ShowSymbolsWithDepth > -1)
{
f.let(SYMBOL_TICKS_BOOKDEPTH, ShowSymbolsWithDepth);
}
// 应用筛选器并填充数组
f.select(UseMarketWatch, SYMBOL_TICKS_BOOKDEPTH, symbols, depths, true);
const int n = ArraySize(symbols);
PrintFormat("===== Book depths for %s symbols %s=====",
(UseMarketWatch ? "Market Watch" : "all available"),
(ShowSymbolsWithDepth > -1 ? "(filtered by depth="
+ (string)ShowSymbolsWithDepth + ") " : ""));
PrintFormat("Total symbols: %d", n);
...
if(ShowSymbolsWithDepth > -1)
{
ArrayPrint(symbols);
return;
}
...
for(int i = 0; i < n; ++i)
{
stats.inc(depths[i]);
}
Print("Stats per depth:");
stats.print();
Print("Legend: key=depth, value=count");
}
使用默认设置时,我们可以得到以下结果。
plaintext
===== Book depths for all available symbols =====
Total symbols: 52357
Stats per depth:
[key] [value]
[0] 0 52244
[1] 5 3
[2] 10 67
[3] 16 5
[4] 20 13
[5] 32 25
Legend: key=depth, value=count
如果你将 ShowSymbolsWithDepth
设置为检测到的值之一,例如 32,我们会得到具有该订单簿深度的品种列表。
plaintext
===== Book depths for all available symbols (filtered by depth=32) =====
Total symbols: 25
[ 0] "USDCNH" "USDZAR" "USDHUF" "USDPLN" "EURHUF" "EURNOK" "EURPLN" "EURSEK" "EURZAR" "GBPNOK" "GBPPLN" "GBPSEK" "GBPZAR"
[13] "NZDCAD" "NZDCHF" "USDMXN" "EURMXN" "GBPMXN" "CADMXN" "CHFMXN" "MXNJPY" "NZDMXN" "USDCOP" "USDARS" "USDCLP"
自定义交易品种属性
在本章的引言中,我们提到了自定义交易品种。这些交易品种是用户通过终端命令或编程方式直接在终端中创建报价的品种。
例如,自定义交易品种可用于基于包含其他市场报价窗口中交易品种的公式来创建合成工具。用户可以直接在终端界面中使用此功能。
MQL程序在MQL5中能实现更复杂的场景,比如合并不同周期的不同交易品种、按照给定的随机分布生成序列,或者从外部源获取数据(报价、K线或 tick 数据)。
为了能在算法中区分标准交易品种和自定义交易品种,MQL5提供了 SYMBOL_CUSTOM
属性,它是一个逻辑标志,用于表明某个交易品种是否为自定义的。
如果交易品种有公式,可通过 SYMBOL_FORMULA
字符串属性获取。如你所知,在公式里可以使用其他交易品种的名称,以及数学函数和运算符。以下是一些示例:
- 合成交易品种:
"@ESU19"/EURCAD
- 日历价差:
"Si - 9.13"-"Si - 6.13"
- 欧元指数:
34.38805726 * pow(EURUSD,0.3155) * pow(EURGBP,0.3056) * pow(EURJPY,0.1891) * pow(EURCHF,0.1113) * pow(EURSEK,0.0785)
为用户指定公式很方便,但MQL程序通常不会使用,因为它们可以在代码中直接计算公式,使用非标准函数并能进行更多控制,特别是可以在每个 tick 上进行计算,而不是每100毫秒通过定时器计算一次。
下面我们来看看 SymbolFilterCustom.mq5
脚本对属性的操作:它会记录所有自定义交易品种及其公式(如果有的话)。
c++
input bool UseMarketWatch = false;
void OnStart()
{
SymbolFilter f; // 过滤器对象
string symbols[]; // 用于存储交易品种名称的数组
string formulae[]; // 用于存储公式的数组
// 应用过滤器并填充数组
f.let(SYMBOL_CUSTOM, true)
.select(UseMarketWatch, SYMBOL_FORMULA, symbols, formulae);
const int n = ArraySize(symbols);
PrintFormat("===== %s custom symbols =====",
(UseMarketWatch ? "Market Watch" : "All available"));
PrintFormat("Total symbols: %d", n);
for(int i = 0; i < n; ++i)
{
Print(symbols[i], " ", formulae[i]);
}
}
以下是仅找到一个自定义交易品种时的运行结果:
===== All available custom symbols =====
Total symbols: 1
synthEURUSD SP500m/UK100
特定属性(证券交易所、衍生品、债券)
在本章的最后部分,我们将简要回顾一些本书范围之外的其他交易品种属性,不过这些属性对于实施高级交易策略可能会很有用。关于这些属性的详细信息,可以在MQL5文档中找到。
如你所知,MetaTrader 5 允许你交易衍生品市场的交易品种,包括期权、期货和债券。这在软件界面中也有所体现。MQL5 API 提供了许多与上述交易品种类别相关的特定交易品种属性。
特别是对于期权而言,这些属性包括流通期(交易开始日期 SYMBOL_START_TIME 和结束日期 SYMBOL_EXPIRATION_TIME)、行权价格(SYMBOL_OPTION_STRIKE)、买卖权利(SYMBOL_OPTION_RIGHT,看涨期权/看跌期权)、根据提前行权可能性区分的欧式或美式类型(SYMBOL_OPTION_MODE)、收盘价的每日变化(SYMBOL_PRICE_CHANGE)和波动率(SYMBOL_PRICE_VOLATILITY),以及表征价格行为动态的估计系数(希腊字母指标)。
对于债券来说,累计票面利息收入(SYMBOL_TRADE_ACCRUED_INTEREST)、面值(SYMBOL_TRADE_FACE_VALUE)、流动性比率(SYMBOL_TRADE_LIQUIDITY_RATE)特别值得关注。
对于期货而言,未平仓合约量(SYMBOL_SESSION_INTEREST)以及买入(SYMBOL_SESSION_BUY_ORDERS_VOLUME)和卖出(SYMBOL_SESSION_SELL_ORDERS_VOLUME)的总订单量,还有交易时段结束时的结算价格(SYMBOL_SESSION_PRICE_SETTLEMENT)是重要属性。
除了构成报价的当前市场数据外,MQL5 还允许你了解它们的每日波动范围:即每个报价字段的最大值和最小值。例如,SYMBOL_BIDHIGH 是当天的最高买价,而 SYMBOL_BIDLOW 是最低买价。请注意,SYMBOL_VOLUMEHIGH、SYMBOL_VOLUMELOW(长整型)属性实际上是对 SYMBOL_VOLUMEHIGH_REAL 和 SYMBOL_VOLUMELOW_REAL(双精度型)中交易量的重复,只是精度较低。
一般来说,关于最新价格和交易量的信息仅对交易所交易品种可用。
请记住,这些属性的填充取决于经纪商所实施的服务器设置。