![通过应用程序了解MQL5中的函数](https://c.mql5.com/2/56/understanding-functions_600x314.jpg)
通过应用程序了解MQL5中的函数
概述
在编程世界里,有一个非常流行的术语,我们经常使用和听到它在任何软件中的重要性,那就是函数。在本文中,我们将详细了解它,以了解如何创建功能强大且高质量的软件。我们将尝试涵盖这一重要主题,以了解什么是函数,为什么我们需要使用函数,以及如何在应用程序中使用它们。之后,我们将分享一些可以在任何交易系统中使用的简单函数作为示例,以应用我们将通过本文学到的内容。以下几点是我们将在本文中分享的主要观点,以涵盖这个有趣的主题:
函数定义
在这一部分中,我们将确定编程中的函数、它们的类型以及为什么需要使用它们。函数是由一个明确而有意义的名称声明的代码块,通过反复调用它来执行特定任务,该代码块可以在软件的任何其他部分中使用。如果我们有一个特定的任务,我们需要软件在软件的许多部分或许多软件中执行,我们创建一个函数或代码块来执行这个任务,然后只在这些部分调用它,而不重新重写完整的代码,所以,我们可以说,函数是一种抽象代码的方法,而不会有很多混乱的代码,就像我们将在本文中看到的那样。我们有两种主要类型的函数,我们有内置函数和用户定义函数。内置函数是编程语言本身现成的函数,而用户定义函数是用户可以根据自己的需求或需要软件执行的任务创建的函数。本文将重点介绍用户定义函数。因此,我们将详细了解这种类型,以了解为什么我们需要使用这种类型的函数,以及使用它们的重要性或功能。
假设我们需要软件来在净值达到最大回撤时执行关闭所有未结订单的任务,并且我们需要在软件的许多部分中执行此任务,最好在这里创建一个函数,并包含执行此任务所需的所有代码或逻辑,然后在其他部分调用此函数,但在许多部分中编写和重复相同的代码来执行此任务是不好的,或者会不堪重负。
如果你问我们为什么需要使用这种类型的函数,这个问题的答案将引导我们学习使用用户定义函数的功能,如下所示:
- 它有助于应用DRY的概念(do not repeat yourself, 不要重复自己):通过使用用户定义的函数将帮助我们不要一遍又一遍地重复相同的代码,但我们将创建一个函数,可以执行我们的任务一次,然后在软件中的任何合适的部分调用它。
- 可重用性:创建我们的函数后,我们可以随时重用它。
- 这有助于应用分而治之的概念:当我们创建软件时,解决问题的代码可能很复杂,但如果我们将大问题划分为小问题,并通过函数解决每个问题,这将非常有助于实现我们解决大问题的目标。
- 这有助于代码更具可读性和可理解性:当我们使用函数时,它有助于使我们的代码可读,因为它变得更有组织性,因为它有函数,每个人都处理特定的问题和特定的任务。
- 它有助于应用抽象的概念:使用函数提供了一种抽象代码的方法,因为如果我们不使用它们,我们可能会发现我们需要编写比使用函数更多的代码行。
- 它有助于应用封装的概念:当我们使用函数时,它允许我们比不使用它们更安全和管理我们的代码和数据。
- 它改进了调试过程:当我们使用函数时,它有助于改进错误,更容易地探索和解决它们。
根据我们提到的使用函数的特点,我们可以很容易地发现,当我们在软件中使用这些用户定义的函数时,会有多大的好处。
函数结构
在这一部分中,我们将了解更多关于函数和函数结构的细节,我们将通过以下两个步骤了解这些细节:
- 函数声明或定义
- 函数调用
首先,我们需要定义或声明一个新函数,因此我们需要执行类似于以下结构的操作:
returnedDataType functionName(param1, param2) { bodyOfFunction }
- (returnedDataType):是我们需要函数在执行后返回的数据类型。
- (functionName):是函数的名称,我们根据函数正在执行的任务给出这个名称。
- (param1,param2):在这里,我们添加必要的变量或占位符,因为我们可能不指定其中的任何一个。
- (bodyOfFunction):我们指定将执行任务的所有代码。
让我们将其应用于一个简单的示例,如果我们需要创建一个简单函数来执行两个值的加法任务,我们可以通过以下代码块来完成:
//addition function // returned data type is an integer - the name of the function is add - parameters or arguments are two int variables val1 and val2 int add(int val1, int val2) { //body of function that we need the function to perform when calling it //create a result new variable to be assigned by the result of val1 and val2 addition int result = val1+val2; //Print result in the experts tab Print(result); //returning value return result; }
在定义我们的函数后,我们需要进行第二步,即调用函数,我们可以通过调用函数的名称并在软件代码的所需部分根据我们的函数指定所需参数来实现这一点。回到我们的例子,如果我们想调用我们的函数,它将与以下相同:
//calling our defined function by its name and specifying arguments add(5,15);
当我们调用它时,我们可以根据我们的函数和指定的参数在专家选项卡中获得20值的结果,如下图所示。
前面的例子只是我们使用函数可以做什么的一个例子,但我们可以在函数中使用许多可用的特性,下面是其中的一些。
带参数的函数
我们在上一个加法函数示例中看到,我们使用了两个整数变量val1和val2,这些变量被视为函数中的自变量。这些参数可以是整数、字符串等数据类型。在我们的上一个例子中,它们是整数变量,与我们看到的相同,我们可以看到另一个字符串参数的例子,如下所示:
//sayHello function // returned data type is string - name of function is sayHello - parameters or arguments are two string variables greeting and name string sayHello(string greeting, string name) { //body of function that we need the function to perform when calling it //create a result new variable to be assigned by the result of greeting and name addition string result = greeting+name; //Print result in the experts tab Print(result); //returning value return result; }
我们可以像前面提到的那样调用这个函数,执行后我们可以发现它的结果如下:
根据我们在函数中需要的参数或自变量,它们也可以是这些数据类型的混合,以执行它们的函数体。这些参数也可以是我们根据需要所需要的数字。
不带参数的函数
可以在不指定参数或自变量的情况下声明或定义函数。也可以只为函数提供一个有意义的名称,然后将自变量留空,然后通过填充函数体来完成任务,然后调用函数而不指定自变量,如下所示:
//sayHello function // returned data type is a string - the name of the function is sayHello - no parameters string sayHello() { //body of the function that we need the function to perform when calling it //create a result new variable to be assigned by the result of greeting and name addition string greeting= "Hello, "; string name= "World!"; string result = greeting+name; //Print the result in the experts' tab Print(result); //returning value return result; }
当我们调用函数时,它将与以下内容相同:
sayHello();
结果将与我们之前在带有参数的函数中提到的结果相同,因为函数的主体是相同的。
具有默认值的函数
我们也定义了一个函数,并为参数提供初始值或默认值,但我们仍然可以用所需的值来更改或更新它们,当应用相同的示例时,这可能与以下相同
//defining function with default values string sayHello(string greeting= "Hello, ", string name="World!") { string result = greeting+name; Print(result); return result; }
然后,我们可以调用两次函数来识别默认值之间的差异,如果我们更新它们,但我也需要在这里提到,如果我们要更新它们,我们可以在这里指定参数,但如果我们不指定它们,函数将返回与以下相同的默认值:
sayHello(); sayHello("Hi, ", "Developer!");
结果如下所示:
传递参数
我们可以传递给函数值,这些值可以是我们提到的int、string、array等任何类型的数据,。。等等。通过按值传递参数,当我们将参数值传递给函数时,函数中的原始变量将保持不变。如果我们需要更新原始变量,我们也可以通过引用将参数传递给函数。
下面是一个简单的例子来理解我们提到的关于通过引用传递的内容。
//passing by reference void updateNums(int &val1, int &val2) { val1*=2; val2/=2; }
然后我们将创建新的变量,然后打印它们的值,然后用这些新变量作为参数调用我们的函数,并在调用后打印其值以显示差异:
//new variables int firstNum = 10; int secondNum = 20; //before calling function Print("before calling: "); Print(firstNum, " - " ,secondNum, "\n"); // calling updateNums(firstNum, secondNum); // after calling Print("after calling: "); Print(firstNum, " - " ,secondNum, "\n");
因此,两次打印的结果将是第一次打印将是新变量10和20的值,在调用后,我们将根据函数体找到更新20和10后的值。我们可以看到,结果如下所示:
Return 运算符
如果我们有一个返回值的函数,它必须有一个 return 运算符。基于函数的任务,我们可能在函数中有多个返回运算符,但如果函数有返回值,那么它必须在函数的最后一行至少有一个返回。这个返回运算符可以是任何类型,但不能是数组,但我们可以从数组中返回元素。如果我们想让函数返回一个数组,我们可以像前面提到的那样,通过引用将数组传递给函数。
以下是我们前面提到的具有返回运算符的示例
string sayHello(string greeting= "Hello, ", string name="World!") { string result = greeting+name; Print(result); return result; }
Void 类型函数
如果我们有一个不返回值的函数,我们使用void类型函数,因为该类型不返回值。我们可以正常地将参数传递给这种类型的函数,但它不需要返回运算符。以下是此类函数的示例。
void add(int val1, int val2) { int result= val1+val2; }
函数重载
在定义函数时,有时我们需要用相同的名称定义多个函数来执行相同的任务,但使用不同的参数。例如,如果我们有一个加法任务,但我们需要对两个值执行加法,并且我们需要对三个值执行相同的任务,在这种情况下,我们在函数的相同名称下创建两个函数,但我们将根据任务更改参数。这意味着我们有一个重载函数,所以重载函数是一个执行相同任务但使用不同参数的函数。
这些不同的参数可以是不同数据类型的参数、相同数据类型的数字,或者两者都是。以下是具有相同数据类型但参数数量不同的重载函数的示例:
void overloadingFun(int val1, int val2) { int result=val1+val2; } void overloadingFun(int val1, int val2, int val3) { int result=val1+val2+val3; }
正如我们所看到的,我们有相同的函数,但参数不同。当我们调用函数时,我们发现在键入函数名称时会出现这两个函数,然后我们可以根据任务详细信息选择需要的内容。以下是根据数据类型使用不同参数的重载函数的示例:
void overloadingFun(int val1, int val2) { int result=val1+val2; } void overloadingFun(string message, int val1, int val2) { int result=message+val1+val2; }
在调用函数时,我们还可以根据参数选择所需的函数。
函数应用
在这一部分中,我们将创建简单的应用程序,我们可以将函数用于从用户定义的函数中受益,并使编码过程更容易。创建这些应用程序后,我们可以在软件的不同部分调用它们,甚至可以通过将它们包含在另一个软件中来调用它们。
新闻提醒应用程序
我们都知道,在经济新闻期间交易风险很大,有很多专业建议我们不要在新闻期间交易。如果你不知道什么是经济日历,它是一个现成的日历,里面有宏观经济新闻和指标,包括描述、日期、时间和重要性,以及这些经济事件的发布值,有很多来源可以用来更新这些重要值,并据此影响市场和贸易。我们在MetaTrader 5交易终端中也有这个日历,因为您可以在工具箱窗口中找到它的选项卡,您可以控制您需要查看的重要性、货币和国家/地区。还有用于处理经济日历的内置函数,您可以通过以下链接在MQL5文档中查看所有这些函数:
因此,我们需要手动检查新闻经济日历,以避免在新闻期间进行交易,或者我们创建一个应用程序,在接近新闻时提醒我们停止交易。这项任务是一项永久性任务,因此我们需要在任何交易系统或软件的许多部分中使用它。因此,我们可以为此创建一个函数,然后轻松地调用它,这就是我们将在本部分通过以下步骤进行的操作:
这个应用程序将是一个EA,在全局范围内,我们将为我们的函数名称创建一个布尔类型,即(isNewsConding),然后我们不会添加参数()
bool isNewsComing()
函数的主体是创建一个数组,其值名称和类型将是(MqlCalendarValue),例如,这将是新闻发布的值,就像实际值一样
MqlCalendarValue values[];
我们需要通过定义一天的开始时间来定义当前一天,方法是:在向(endTime)声明新的日期时间变量后,使用(iTime)返回条形图的打开时间,并使用(PeriodSeconds)函数将一天的结束时间等于定义的开始时间和秒数
datetime startTime=iTime(_Symbol,PERIOD_D1,0); datetime endTime=startTime+PeriodSeconds(PERIOD_D1);
使用定义的开始时间和结束时间获取一天中所有事件的值的数组,以确定时间范围,并使用CalendarValueHistory函数按当前国家/地区和货币排序,参数为值的数组、开始时间、结束时间、国家和货币
CalendarValueHistory(values,startTime,endTime,NULL,NULL);
我们将创建一个循环,该循环将从创建的(i)int变量的值(0)开始,如果(i)小于值数组的数组大小,则继续循环,并将(i)增加一个值
for(int i=0; i<ArraySize(values); i++)
for循环的主体正在创建一个事件变量,其类型为(MqlCalendarEvent),用于事件描述,并且可以在(CalendarEventById)中使用
MqlCalendarEvent event;
使用(CalendarEventById)按ID获取事件描述,其参数为event_ID和event
CalendarEventById(values[i].event_id,event);
创建一个国家/地区变量,其类型将为(MqlCalendarCountry)作为国家/地区描述,并且可以与(CalendarCountryById)一起使用
MqlCalendarCountry country;
使用(MqlCalendarCountry)函数按其ID获取国家/地区描述,其参数为country_ID和country
CalendarCountryById(event.country_id,country);
设置条件以按当前符号或货币新闻过滤事件,新闻的重要性为中等或高,以及是否有其他内容继续
if(StringFind(_Symbol,country.currency)<0) continue; if(event.importance==CALENDAR_IMPORTANCE_NONE) continue; if(event.importance==CALENDAR_IMPORTANCE_LOW) continue;
设置一个我们需要的警报时间范围为新闻发布前30秒的条件
if(TimeCurrent()>=values[i].time-30*PeriodSeconds(PERIOD_M1) && TimeCurrent()<values[i].time+30*PeriodSeconds(PERIOD_M1))
然后,我们需要在专家选项卡中打印一条消息,其中包含事件名称和文本(即将到来!停止交易…)
Print(event.name, " is coming! Stop Trading...");
返回的值将为true
return true;
如果条件为false,则结束循环并返回false以终止函数
return false;
然后我们可以调用OnTick函数中的函数,如果它返回true,我们需要打印一条消息(新闻即将到来…!)
if(isNewsComing()) { Print("News is comming...!"); }
现在,我们创建了这个函数并调用了它,我们可以根据需要在软件的任何部分使用它。下面是一个块中的完整代码,以便于再次阅读,您会发现所有应用程序的源代码文件都附在文章中。
//+------------------------------------------------------------------+ //| News Alert Function | //+------------------------------------------------------------------+ void OnTick() { if(isNewsComing()) { Print("News is comming...!"); } } //+------------------------------------------------------------------+ bool isNewsComing() { MqlCalendarValue values[]; datetime startTime=iTime(_Symbol,PERIOD_D1,0); datetime endTime=startTime+PeriodSeconds(PERIOD_D1); CalendarValueHistory(values,startTime,endTime,NULL,NULL); for(int i=0; i<ArraySize(values); i++) { MqlCalendarEvent event; CalendarEventById(values[i].event_id,event); MqlCalendarCountry country; CalendarCountryById(event.country_id,country); if(StringFind(_Symbol,country.currency)<0) continue; if(event.importance==CALENDAR_IMPORTANCE_NONE) continue; if(event.importance==CALENDAR_IMPORTANCE_LOW) continue; if(TimeCurrent()>=values[i].time-30*PeriodSeconds(PERIOD_M1) && TimeCurrent()<values[i].time+30*PeriodSeconds(PERIOD_M1)) { Print(event.name, " is coming! Stop Trading..."); return true; } } return false; } //+------------------------------------------------------------------+
手数大小计算应用程序
我们需要创建一个应用程序,在确定风险百分比和最大损失(以点数为单位)后,能够计算出最佳手数,并且我们需要创建重载函数,在确定了风险百分比、进入价格和止损价格后,计算出最佳手数。我们将按照以下步骤将此应用程序创建为脚本:
创建一个名为OptimalLotSize的函数,第一个函数的参数为两个,最大风险百分比的double型变量和最大损失的double型变量,如下所示
double OptimalLotSize(double maxRiskPrc, double maxLossInPips)
然后,我们指定需要对这些参数执行的操作,首先,我们将使用(AccountInfoDouble)函数来定义帐户权益值,该函数返回适当帐户属性的值(此处为帐户净值的标识符)为(ENUM_ACCOUNT_INFO_DOUBLE),并创建一个具有值的提醒。
double accEquity = AccountInfoDouble(ACCOUNT_EQUITY); Alert("accEquity: ", accEquity);
通过使用(SymbolInfoDouble)函数定义符号合约大小,该函数返回指定交易品种的相应属性及其交易品种名称的变体(_symbol)以返回当前符号,prop_id将为(SYMBOL_TRADE_CONTRACT_SIZE)作为(ENUM_SYMBOL_INFO_DOUBLE)值之一,之后我们需要具有此返回值的提醒。
double lotSize = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_CONTRACT_SIZE); Alert("lotSize: ", lotSize);
计算pip值并使用该值获得提醒
double tickValue = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE); Alert("tickValue: ", tickValue);
从已定义的账户净值中计算损失的最大值,并获得具有该值的提醒
double maxLossDollar = accEquity * maxRiskPrc; Alert("maxLossDollar: ", maxLossDollar);
根据计算出的最大损失值计算报价货币的最大值,并返回具有此值的提醒
double maxLossInQuoteCurr = maxLossDollar / tickValue; Alert("maxLossInQuoteCurr: ", maxLossInQuoteCurr);
计算最佳手数大小并返回具有值的提醒
double OptimalLotSize = NormalizeDouble(maxLossInQuoteCurr / (maxLossInPips * 0.0001)/ lotSize,2); Alert("OptimalLotSize: ", OptimalLotSize);
return 运算符将作为 double 类型值返回OptimalLotSize。
return OptimalLotSize;
之后,我们将通过传递最大风险百分比、入场价格和止损价格的三个 double 类型参数来创建重载函数。
double OptimalLotSize(double maxRiskPrc, double entryPrice, double stopLoss)
根据进场价格和止损价格的输入参数,将最大损失定义为绝对值,然后除以0.0001。
double maxLossInPips = MathAbs(entryPrice - stopLoss)/0.0001;
返回运算符将是创建的OptimalLotSize函数,其参数为最大风险百分比和最大损失(以点数为单位)。
return OptimalLotSize(maxRiskPrc,maxLossInPips);
然后,我们可以根据需要调用OnStart()部分中的两个函数中的任意一个,例如,如下所示。
OptimalLotSize(0.01, 1.12303, 1.11920);
以下是创建此类功能以创建此类应用程序的完整代码。
//+------------------------------------------------------------------+ //| lotSize Calc Function | //+------------------------------------------------------------------+ void OnStart() { OptimalLotSize(0.01, 1.12303, 1.11920); } //+------------------------------------------------------------------+ double OptimalLotSize(double maxRiskPrc, double maxLossInPips) { double accEquity = AccountInfoDouble(ACCOUNT_EQUITY); Alert("accEquity: ", accEquity); double lotSize = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_CONTRACT_SIZE); Alert("lotSize: ", lotSize); double tickValue = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE); Alert("tickValue: ", tickValue); double maxLossDollar = accEquity * maxRiskPrc; Alert("maxLossDollar: ", maxLossDollar); double maxLossInQuoteCurr = maxLossDollar / tickValue; Alert("maxLossInQuoteCurr: ", maxLossInQuoteCurr); double OptimalLotSize = NormalizeDouble(maxLossInQuoteCurr / (maxLossInPips * 0.0001)/ lotSize,2); Alert("OptimalLotSize: ", OptimalLotSize); return OptimalLotSize; } //+------------------------------------------------------------------+ double OptimalLotSize(double maxRiskPrc, double entryPrice, double stopLoss) { double maxLossInPips = MathAbs(entryPrice - stopLoss)/0.0001; return OptimalLotSize(maxRiskPrc,maxLossInPips); } //+------------------------------------------------------------------+
通过执行此脚本,我们将收到以下提醒
根据前面提到的应用程序,我们有lotSize Calc函数,我们可以在不同的软件部分中轻松使用和调用它来执行任务,而无需再次重写代码。
关闭所有应用程序
我们需要在这里创建一个脚本,通过创建一个函数来关闭打开的订单和挂单,该函数可以在软件的任何合适部分中使用或调用以执行此任务。我们可以通过以下步骤做到这一点:
通过使用预处理或#include将所有交易函数包含在(Trade.mqh)文件中,将Trade类包含到代码中
#include <Trade/Trade.mqh>
创建一个具有要在软件中使用的CTrade类类型的对象
CTrade trade;
同样在全局范围内,我们需要创建一个没有参数的void closeAll函数
void closeAll()
函数的主体是创建一个for循环来检查未结订单
for(int i=PositionsTotal()-1; i>=0; i--)
循环的主体,创建ulong posTicket变量并将打开的订单的单号分配给它
ulong posTicket=PositionGetTicket(i);
使用 trade.PositionClose(posTicket) 关闭开启的交易。
trade.PositionClose(posTicket);
我们将删除挂单,方法是创建另一个for循环来检测这些订单,将其单号分配给posTicket的ulong变量,通过其检测到的单号删除挂单
for(int i=OrdersTotal()-1; i>=0; i--) { ulong posTicket=OrderGetTicket(i); trade.OrderDelete(posTicket); }
之后,我们可以在OnStart()部分调用此函数。
closeAll();
通过执行此脚本,它将关闭并删除所有订单。以下是整体创建此closeAllApp的完整代码:
//+------------------------------------------------------------------+ //| closeAll Function | //+------------------------------------------------------------------+ #include <Trade/Trade.mqh> CTrade trade; //+------------------------------------------------------------------+ void OnStart() { closeAll(); } //+------------------------------------------------------------------+ void closeAll() { //close all open positions for(int i=PositionsTotal()-1; i>=0; i--) { ulong posTicket=PositionGetTicket(i); trade.PositionClose(posTicket); } //delete all pending orders for(int i=OrdersTotal()-1; i>=0; i--) { ulong posTicket=OrderGetTicket(i); trade.OrderDelete(posTicket); } } //+------------------------------------------------------------------+
这些提到的应用程序只是我们可以创建为用户定义功能的示例,您可以根据自己的需求开发它们或创建任何其他应用程序或功能,如跟踪止损和交易管理应用程序以及安全回撤工具等。
结论
根据我们在本文中提到的内容,假设您发现在软件中使用函数是至关重要的,因为您在使用这些函数时会获得所有功能和好处,因为假设您确定了使用这些功能的函数,如:
- 它有助于在编程中应用DRY(不要重复)概念。
- 通过将大问题划分为小问题并加以处理,有助于最大限度地减少大问题。
- 使代码更具可读性。
- 可重复使用性。
- 抽象代码。
- 封装代码。
- 调试改进。
还应该了解函数是什么,它的类型,内置函数和用户定义函数,以及如何通过学习函数的结构和它们的所有特征来创建或定义函数,比如带参数的函数,以及我们如何传递这些参数或参数,不带参数,以及带默认值的函数。假设您可以使用任何数据类型定义函数,并在此基础上处理返回运算符,并且您知道如何创建许多具有相同名称但不同参数的重载函数来执行相同的任务。
在分享了创建函数的应用程序作为示例后,我相信这对加深您对该主题的理解有很大帮助,因为我们创建了两个不同的函数:
- 新闻提醒应用程序:在软件的任何部分使用或调用,以在重要消息到来时获得提醒。您会发现附带的newsAlertApp源代码。
- 手数计算应用程序:在软件的任何部分使用或调用,以根据定义的风险百分比、进场价格和止损价格返回打开交易的最佳手数。或者,基于定义的风险百分比和最大损失,这意味着我们在这个应用程序中创建了一个重载函数。您会发现附加的lotSizeCalcApp源代码。
- 关闭所有应用程序:用于或调用以关闭所有打开和挂起的订单。你会发现附件中的closeAllApp源代码。
函数的世界非常有趣,关注它以便能够轻松、流畅、有效地创建有用的代码片段是非常重要的。我希望你发现这篇文章对你有用,可以通过创建有用的工具来帮助你进行良好的交易,从而发展你的编码技能,提升你的交易水平。如果你想阅读更多关于编程的文章,或者关于如何创建基于最流行的技术指标的交易系统,如移动平均线,RSI,MACD,随机振荡,布林带,抛物线Sar等,你可以查看我的出版物,你会发现关于这方面的文章,我希望你也能发现它们很有用。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/12970
![离散哈特莱变换](https://c.mql5.com/2/57/discrete_hartley_transform_avatar.png)
![StringFormat(). 回顾和现成的例子](https://c.mql5.com/2/56/stringformatzj-avatar.png)
![MQL5中的结构及其数据打印方法](https://c.mql5.com/2/57/formatte_series_mqlformat-avatar.png)
![了解 MQL5 面向对象编程(OOP)](https://c.mql5.com/2/56/object-oriented-programming-avatar.png)