数学函数
几乎所有现代编程语言都提供了一些最常用的数学函数,MQL5 也不例外。在本章中,我们将介绍几组 MQL5 自带的函数。这些函数包括取整函数、三角函数、双曲函数、指数函数、对数函数、幂函数,以及一些特殊函数,例如生成随机数函数和检查实数是否为正常数的函数。
大多数函数都有两个名称:全称(带有 “Math” 前缀且首字母大写)和简称(无前缀且全部小写)。我们会给出这两种形式,它们的功能是相同的。你可以根据源代码的格式风格来选择使用哪一种。
由于数学函数会进行一些计算并返回一个实数结果,潜在的错误可能会导致结果无法确定。例如,你不能对负数求平方根,也不能对零取对数。在这种情况下,函数会返回特殊值,即非数字值(NaN,Not A Number)。我们在 “实数”、“算术运算” 以及 “数字与字符串的转换” 等部分已经遇到过这些特殊值。可以使用 MathIsValidNumber
和 MathClassify
函数来分析数字的正确性和是否存在错误(请参考 “检查实数是否为正常数” 部分)。
如果至少有一个操作数的值为 NaN,那么任何涉及该操作数的后续计算(包括函数调用)也会得到 NaN 结果。
为了便于自学和获得直观的材料,你可以使用附件中的 MathPlot.mq5
脚本,它可以显示我们所描述的一元数学函数的图像。该脚本使用了 MetaTrader 5 中提供的标准绘图库 Graphic.mqh
(本书不涉及该库的详细内容)。下面是一个双曲正弦曲线在 MetaTrader 5 窗口中可能的显示示例。
数的绝对值
MQL5 应用程序编程接口(API)提供了 MathAbs
函数,如果一个数带有负号,该函数可以将其去掉。因此,无需手动编写像下面这样冗长的等效代码:
if(x < 0) x = -x;
numeric MathAbs(numeric value) ≡ numeric fabs(numeric value)
该函数返回传递给它的数的绝对值,即其模。参数可以是任何类型的数。换句话说,该函数针对 char/uchar
、short/ushort
、int/uint
、long/ulong
、float
和 double
类型进行了重载,不过对于无符号类型,其值始终是非负的。
当传递一个字符串时,它会被隐式转换为 double
类型的数字,并且编译器会生成一个相关的警告。
返回值的类型始终与参数的类型相同,因此,如果接收变量的类型与返回值类型不同,编译器可能需要将返回值强制转换为接收变量的类型。
在 MathAbs.mq5
文件中可以找到该函数的使用示例:
void OnStart()
{
double x = 123.45;
double y = -123.45;
int i = -1;
PRT(MathAbs(x)); // 123.45,数字保持“原样”
PRT(MathAbs(y)); // 123.45,负号被去掉
PRT(MathAbs(i)); // 1,整数能正常处理
int k = MathAbs(i); // 无警告:参数和结果的类型均为 int
// 会产生警告的情况:
// 需要将 double 类型转换为 long 类型
long j = MathAbs(x); // 由于类型转换可能会导致数据丢失
// 需要从大类型(4 字节)转换为小类型(2 字节)
short c = MathAbs(i); // 由于类型转换可能会导致数据丢失
...
需要注意的是,将有符号整数转换为无符号整数并不等同于取一个数的模:
uint u_cast = i;
uint u_abs = MathAbs(i);
PRT(u_cast); // 4294967295, 0xFFFFFFFF
PRT(u_abs); // 1
还要注意,数字 0 可以有符号:
...
double n = 0;
double z = i * n;
PRT(z); // -0.0
PRT(MathAbs(z)); // 0.0
PRT(z == MathAbs(z)); // true
}
使用 MathAbs
的一个最佳示例是测试两个实数是否相等。众所周知,实数表示数值的精度是有限的,在冗长的计算过程中,这种精度可能会进一步降低(例如,十个 0.1 的和并不恰好等于 1.0)。在大多数情况下,严格的条件 value1 == value2
会返回 false
,而实际上从理论上来说它们应该是相等的。
因此,为了比较实数值,通常使用以下表示方法:
MathAbs(value1 - value2) < EPS
其中 EPS
是一个小的正值,表示精度(请参考“比较运算”部分的示例)。
两个数的最大值和最小值
为了从两个数中找出最大或最小的数,MQL5 提供了 MathMax
和 MathMin
函数。它们的简称分别是 fmax
和 fmin
。
函数原型
numeric MathMax(numeric value1, numeric value2) ≡ numeric fmax(numeric value1, numeric value2)
numeric MathMin(numeric value1, numeric value2) ≡ numeric fmin(numeric value1, numeric value2)
这两个函数会返回传入的两个值中的最大值或最小值。这些函数针对所有内置类型进行了重载。
类型转换规则
如果向函数传递不同类型的参数,“较低”类型的参数会自动转换为“较高”类型。例如,对于 int
和 double
这对类型,int
会被转换为 double
。有关隐式类型转换的更多信息,请参阅“算术类型转换”部分。返回类型对应于“最高”类型。
如果有一个参数是 string
类型,它将是“高级”类型,即所有参数都会转换为字符串。字符串会像 StringCompare
函数那样按字典顺序进行比较。
示例脚本
MathMaxMin.mq5
脚本展示了这些函数的实际使用:
void OnStart()
{
int i = 10, j = 11;
double x = 5.5, y = -5.5;
string s = "abc";
// 数字比较
PRT(MathMax(i, j)); // 输出 11,因为 11 大于 10
PRT(MathMax(i, x)); // 输出 10,这里先将 i 转换为 double 类型,但 10.0 大于 5.5
PRT(MathMax(x, y)); // 输出 5.5,5.5 大于 -5.5
PRT(MathMax(i, s)); // 输出 abc,因为 s 是 string 类型,按规则将 i 转换为字符串比较,字符串比较中 "abc" 更“大”
// 类型转换检查
PRT(typename(MathMax(i, j))); // 输出 int,因为两个参数都是 int 类型,返回类型不变
PRT(typename(MathMax(i, x))); // 输出 double,因为 int 会转换为 double 类型,返回类型为 double
PRT(typename(MathMax(i, s))); // 输出 string,因为 s 是 string 类型,返回类型为 string
}
这个示例清晰地展示了 MathMax
函数在不同类型参数下的行为,包括如何根据类型规则进行类型转换以及返回相应类型的结果。MathMin
函数的工作原理与之类似,只是它返回的是两个数中的最小值。
取整函数
MQL5 应用程序编程接口(API)包含多个用于将数字向最接近的整数进行取整的函数(可向不同方向取整)。尽管是取整操作,但所有这些函数都返回 double
类型的数字(小数部分为空)。
从技术角度来看,这些函数可以接受任何数值类型的参数,但只有实数会被取整,整数则只是被转换为 double
类型。
如果你想将数字精确到特定的小数位数,可以使用 NormalizeDouble
函数(请参考“双精度数的规范化”部分)。
在 MathRound.mq5
文件中给出了这些函数的使用示例。
四舍五入函数
double MathRound(numeric value) ≡ double round(numeric value)
该函数将一个数字向上或向下舍入到最接近的整数。
PRT((MathRound(5.5))); // 输出 6.0
PRT((MathRound(-5.5))); // 输出 -6.0
PRT((MathRound(11))); // 输出 11.0
PRT((MathRound(-11))); // 输出 -11.0
如果小数部分的值大于或等于 0.5,尾数会增加 1(无论数字的符号如何)。
向上取整和向下取整函数
double MathCeil(numeric value) ≡ double ceil(numeric value)
double MathFloor(numeric value) ≡ double floor(numeric value)
MathCeil
函数返回大于或等于传入值的最接近的整数值;MathFloor
函数返回小于或等于传入值的最接近的整数值。如果传入的值本身就是整数(小数部分为 0),则直接返回该整数。
PRT((MathCeil(5.5))); // 输出 6.0
PRT((MathCeil(-5.5))); // 输出 -5.0
PRT((MathFloor(5.5))); // 输出 5.0
PRT((MathFloor(-5.5))); // 输出 -6.0
PRT((MathCeil(11))); // 输出 11.0
PRT((MathCeil(-11))); // 输出 -11.0
PRT((MathFloor(11))); // 输出 11.0
PRT((MathFloor(-11))); // 输出 -11.0
这些取整函数在处理数值时非常有用,特别是在需要对数值进行标准化处理或者需要将浮点数转换为整数的场景中。不同的取整方式可以根据具体的业务需求进行选择。
除法取余(模运算)
在 MQL5 里,若要进行整数的带余除法,可使用内置的取模运算符 %
,这在“算术运算”部分有相关介绍。不过,该运算符不适用于实数。当除数、被除数或者两个操作数均为实数时,就需要使用 MathMod
函数(或者其简称 fmod
)。
函数原型
double MathMod(double dividend, double divider) ≡ double fmod(double dividend, double divider)
此函数会返回第一个传入的数(被除数)除以第二个数(除数)后的实余数。
若任一参数为负数,结果的符号由前面章节所描述的规则来确定。
示例脚本
在 MathMod.mq5
脚本中能找到该函数的使用示例:
PRT(MathMod(10.0, 3)); // 输出 1.0
PRT(MathMod(10.0, 3.5)); // 输出 3.0
PRT(MathMod(10.0, 3.49)); // 输出 3.02
PRT(MathMod(10.0, M_PI)); // 输出 0.5752220392306207
PRT(MathMod(10.0, -1.5)); // 输出 1.0,符号被去除
PRT(MathMod(-10.0, -1.5)); // 输出 -1.0
这些示例清晰展示了 MathMod
函数在不同参数组合下的工作情况,尤其是在处理实数除法取余以及负数参数时的表现。该函数为处理实数的模运算提供了便利,弥补了取模运算符 %
只能用于整数的不足。
幂运算和开方运算
MQL5 应用程序编程接口(API)提供了一个通用函数 MathPow
,用于将一个数提升到任意次幂,同时还提供了一个特殊情况的函数 MathSqrt
,当幂为 0.5 时,它就是我们更为熟悉的开平方根运算。
要测试这些函数,可以使用 MathPowSqrt.mq5
脚本。
幂运算函数
double MathPow(double base, double exponent) ≡ double pow(double base, double exponent)
该函数将底数 base
提升到指定的指数 exponent
次幂。
PRT(MathPow(2.0, 1.5)); // 输出 2.82842712474619
PRT(MathPow(2.0, -1.5)); // 输出 0.3535533905932738
PRT(MathPow(2.0, 0.5)); // 输出 1.414213562373095
开平方根函数
double MathSqrt(double value) ≡ double sqrt(double value)
该函数返回一个数的平方根。
PRT(MathSqrt(2.0)); // 输出 1.414213562373095
PRT(MathSqrt(-2.0)); // 输出 -nan(ind),因为不能对负数开平方根
MQL5 定义了几个常量,这些常量包含了涉及平方根的预计算值:
常量 | 描述 | 值 |
---|---|---|
M_SQRT2 | \(\sqrt\) | 1.41421356237309504880 |
M_SQRT1_2 | \(1 / \sqrt\) | 0.707106781186547524401 |
M_2_SQRTPI | \(2.0 / \sqrt{\pi}\)(其中 \(\pi\) 为圆周率,M_PI ) | 1.12837916709551257390 |
这里的 M_PI
是圆周率 \(\pi\) 的值(\(\pi = 3.14159265358979323846\),更多关于圆周率的内容可参考“三角函数”部分)。
所有内置常量都在文档中有详细描述。
这些函数和常量为在 MQL5 中进行幂运算和开方运算提供了便利,无论是处理一般的幂次还是特定的平方根运算,都能轻松实现。
指数函数和对数函数
在 MQL5 里,可借助相应的 API 部分来计算指数函数和对数函数。
虽然 API 中没有提供计算机科学和组合数学里常用的二进制对数函数,但这并非难题,因为可以根据需要,通过现有的自然对数函数或十进制对数函数轻松计算得出。
二进制对数计算公式如下:
- \(\log_2(x) = \frac{\log(x)}{\log(2)} = \frac{\log(x)}\)
- \(\log_2(x) = \frac{\log_{10}(x)}{\log_{10}(2)}\)
这里的 \(\log\) 和 \(\log_{10}\) 分别是可用的自然对数函数(以 \(e\) 为底)和十进制对数函数(以 10 为底),M_LN2
是一个内置常量,其值为 \(\log(2)\)。
以下表格列出了在对数计算中可能会用到的所有常量:
常量 | 描述 | 值 |
---|---|---|
M_E | 自然常数 \(e\) | 2.71828182845904523536 |
M_LOG2E | \(\log_2(e)\) | 1.44269504088896340736 |
M_LOG10E | \(\log_{10}(e)\) | 0.434294481903251827651 |
M_LN2 | \(\ln(2)\) | 0.693147180559945309417 |
M_LN10 | \(\ln(10)\) | 2.30258509299404568402 |
下面这些函数的使用示例都收录在 MathExp.mq5
文件中。
指数函数
double MathExp(double value) ≡ double exp(double value)
该函数返回指数值,也就是自然常数 \(e\)(可通过预定义常量 M_E
获取)的 value
次幂。若发生溢出,函数将返回 inf
(一种表示无穷大的 NaN)。
PRT(MathExp(0.5)); // 输出 1.648721270700128
PRT(MathPow(M_E, 0.5)); // 输出 1.648721270700128
PRT(MathExp(10000.0)); // 输出 inf,NaN
自然对数函数
double MathLog(double value) ≡ double log(double value)
此函数返回传入数字的自然对数。若 value
为负数,函数返回 -nan(ind)
(表示“未定义值”的 NaN);若 value
为 0,函数返回 inf
(表示“无穷大”的 NaN)。
PRT(MathLog(M_E)); // 输出 1.0
PRT(MathLog(10000.0)); // 输出 9.210340371976184
PRT(MathLog(0.5)); // 输出 -0.6931471805599453
PRT(MathLog(0.0)); // 输出 -inf,NaN
PRT(MathLog(-0.5)); // 输出 -nan(ind)
PRT(Log2(128)); // 输出 7
最后一行代码使用了通过 MathLog
实现的二进制对数函数:
double Log2(double value)
{
return MathLog(value) / M_LN2;
}
十进制对数函数
double MathLog10(double value) ≡ double log10(double value)
该函数返回一个数的十进制对数。
PRT(MathLog10(10.0)); // 输出 1.0
PRT(MathLog10(10000.0)); // 输出 4.0
指数减 1 函数
double MathExpm1(double value) ≡ double expm1(double value)
此函数返回表达式 (MathExp(value) - 1)
的值。在经济计算中,当周期数趋近于无穷大时,该函数可用于计算复利方案下单位时间的有效利息(收益或支付)。
PRT(MathExpm1(0.1)); // 输出 0.1051709180756476
对数加 1 函数
double MathLog1p(double value) ≡ double log1p(double value)
该函数返回表达式 MathLog(1 + value)
的值,即它与 MathExpm1
函数的作用相反。
PRT(MathLog1p(0.1)); // 输出 0.09531017980432487
这些指数函数和对数函数为 MQL5 编程中涉及数学计算的场景提供了丰富的工具,无论是处理自然科学中的指数增长问题,还是计算机科学里的对数复杂度分析,都能发挥重要作用。同时,通过常量和函数之间的配合,还能灵活计算出一些 API 未直接提供的对数类型,如二进制对数。
三角函数
MQL5 提供了三个主要的三角函数(MathCos
、MathSin
、MathTan
)以及它们的反函数(MathArccos
、MathArcsin
、MathArctan
)。所有这些函数都使用弧度制的角度进行运算。如果角度是用度来表示的,则需要使用以下公式将其转换为弧度:
radians = degrees * M_PI / 180
这里的 M_PI
是该语言内置的几个与三角函数量(圆周率 \(\pi\) 及其相关衍生值)有关的常量之一。
常量 | 描述 | 值 |
---|---|---|
M_PI | \(\pi\) | 3.14159265358979323846 |
M_PI_2 | \(\frac{\pi}\) | 1.57079632679489661923 |
M_PI_4 | \(\frac{\pi}\) | 0.785398163397448309616 |
M_1_PI | \(\frac{1}\) | 0.318309886183790671538 |
M_2_PI | \(\frac{2}\) | 0.636619772367581343076 |
反正切函数也可以针对由两个坐标 y
和 x
的比值所表示的量进行计算:这个扩展版本被称为 MathArctan2
;与 MathArctan
不同,MathArctan2
能够还原出从 -M_PI
到 +M_PI
的整个圆周范围内的角度,而 MathArctan
的范围仅限于 -M_PI_2
到 +M_PI_2
。
三角函数
double MathCos(double value) ≡ double cos(double value)
double MathSin(double value) ≡ double sin(double value)
这两个函数分别返回传入数字(角度为弧度制)的余弦值和正弦值。
double MathTan(double value) ≡ double tan(double value)
该函数返回传入数字(角度为弧度制)的正切值。
反三角函数
double MathArccos(double value) ≡ double acos(double value)
double MathArcsin(double value) ≡ double asin(double value)
这两个函数分别返回传入数字的反余弦值和反正弦值,即弧度制的角度。如果 x = MathCos(t)
,那么 t = MathArccos(x)
。正弦函数和反正弦函数也有类似的关系,如果 y = MathSin(t)
,那么 t = MathArcsin(y)
。
参数必须在 -1
到 +1
之间。否则,函数将返回 NaN
。
反余弦函数的结果范围是从 0
到 M_PI
,反正弦函数的结果范围是从 -M_PI_2
到 +M_PI_2
。这些指定的范围被称为主值范围,因为这些函数是多值的,即它们的值会周期性地重复。所选择的半个周期完全覆盖了从 -1
到 +1
的定义域。
对于余弦函数,得到的角度位于上半圆,要得到下半圆的对称解,可以通过添加负号,即 t = -t
。对于正弦函数,得到的角度位于右半圆,左半圆的第二个解是 M_PI - t
(如果对于负的 t
也需要得到一个负的附加角度,那么是 -M_PI - t
)。
double MathArctan(double value) ≡ double atan(double value)
该函数返回传入数字的反正切值,即弧度制的角度,范围是从 -M_PI_2
到 +M_PI_2
。
这个函数是 MathTan
的反函数,但有一个需要注意的地方。
请注意,由于正弦和余弦的比值在相对的象限(圆的四分之一)中由于符号的叠加而重复,正切函数的周期比整个圆周的周期小 2 倍。因此,仅靠正切值不足以在从 -M_PI
到 +M_PI
的整个范围内唯一确定原始角度。这可以通过 MathArctan2
函数来完成,在这个函数中,正切由两个独立的分量表示。
double MathArctan2(double y, double x) ≡ double atan2(double y, double x)
该函数以弧度为单位返回一个角度值,该角度的正切值等于两个指定数字(y
轴坐标和 x
轴坐标)的比值。
结果(我们将其表示为 r
)位于从 -M_PI
到 +M_PI
的范围内,并且满足 MathTan(r) = y / x
的条件。
该函数会考虑两个参数的符号,以便确定正确的象限(同时考虑边界条件,即当 x
或 y
等于 0
时,也就是在象限的边界上的情况)。
x >= 0, y >= 0
,0 <= r <= M_PI_2
(第一象限)x < 0, y >= 0
,M_PI_2 < r <= M_PI
(第二象限)x < 0, y < 0
,-M_PI < r < -M_PI_2
(第三象限)x >= 0, y < 0
,-M_PI_2 <= r < 0
(第四象限)
以下是 MathTrig.mq5
脚本中调用三角函数的结果:
void OnStart()
{
PRT(MathCos(1.0)); // 输出 0.5403023058681397
PRT(MathSin(1.0)); // 输出 0.8414709848078965
PRT(MathTan(1.0)); // 输出 1.557407724654902
PRT(MathTan(45 * M_PI / 180.0)); // 输出 0.9999999999999999
PRT(MathArccos(1.0)); // 输出 0.0
PRT(MathArcsin(1.0)); // 输出 1.570796326794897 等于 M_PI_2
PRT(MathArctan(0.5)); // 输出 0.4636476090008061,第一象限
PRT(MathArctan2(1.0, 2.0)); // 输出 0.4636476090008061,第一象限
PRT(MathArctan2(-1.0, -2.0)); // 输出 -2.677945044588987,第三象限
}
这些三角函数在 MQL5 编程中非常有用,可用于处理与几何形状、物理模拟、信号处理等相关的计算任务,例如计算角度、坐标变换等。通过掌握这些函数的使用方法和特性,可以更高效地解决各种实际问题。
双曲函数
MQL5 应用程序编程接口(API)包含了一组直接双曲函数和反双曲函数。
基本双曲函数
double MathCosh(double value) ≡ double cosh(double value)
double MathSinh(double value) ≡ double sinh(double value)
double MathTanh(double value) ≡ double tanh(double value)
这三个基本函数分别计算双曲余弦、双曲正弦和双曲正切。
反双曲函数
double MathArccosh(double value) ≡ double acosh(double value)
double MathArcsinh(double value) ≡ double asinh(double value)
double MathArctanh(double value) ≡ double atanh(double value)
这三个反函数分别计算反双曲余弦、反双曲正弦和反双曲正切。
对于反双曲余弦函数 MathArccosh
,其参数必须大于或等于 +1
。否则,该函数将返回 NaN
。
反双曲正切函数 MathArctanh
的定义域是从 -1
到 +1
。如果参数超出这个范围,函数将返回 NaN
。
双曲函数的使用示例在 MathHyper.mq5
脚本中展示:
void OnStart()
{
PRT(MathCosh(1.0)); // 输出 1.543080634815244
PRT(MathSinh(1.0)); // 输出 1.175201193643801
PRT(MathTanh(1.0)); // 输出 0.7615941559557649
PRT(MathArccosh(0.5)); // 输出 nan,因为参数小于 1
PRT(MathArcsinh(0.5)); // 输出 0.4812118250596035
PRT(MathArctanh(0.5)); // 输出 0.5493061443340549
PRT(MathArccosh(1.5)); // 输出 0.9624236501192069
PRT(MathArcsinh(1.5)); // 输出 1.194763217287109
PRT(MathArctanh(1.5)); // 输出 nan,因为参数大于 1
}
双曲函数在数学和工程的多个领域中都有应用,例如在描述悬链线的形状(双曲余弦函数的应用)、信号处理以及一些物理模型等方面。在 MQL5 编程中,这些函数为处理相关的计算任务提供了有力的工具。通过了解它们的定义、定义域以及使用示例,可以更准确地在代码中运用这些函数来实现具体的功能。
实数的合法性测试
在进行实数计算时,可能会出现异常情况,例如函数超出定义域、得到数学上的无穷大、精度丢失等,此时计算结果可能不再是一个普通的数字,而是一个特殊值,这类特殊值统称为“非数字”(Not A Number,NaN)。
在本书前面的章节中我们已经遇到过这些特殊值。特别是在输出到日志时(参见“数字与字符串的相互转换”部分),它们会以文本标签的形式显示(例如 nan(ind)
、+inf
等)。另外,只要表达式的操作数中有一个是 NaN,整个表达式就会停止正确求值,最终结果也会是 NaN。不过,代表正无穷或负无穷的“非数字”是个例外:如果用其他数除以它们,结果会是零。但也有预期中的特殊情况:如果用无穷大除以无穷大,结果又会是 NaN。
因此,程序能够判断计算中何时出现 NaN 并对这种情况进行特殊处理是很重要的,比如发出错误信号、用可接受的默认值替代,或者使用其他参数重新进行计算(例如降低迭代算法的精度或步长)。
MQL5 中有两个函数可以用来分析实数的合法性:MathIsValidNumber
给出简单的是(true
)或否(false
)的答案,而 MathClassify
则能进行更详细的分类。
从物理层面来看,所有特殊值在数字中都是通过一种特殊的位组合来编码的,这种组合不会用于表示普通数字。当然,double
和 float
类型的编码是不同的。下面我们来深入了解一下 double
类型(因为它比 float
更常用)。
在“嵌套模板”章节中,我们创建了一个 Converter
类,通过联合(union
)将两种不同类型组合起来以实现视图切换。我们可以使用这个类来研究 NaN 的位表示。
为了方便,我们将这个类移到一个单独的头文件 ConverterT.mqh
中。在测试脚本 MathInvalid.mq5
中包含这个 .mqh
文件,并创建一个用于 double
/ulong
类型对的转换器实例(顺序不重要,因为转换器可以双向工作)。
static Converter<ulong, double> NaNs;
NaN 中的位组合是标准化的,我们选取几个用 ulong
常量表示的常用值,看看内置函数对它们的反应。
// 基本的 NaN 值
#define NAN_INF_PLUS 0x7FF0000000000000
#define NAN_INF_MINUS 0xFFF0000000000000
#define NAN_QUIET 0x7FF8000000000000
#define NAN_IND_MINUS 0xFFF8000000000000
// 自定义 NaN 示例
#define NAN_QUIET_1 0x7FF8000000000001
#define NAN_QUIET_2 0x7FF8000000000002
static double pinf = NaNs[NAN_INF_PLUS]; // +无穷大
static double ninf = NaNs[NAN_INF_MINUS]; // -无穷大
static double qnan = NaNs[NAN_QUIET]; // 静默 NaN
static double nind = NaNs[NAN_IND_MINUS]; // -nan(ind)
void OnStart()
{
PRT(MathIsValidNumber(pinf)); // false
PRT(EnumToString(MathClassify(pinf))); // FP_INFINITE
PRT(MathIsValidNumber(nind)); // false
PRT(EnumToString(MathClassify(nind))); // FP_NAN
...
}
正如预期的那样,测试结果符合我们的设想。
下面我们来看 MathIsValidNumber
和 MathClassify
函数的正式描述,然后继续进行测试。
bool MathIsValidNumber(double value)
该函数用于检查实数的合法性。参数可以是 double
或 float
类型。返回 true
表示该数字合法,返回 false
表示“非数字”(NaN 的一种变体)。
ENUM_FP_CLASS MathClassify(double value)
该函数返回实数(double
或 float
类型)的类别,它是枚举类型 ENUM_FP_CLASS
中的一个值:
FP_NORMAL
:正常数字。FP_SUBNORMAL
:小于以规范化形式表示的最小数字的数(例如,对于double
类型,是小于DBL_MIN
(2.2250738585072014e - 308
)的值);存在精度损失。FP_ZERO
:零(正数零或负数零)。FP_INFINITE
:无穷大(正无穷或负无穷)。FP_NAN
:表示所有其他类型的“非数字”(可细分为“静默”和“信号”NaN 家族)。
MQL5 没有提供用于异常机制的警示 NaN,这种 NaN 可以在程序中拦截和响应关键错误。MQL5 中没有这样的机制,所以例如在发生除零错误时,MQL 程序会直接终止运行(从图表中卸载)。
“静默”NaN 有很多种,你可以使用转换器来构造它们,以便在计算算法中区分和处理非标准状态。
下面在 MathInvalid.mq5
中进行一些计算,直观展示如何得到不同类别的数字。
// 对 double 类型的计算
PRT(MathIsValidNumber(0)); // true
PRT(EnumToString(MathClassify(0))); // FP_ZERO
PRT(MathIsValidNumber(M_PI)); // true
PRT(EnumToString(MathClassify(M_PI))); // FP_NORMAL
PRT(DBL_MIN / 10); // 2.225073858507203e - 309
PRT(MathIsValidNumber(DBL_MIN / 10)); // true
PRT(EnumToString(MathClassify(DBL_MIN / 10))); // FP_SUBNORMAL
PRT(MathSqrt(-1.0)); // -nan(ind)
PRT(MathIsValidNumber(MathSqrt(-1.0))); // false
PRT(EnumToString(MathClassify(MathSqrt(-1.0))));// FP_NAN
PRT(MathLog(0)); // -inf
PRT(MathIsValidNumber(MathLog(0))); // false
PRT(EnumToString(MathClassify(MathLog(0)))); // FP_INFINITE
// 对 float 类型的计算
PRT(1.0f / FLT_MIN / FLT_MIN); // inf
PRT(MathIsValidNumber(1.0f / FLT_MIN / FLT_MIN)); // false
PRT(EnumToString(MathClassify(1.0f / FLT_MIN / FLT_MIN))); // FP_INFINITE
我们还可以反向使用转换器:通过 double
值获取其位表示,从而检测“非数字”:
PrintFormat("%I64X", NaNs[MathSqrt(-1.0)]); // FFF8000000000000
PRT(NaNs[MathSqrt(-1.0)] == NAN_IND_MINUS); // true, nind
PrintFormat
函数与 StringFormat
类似,唯一的区别是它会立即将结果打印到日志中,而不是存储到字符串中。
最后,我们来验证“非数字”总是不相等的:
// NaN != NaN 始终为 true
PRT(MathSqrt(-1.0) != MathSqrt(-1.0)); // true
PRT(MathSqrt(-1.0) == MathSqrt(-1.0)); // false
在 MQL5 中,有一种通过将字符串 "nan"
和 "inf"
转换为 double
类型来得到 NaN 或无穷大的方法:
double nan = (double)"nan";
double infinity = (double)"inf";
这些功能和方法能帮助开发者在 MQL5 编程中更好地处理实数计算中的异常情况,确保程序的健壮性和稳定性。
随机数生成
在交易中,许多算法都需要生成随机数。MQL5 提供了两个函数,用于初始化并随后调用伪随机整数生成器。
为了获得更好的“随机性”,可以使用 MetaTrader 5 中可用的 Alglib 库(请参阅 MQL5/Include/Math/Alglib/alglib.mqh)。
void MathSrand(int seed) ≡ void srand(int seed)
该函数设置伪随机整数生成器的初始状态。在启动算法之前,应该调用该函数一次。而随机值本身则应通过顺序调用 MathRand
函数来获取。
通过使用相同的种子值初始化生成器,可以得到可重现的数字序列。种子值并不是从 MathRand
获得的第一个随机数。生成器维护着一些内部状态,在每个时刻(在为获取新的随机数而调用它的间隔期间),该内部状态由一个整数值来表征,程序可以通过内置的 uint
变量 _RandomSeed
访问这个值。正是这个初始状态值由 MathSrand
调用进行设置。
每次调用 MathRand
时,生成器的操作由以下两个公式描述:
\(X_n = T_f(X_p)\)
\(R = G_f(X_n)\)
\(T_f\) 函数被称为转换函数。它根据先前的 \(X_p\) 状态计算生成器的新内部状态 \(X_n\)。
\(G_f\) 函数使用新的内部状态生成另一个“随机”值,MathRand
函数将返回这个值。
在 MQL5 中,这些公式的实现如下(伪代码):
\(T_f\):_RandomSeed = _RandomSeed * 214013 + 2531011
\(G_f\):MathRand = (_RandomSeed >> 16) & 0x7FFF
建议将 GetTickCount
或 TimeLocal
函数作为种子值传递。
int MathRand() ≡ int rand()
该函数返回一个范围在 0 到 32767 之间的伪随机整数。生成的数字序列会根据调用 MathSrand
进行的初始初始化而有所不同。
在 MathRand.mq5
文件中给出了使用生成器的示例。它计算在给定数量的子范围(区间)上生成的数字的分布统计信息。理想情况下,我们应该得到均匀分布。
#define LIMIT 1000 // 尝试次数(生成的数字数量)
#define STATS 10 // 区间数量
int stats[STATS] = {}; // 计算落入区间的统计信息
void OnStart()
{
const int bucket = 32767 / STATS;
// 重置生成器
MathSrand((int)TimeLocal());
// 在循环中重复实验
for(int i = 0; i < LIMIT; ++i)
{
// 获取新的随机数并更新统计信息
stats[MathRand() / bucket]++;
}
ArrayPrint(stats);
}
以下是三次运行的结果示例(每次我们都会得到一个新的序列):
96 93 117 76 98 88 104 124 113 91
110 81 106 88 103 90 105 102 106 109
89 98 98 107 114 90 101 106 93 104
这些函数和示例展示了在 MQL5 中生成伪随机数的基本方法和对生成数字分布的简单统计分析,对于需要随机数的交易算法等应用场景提供了实用的工具和参考。
整数中的字节序控制
在硬件层面上,各种信息系统在内存中表示数字时会使用不同的字节顺序。因此,当将 MQL 程序与“外部世界”集成时,特别是在实现网络通信协议或读写常见格式的文件时,可能需要更改字节顺序。
Windows 计算机采用小端序(从最低有效字节开始),也就是说,在为变量分配的内存单元中,最低字节排在最前面,然后是更高位的字节,依此类推。而大端序(从最高有效字节开始)在互联网上被广泛使用。在这种情况下,内存单元中的第一个字节是高位字节,最后一个字节是低位字节。这种顺序与我们日常生活中“从左到右”书写数字的方式类似。例如,值 1234 以 1 开头表示千位,接着是 2 表示百位,3 表示十位,最后 4 表示个位(低位)。
让我们看看 MQL5 中的默认字节顺序。为此,我们将使用脚本 MathSwap.mq5
。
它描述了一个联合模板,可将整数转换为字节数组:
template<typename T>
union ByteOverlay
{
T value;
uchar bytes[sizeof(T)];
ByteOverlay(const T v) : value(v) { }
void operator=(const T v) { value = v; }
};
这段代码允许直观地将数字划分为字节,并使用数组索引对它们进行枚举。
在 OnStart
函数中,我们定义一个 uint
变量,其值为 0x12345678
(请注意,这些数字是十六进制的;在这种表示法中,它们恰好对应字节边界:每两位是一个单独的字节)。我们将这个数字转换为数组并输出到日志中。
void OnStart()
{
const uint ui = 0x12345678;
ByteOverlay<uint> bo(ui);
ArrayPrint(bo.bytes); // 120 86 52 18 <==> 0x78 0x56 0x34 0x12
...
ArrayPrint
函数不能以十六进制打印数字,所以我们看到的是它们的十进制表示,但很容易将它们转换为十六进制并确认它们与原始字节匹配。直观地看,它们的顺序是相反的:即数组中索引为 0 的位置是 0x78
,然后是 0x56
、0x34
和 0x12
。显然,这种顺序是从最低有效字节开始的(确实,我们处于 Windows 环境中)。
现在让我们熟悉一下 MQL5 提供的用于更改字节顺序的函数 MathSwap
。
integer MathSwap(integer value)
该函数返回一个整数,其中传入参数的字节顺序被反转。该函数接受 ushort
/uint
/ulong
类型的参数(即大小分别为 2、4、8 字节)。
让我们测试一下这个函数的实际效果:
const uint ui = 0x12345678;
PrintFormat("%I32X -> %I32X", ui, MathSwap(ui));
const ulong ul = 0x0123456789ABCDEF;
PrintFormat("%I64X -> %I64X", ul, MathSwap(ul));
结果如下:
12345678 -> 78563412
123456789ABCDEF -> EFCDAB8967452301
让我们尝试在使用 MathSwap
转换值 0x12345678
后记录字节数组:
bo = MathSwap(ui); // 将 MathSwap 的结果放入 ByteOverlay
ArrayPrint(bo.bytes); // 18 52 86 120 <==> 0x12 0x34 0x56 0x78
在索引为 0 的字节中,原来的值是 0x78
,现在变成了 0x12
,其他索引位置的元素值也进行了交换。
通过了解 MQL5 中的字节序以及 MathSwap
函数的使用方法,开发者在处理与外部系统交互时,能够正确地处理不同字节序带来的问题,确保数据的正确传输和处理。