两种基本类型的金融市场包括交易所和场外交易市场。 我们可以使用现代 MetaTrader 和 MetaEditor 工具享受 OTC(场外交易市场) 外汇交易,这些工具正在不断得到进一步改进。 除了交易自动化,这些工具还可以使用历史数据对交易算法进行全面测试。
如何运用我们自己的思路进行兑换交易? 一些兑换交易终端具有内置的可编程语言。 例如,广受欢迎的 Transaq 终端具有 ATF (超级交易装置) 可编程语言。 但是,当然了,它无法与 MQL5 进行比较。 此外,它没有任何策略测试功能。 一个好的解决方案是在 MetaTrader 策略测试器中获取兑换数据并优化交易算法。
这可以通过创建自定义品种来完成。 文章 在 MetaTrader 5 中创建并测试自定义品种 中详细描述了自定义品种的创建过程。 所需要的只是从 CSV(TXT)格式中获取数据,并按照本文中描述的步骤导入价格历史记录。
如果数据格式没有差异,这将很容易。 例如,我们研究一下兑换相关的流行网站资源 finam.ru。 报价可以在这里下载:
从莫斯科交易所导出报价
Finam 提供的数据格式:
可用的日期格式: "yyyymmdd", "yymmdd", "ddmmyy", "dd/mm/yy", "mm/dd/yy"。 我们的格式:
未提供我们需要的“yyyy.mm.dd”格式。 所以,finam.ru 提供了大量不同的格式,但没有一种格式是我们需要的。
进而,还有很多其他的兑换资源。 其他网站提供的格式也可能不合适。 我们需要一定顺序的数据。 然而,报价可按不同的顺序存储,例如,开盘价,收盘价,最高价,最低价。
所以,我们的任务是将随机顺序和不同格式提供的数据转换为所需格式。 这将为 MetaTrader 5 提供从任意资源接收数据的机会。 然后,我们将基于收到的数据利用 MQL5 工具创建自定义品种,这将令我们能够执行测试。
导入报价有几分困难。
兑换支持点差,竞卖价(Ask)和竞买价(Bid)。 但是,在市场深度里所有这些数值仅存在“片刻”。 此后,无论其执行价格如何,只有成交价格被写入,即竞卖价或竞买价。 我们需要终端的点差值。 此处加入了固定点差,因为无法复原市场深度点差。 如果点差是必要的,您可按某种方式模拟它。 其中一种方法已在文章 根据指定分布定律为自定义品种的时间序列建模 中有所描述。 或者,您可以编写一个简单的函数来体现点差对波动率的依赖性 Spread = f(High-Low)。
当按照时间帧操作时,使用固定点差是完全可以接受的。 这点误差在很大的周期内是微不足道的。 不过,点差建模对于逐笔报价非常重要。 兑换逐笔报价格式:
我们的格式:
除了最后成交( LAST) 之外,我们还需要设置竞卖价(ASK)和竞买价(BID)。 数据以毫秒精度排序。 兑换仅提供价格流。 第一页中的数据更像是将大数据切分为几个片段。 外汇方面没有逐笔报价。 它可以是竞买价(Bid),竞卖价(Ask),或竞买价和竞卖价同时出现。 此外,我们需要人为地按时间对交易进行排位并添加毫秒数。
因此,本文不涉及数据导入,而是涉及数据建模,就像上面提到的文章一样。 所以,为了不会误导您,我决定不按照竞卖价=竞买价(+点差)=最后成交的原则来发送逐笔报价导入应用程序。 当使用毫秒处理时,点差很重要,因此在测试中我们需要选择合适的建模方法。
在此之后,修改逐笔报价导入代码将花费几分钟。 只需要用 MqlTick 替换 MqlRates 结构。 CustomRatesUpdate() 函数需要由 CustomTicksAdd() 替代。
下一个关联点是无法考虑所有可能的数据格式。 例如,在书写数字时,可以使用空白作为分隔符(1 000 000),或使用逗号来代替小数点(如 3,14)。 或者甚至更糟 - 当数据分隔符和小数点分隔符都是圆点或逗号时(您会如何区分它们呢)。 这里只考虑最常见的格式。 如果您需要处理非标准格式,则您必须自行处理它。
此外,兑换没有逐笔报价历史记录 — 它只提供交易量。 因此,在本文中我们设定兑换交换量 =VOL=TICKVOL。
本文分为两部分。 第一部分介绍了代码说明。 它可令您熟悉代码,以便稍后您可以编辑它,以便用于非标准数据格式的处理。 第二部分包含循序渐进的指南(用户手册)。 它适用于那些对编程不感兴趣,但仅需要使用已实现功能的人士。 如果您使用标准数据格式(特别是使用 finam.ru 网站作为来源),您可以立即进入第 2 部分。
在此仅提供部分代码。 完整代码可在附件中找到。
首先,我们输入所需的参数,例如字符串中数据所在位置,文件参数,品种名称,等等。
input int SkipString =1; // 要跳过的字符串数量 input string mark1 ="Time position and format"; // 时间 input DATE indate =yyyymmdd; // 源日期格式 input TIME intime =hhdmmdss; // 源时间格式 input int DatePosition =1; // 日期位置 input int TimePosition =2; // 时间位置 //------------------------------------------------------------------+ input string mark2 ="Price data position"; // 价格 input int OpenPosition =3; // 开盘价位置 input int HighPosition =4; // 最高价位置 input int LowPosiotion =5; // 最低价位置 input int ClosePosition =6; // 收盘价位置 input int VolumePosition =7; // 成交量位置 input string mark3 ="File parameters"; // 文件 //-------------------------------------------------------------------+ input string InFileName ="sb"; // 源文件名 input DELIMITER Delimiter =comma; // 分隔符 input CODE StrType =ansi; // 字符串类型 input string mark4 ="Other parameters"; // 其它 //-------------------------------------------------------------------+ input string spread ="2"; // 固定点差点数 input string Name ="SberFX"; // 您所创建的品种名称
为某些数据创建枚举。 例如,对于日期和时间格式:
enum DATE { yyyycmmcdd, // yyyy.mm.dd yyyymmdd, // yyyymmdd yymmdd, // yymmdd ddmmyy, // ddmmyy ddslmmslyy, // dd/mm/yy mmslddslyy // mm/dd/yy // 其他格式在此处添加 }; enum TIME { hhmmss, // hhmmss hhmm, // hhmm hhdmmdss, // hh:mm:ss hhdmm // hh:mm // 其他格式在此处添加 };
如果所需格式未提供,则添加它。
然后打开源文件。 为了方便编辑格式化数据,我建议将它们保存在 CSV 文件中。 同时,应将数据写入 MqlRates 结构,以便能够自动创建自定义品种。
// 打开输入文件 int out =FileOpen(InFileName,FILE_READ|StrType|FILE_TXT); if(out==INVALID_HANDLE) { Alert("Failed to open the file for reading"); return; } // 打开输出文件 int in =FileOpen(Name+"(f).csv",FILE_WRITE|FILE_ANSI|FILE_CSV); if(in==INVALID_HANDLE) { Alert("Failed to open the file for writing"); return; } //---插入标题字符串 string Caption ="<DATE>\t<TIME>\t<OPEN>\t<HIGH>\t<LOW>\t<CLOSE>\t<TICKVOL>\t<VOL>\t<SPREAD>"; FileWrite(in,Caption); //----------------------------------------------------------- string fdate="",ftime="",open=""; string high="",low="",close="",vol=""; int left=0,right=0; string str="",temp=""; for(int i=0;i<SkipString;i++) { str =FileReadString(out); i++; } MqlRates Rs[]; ArrayResize(Rs,43200,43200); // 一个月中有 43200 分钟 datetime time =0;
源文件必须保存到 MQL5/Files 目录。 SkipString 外部变量表示要自文件头跳过的行数。 为了能够使用空格和制表符作为分隔符,我们使用 标志 FILE_TXT 打开文件。
然后我们需要从字符串中提取数据。 在输入参数中指定该位置。 编号从 1 开始。 我们以 Sberbank 股票报价为例。
这里的日期位置是 1,时间是 2,等等。 SkipString=1。
若要解析字符串,我们可以使用 StringSplit() 函数。 但是最好开发自己的函数,以便更便洁地监控源文件中的错误。 可在这些函数中添加数据分析。 尽管,使用 StringSplit() 代码会更轻松。 查找数据边界的第一个函数接收字符串,分隔符和位置。 边界会被写入 a 和 b 变量,这些变量会作为引用传递。
//---搜索数据位置边界-----------------------------+ bool SearchBorders(string str,int pos,int &a,int &b,DELIMITER delim) { // 辅助变量 int left=0,right=0; int count=0; int start=0; string delimiter=""; //-------------------------------------------------------------------+ switch(delim) { case comma : delimiter =","; break; case tab : delimiter ="/t"; break; case space : delimiter =" "; break; case semicolon : delimiter =";"; break; } while(count!=pos||right!=-1) { right =StringFind(str,delimiter,start); if(right==-1&&count==0){Print("Wrong date");return false;} //Incorrect data if(right==-1) { right =StringLen(str)-1; a =left; b =right; break; } count++; if(count==pos) { a =left; b =right-1; return true; } left =right+1; start =left; } return true; }
现在,我们来利用 StringSubstr() 函数获取相应的数据。 必须将接收的数值转换为所需的格式。 为此,我们来编写日期和时间转换函数。 例如,这是日期转换函数:
//---日期格式-------------------------------------------------+ //2017.01.02 string DateFormat(string str,DATE date) { string res=""; string yy=""; switch(date) { case yyyycmmcdd : //我们的格式 res =str; if(StringLen(res)!=10)res=""; // 检查日期格式 case yyyymmdd : res =StringSubstr(str,0,4)+"."+StringSubstr(str,4,2)+"."+StringSubstr(str,6,2); if(StringLen(res)!=10)res=""; // 检查日期格式 break; case yymmdd : yy =StringSubstr(str,0,2); if(StringToInteger(yy)>=70) yy ="19"+yy; else yy ="20"+yy; res =yy+"."+StringSubstr(str,2,2)+"."+StringSubstr(str,4,2); if(StringLen(res)!=10)res=""; // 检查日期格式 break; //---其他格式 (完整代码在文件当中)------------- //如有必要,添加其他格式的解析 default : break; } return res; }
如果所需格式未提供(例如 1 月 18 日之前的日期),则应添加该格式。 此处检查接收的数据是否符合所需的格式(如果源文件中有错误) if(StringLen(res)!=10) res="";。我明白这不是很彻底的检查。 但数据分析并不是一件容易的事,因此需要一个单独的程序来进行更详细的分析。 如果出现错误,函数将返回 res = "",然后跳过相应的行。
以下代码可供 ddmmyy 类型的格式转换,其中年份写为两位数。 数值 >=70 则转换为 19yy,数值小于它则转换为 20yy。
格式转换后,我们将数据写入相应的变量,并编译最终的字符串。
while(!FileIsEnding(out)) { str =FileReadString(out); count++; //---fdate----------------------------- if(SearchBorders(str,DatePosition,left,right,Delimiter)) { temp =StringSubstr(str,left,right-left+1); fdate =DateFormat(temp,indate); if(fdate==""){Print("Error in string ",count);continue;} } else {Print("Error in string ",count);continue;} //---其他数据的处理方式类似
如果在函数 SearchBorders,DateFormat 或 TimeFormat 中发现错误,则跳过该字符串并使用Print() 函数输出其顺序编号。 所有枚举和格式转换函数都位于单独的头文件 FormatFunctions.mqh 当中。
然后形成并输出所得到的字符串。 数据被分配给 MqlRates 结构的相应元素。
//-------------------------------------------------------------------+ str =fdate+","+ftime+","+open+","+high+","+low+","+close+","+vol+","+vol+","+Spread; FileWrite(in,str); //---填充 MqlRates -----------------------------------------------+ Rs[i].time =time; Rs[i].open =StringToDouble(open); Rs[i].high =StringToDouble(high); Rs[i].low =StringToDouble(low); Rs[i].close =StringToDouble(close); Rs[i].real_volume =StringToInteger(vol); Rs[i].tick_volume =StringToInteger(vol); Rs[i].spread =int(StringToInteger(Spread)); i++; //-------------------------------------------------------------------+ }
读取所有字符串后,动态数组将获得最终大小,且文件将关闭:
ArrayResize(Rs,i); FileClose(out); FileClose(in);
现在,创建自定义品种已一切就绪。 此外,我们还有一个 CSV 文件,可以直接在 MetaEditor 中轻松编辑。 基于该 CSV 文件,我们可以使用 MetaTrader 5 终端中的标准方法创建自定义品种。
既然已经准备好了所有数据,我们只需要添加自定义品种。
CustomSymbolCreate(Name); CustomRatesUpdate(Name,Rs);
利用 CustomRatesUpdate() 函数导入报价,这意味着该程序不仅可用于创建品种,还可用于添加新数据。 如果品种已存在,CustomSymbolCreate() 将返回 -1(负 1),且程序将继续执行,而报价将通过 CustomRatesUpdate() 函数进行更新。 该品种显示在 市场观察 窗口中,并以绿色高亮显示。
现在我们可以打开图表以确保一切正常:
EURUSD 图表
在测试品种时,我们可能需要配置其特性(规格)。 我已编写了一个单独的规格包含文件,可以方便地编辑品种属性。 在此文件中,品种属性在 SetSpecifications() 函数中设置。 所有品种属性来自在此收集的 ENUM_SYMBOL_INFO_INTEGER, ENUM_SYMBOL_INFO_DOUBLE , ENUM_SYMBOL_INFO_STRING 枚举。
void SetSpecifications(string Name) { //---整数属性------------------------------------- // CustomSymbolSetInteger(Name,SYMBOL_CUSTOM,true); // 布尔值表示此品种自定义 // CustomSymbolSetInteger(Name,SYMBOL_BACKGROUND_COLOR,clrGreen); // 在市场观察中此品种所用的背景颜色 // 其它整数属性 //---双精度属性 --------------------------------------------------- // CustomSymbolSetDouble(Name,SYMBOL_BID,0); // 竞买价,出售品种的最佳价格 // CustomSymbolSetDouble(Name,SYMBOL_BIDHIGH,0); // 每日最高的竞买价 // CustomSymbolSetDouble(Name,SYMBOL_BIDLOW,0); // 每日最低的竞买价 // 其它双精度属性 //---字符串属性-----------------------------------------------+ // CustomSymbolSetString(Name,SYMBOL_BASIS,""); // 自定义品种的基准资产名称 // CustomSymbolSetString(Name,SYMBOL_CURRENCY_BASE,""); // 品种的基准货币 // CustomSymbolSetString(Name,SYMBOL_CURRENCY_PROFIT,""); // 盈利货币 // 其它字符串属性 }
此函数在 CustomSymbolCreate 函数之后执行。 事先不知道这是什么类型的品种,期货,股票或期权,大多数属性不是必需的,且被注释掉。 源代码中只有部分行未注释:
CustomSymbolSetInteger(Name,SYMBOL_CUSTOM,true); // 布尔值表示此品种是自定义的 CustomSymbolSetInteger(Name,SYMBOL_BACKGROUND_COLOR,clrGreen); // 在市场观察中此品种所用的背景颜色 CustomSymbolSetInteger(Name,SYMBOL_SELECT,true); // 布尔值表示在市场观察中选择了此品种 CustomSymbolSetInteger(Name,SYMBOL_VISIBLE,true); // 布尔值表示此品种已在市场观察中显示
出于测试目的,以下参数未被注释:最小交易量,交易量增量,价格增量,点数大小,这些都是必要的特性。 这些特性是 Sberbank 股票的典型特征。 不同品种的属性集合及其特征不同。
CustomSymbolSetDouble(name,SYMBOL_POINT,0.01); // 点数 CustomSymbolSetDouble(name,SYMBOL_VOLUME_MIN,1); // 一笔成交最小交易量 CustomSymbolSetDouble(name,SYMBOL_VOLUME_STEP,1); // 最小交易量变化增量 CustomSymbolSetInteger(name,SYMBOL_DIGITS,2); // 小数位 CustomSymbolSetInteger(name,SYMBOL_SPREAD,2); // 点差的点数 CustomSymbolSetInteger(name,SYMBOL_SPREAD_FLOAT,false); // 布尔值表示浮动点差 CustomSymbolSetDouble(name,SYMBOL_TRADE_TICK_SIZE,0.01); // 最小价格变化
这种方式很好,我们无需在每次需要设置必要属性时重新编译代码。 如果可以通过输入所需参数就可以完成就更方便了。 因此我不得不改变方法。 品种属性将在纯文本文件 Specifications.txt中 提供,可为每个新品种手动编辑。 这样就不需要重新编译源代码。
在 MetaEditor 中编辑文本文件更方便。 主要是因为 MetaEditor 提供了参数和数据的高亮显示。 属性按以下格式编写:
数据以逗号分隔。 字符串解析如下:
while(!FileIsEnding(handle)) { str =FileReadString(handle); //--- 跳过行 -----------------------+ if(str=="") continue; if(StringFind(str,"//")<10) continue; //------------------------------------------+ sub =StringSplit(str,u_sep,split); if(sub<2) continue; SetProperties(SName,split[0],split[1]); }
如果该行为空,或者在开头(位置<10)处有注释符号“//”,则跳过该行。 然后利用 StringSplit() 函数将字符串切分为子串。 之后,将字符串传递给 SetProperties() 函数,在其中设置品种属性。 函数代码结构:
void SetProperties(string name,string str1,string str2) { int n =StringTrimLeft(str1); n =StringTrimRight(str1); n =StringTrimLeft(str2); n =StringTrimRight(str2); if(str1=="SYMBOL_CUSTOM") { if(str2=="0"||str2=="false"){CustomSymbolSetInteger(name,SYMBOL_CUSTOM,false);} else {CustomSymbolSetInteger(name,SYMBOL_CUSTOM,true);} return; } if(str1=="SYMBOL_BACKGROUND_COLOR") { CustomSymbolSetInteger(name,SYMBOL_BACKGROUND_COLOR,StringToInteger(str2)); return; } if(str1=="SYMBOL_CHART_MODE") { if(str2=="SYMBOL_CHART_MODE_BID"){CustomSymbolSetInteger(name,SYMBOL_CHART_MODE,SYMBOL_CHART_MODE_BID);} if(str2=="SYMBOL_CHART_MODE_LAST"){CustomSymbolSetInteger(name,SYMBOL_CHART_MODE,SYMBOL_CHART_MODE_LAST);} return; } //--- 其他品种属性 }
如果用户在编辑时留下空格或制表符,则会为这些情况添加另外两个函数,StringTrimLeft() 和 StringTrimRight()。
完整代码可在包含文件 PropertiesSet.mqh 中找到。
现在,所有品种属性都是通过附加的文本文件设置的,而对于其它的,需要重新编译。 您可以检查下面附带的两种代码变体。 第一个变体需要通过包含文件进行属性设置,它已被注释掉。
为了方便编辑代码,使用 输入参数 指定设置。 如果没有可编辑的内容,我们可以考虑界面。 对于最终版本,我开发了输入面板:
关于面板代码。 此处用到的 标准控件集合 来自以下头文件:
#include <Controls\Dialog.mqh> #include <Controls\Label.mqh> #include <Controls\Button.mqh> #include <Controls\ComboBox.mqh>
已为 OK 按钮创建了事件处理程序。
//+------------------------------------------------------------------+ //| 事件处理 | //+------------------------------------------------------------------+ EVENT_MAP_BEGIN(CFormatPanel) ON_EVENT(ON_CLICK,BOK,OnClickButton) EVENT_MAP_END(CAppDialog) void CFormatPanel::OnClickButton(void) { // 上述属性 }
现在几乎将上述程序的整个代码移到此事件处理程序。 外部参数变为 局部变量。
long SkipString =1; // 要跳过的字符串数量 DATE indate =yyyymmdd; // 源日期格式 TIME intime =hhdmmdss; // 源时间格式 int DatePosition =1; // 日期位置 int TimePosition =2; // 时间位置 // 其它参数
为每个控件编写 Create() 函数,因此在执行后将相应的数值添加到控件列表中。 例如,对日期格式执行以下操作:
//-----------多选框日期格式------------------------------------+ if(!CreateComboBox(CDateFormat,"ComDateFormat",x0,y0+h+1,x0+w,y0+2*h+1)) { return false; } CDateFormat.ListViewItems(6); CDateFormat.AddItem(" yyyy.mm.dd",0); CDateFormat.AddItem(" yyyymmdd",1); CDateFormat.AddItem(" yymmdd",2); CDateFormat.AddItem(" ddmmyy",3); CDateFormat.AddItem(" dd/mm/yy",4); CDateFormat.AddItem(" mm/dd/yy",5); CDateFormat.Select(1); }
然后,这些数值从输入字段返回到相应的变量:
long sw; SkipString =StringToInteger(ESkip.Text()); sw =CDateFormat.Value(); switch(int(sw)) { case 0 :indate =yyyycmmcdd; break; case 1 :indate =yyyymmdd; break; case 2 :indate =yymmdd; break; case 3 :indate =ddmmyy; break; case 4 :indate =ddslmmslyy; break; case 5 :indate =mmslddslyy; break; } // 其他变量
此版本已得到大量实现,因此如果您需要编辑代码,您应使用输入版本。
本部分循序渐进介绍创建自定义兑换品种所需的操作。 当可用报价拥有任何标准格式,且您无需编辑代码时,可以使用本指南。 例如,如果报价是从网站 finam.ru 网站获得的。 如果报价是某些非标准格式,那么您应该编辑第 1 部分中描述的代码。
因此,我们有一个包含金融产品兑换报价的源文件。 假设,我们已经从文章开头所述的 Finam 网站得到了它。 不要忘记我们需要的是一分钟时间帧的报价。
本文介绍了两种数据导入选项。 您可以使用 CreateCustomSymbol 脚本,以及 CreateSymbolPanel 智能交易系统,它拥有输入面板。 两个 EA 的表现完全相同。 例如,我们来研究使用输入面板进行操作。 在此处提供的示例中,我们使用来自 莫斯科交易所 的 Sberbank 股票报价。 报价附在下面的 sb.csv 文件中。
1. 文件的编排
首先,我们需要将报价文件保存到 MQL5/Files 当中。 这与 MQL5 编程概念有关,因为 出于安全原因,文件的操作严格受限。 找到所需目录的最简单方法是从 MetaTrader 中打开它。 在导航窗口中,右键单击文件夹,然后从关联菜单中选择“打开文件夹”。
源数据文件应保存到此文件夹(程序文件的位置在下面的文件一章中介绍)。 现在可以在 MetaEditor 中打开该文件。
将 Specifications.txt 添加到同一文件夹。 它设置品种属性。
2. 输入
下一步是确定数据格式和位置,选择文件属性并设置自定义品种的名称。 如何填充字段的示例如下所示:
数据应传送到面板。 在此版本中使用固定点差,因此不对浮动点差进行建模。 因此,您应在此处输入适当的点差值。
填写完整文件名,包括扩展名。
现在,在单击“OK”之前,请指定必要的品种规格。 它们可以在之前放在 MQL5/Files 当中的 Specifications.txt 文件中找到。
在 MetaEditor 中编辑文本文件非常方便。 主要原因是 MetaEditor 支持数据高亮显示。 如果您无法理解任何属性,请将光标悬停在其上并按 F1。
属性以红色高亮显示,数值则以绿色显示。 注释掉的属性(//)不会用到,并以绿色显示。 请注意,逗号用于数据分离。 编辑时不要删除属性。 为了避免错误,您应该保留现有格式。
若要编辑属性,请取消所需属性注释(删除“//”),然后设置适当的值。 附加文件中设置的最小属性集合:价格增量,点值,最小手数,等等。
莫斯科交易所的 Sberbank 股票需要所有这些特征(在源文件中)。 其他金融产品需要不同的特征,因此您需要编辑属性。
最低要求的属性集合位于文件的最开头。
通常,股票价格有 2 位小数(SYMBOL_DIGITS),而点数值等于 0.01 卢布。 股票期货价格的小数位数为 0,点值为 1 卢布。 请参阅 moex.com 上的规格。
设置完所有必需属性后,单击“OK”。 创建的自定义品种将显示在导航窗口中。 在我的示例中,它以绿色高亮显示。
打开图表进行检查:
一切都很好,所以现在可以在 策略测试器 中测试自定义品种。
自定义品种设置的执行方式与标准品种类似。 在此重要的一点是正确配置品种规格。
例如,我们使用自己的数据来测试终端中可用的任意标准智能交易系统(此处为移动平均线):
一切都按预期工作。 如果需要添加新报价或更改属性,只需对现有品种重复上述操作即可。 如果规格未更改,请单击“OK”而无需编辑属性。
位于文件夹中的附加文件,应保存在计算机中:
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程