有很多理由都让我选择写下这篇文章以及研究它是否可行。
首先, MetaTrader 5已经发布了很长时间,但是我们还是在等待我们喜爱的交易经纪公司允许我们在真实交易中使用它。有些人使用MQL5创建交易策略并且有很好的成果,现在想在他们的真实帐户下运行,其他一些人可能希望人工交易,但是更喜欢MetaTrader 5中的交易组织方式而不是MetaTrader 4。
第二个原因,在自动交易锦标赛中,每个人都想过在他们的真实帐户中跟随比赛的领先者,一些人已经使用了他们自己的方法来跟随交易,但是其他一些人还在寻找方法来获得和锦标赛中交易者尽可能接近的结果,以及怎样应用资金管理等。
第三,一些人有很好的交易策略,他们希望信号不光给自己用,也给他们的朋友或者其他人使用,他们需要能接收多个连接,但是在实时条件下发布信号又不会降低效率。
以上这些问题困扰了我很久,我将寻找一个解决方案来处理这些需求。
最近我在MQL5.community找到了多篇符合我知识范围的文章,我想我可以建立这样的系统。我还想告诉您我已经使用了应用程序来在我的真实帐户中跟随锦标赛首页中的活动(很幸运,我已经有了利润)。问题是 - 数据每五分钟才更新一次,您可能会错失买卖的最佳时机。
从锦标赛论坛中我了解到,还有其他一些人在做同样的事情,这样的效率是不高的,而且这样给锦标赛首页带来很大的流量,组织者恐怕不会高兴。那么,有解决方案吗?我浏览了所有的解决方案,我很高兴可以通过MetaTrader 5中的“只读”模式(禁止交易)来访问每个锦标赛选手的账户。
我们可以使用它在实时下获取和传输每一个交易活动的信息吗?为了寻找方法,我创建了EA交易程序并且尝试在只有“只读”模式访问权限下运行它,令我惊讶的是,这是可能的,并且还可能获得仓位,订单和交易的信息 - 那些都是迈向我们解决方案的大门!
如果我们准备从MetaTrader 5中发信息给MetaTrader 4,我们就需要考虑MetaTrader 4中的所有交易类型,还有,我们什么时候跟随,我们需要知道帐户中交易的所有行为,这样“仓位”就不能给我们全面的信息了,除非我们每个订单号或者每秒都比较“仓位”状态。
这样,最好还是跟随“订单”或者“交易”。
我首先考虑订单:
我很喜欢在“交易”之前执行订单,而且它们还包含了挂单信息,但是和“交易”相比,他们缺少了一件很重要的东西 - 进场类型(ENUM_DEAL_ENTRY):
DEAL_ENTRY_TYPE 帮助我们了解在“订单”需要平行计算的时候交易者帐户发生了什么,最好的解决方案是综合“交易”和“订单”,这样我们就能够既获得挂单信息,也能跟随交易账户的行为了,因为价格的移动在不同交易经纪公司是不同的,所以挂单可能会引起错误而产生不正确的结果。
如果我们只是跟随“交易”,我们还是能够执行挂单,只是有小的延迟(根据网络连接状况)。在速度(挂单)和效果(交易)两者间,我选择了效果(“交易”)。
有很多不同文章都讨论了怎样从MetaTrader 5中和其他应用程序和计算机进行通信和传输数据,因为我希望其他客户端能够和我们相连,他们很可能在其他计算机上,所以我选择 TCP 连接。
因为MQL5没有直接的API函数来做这些,我们需要用到外部函数库,有些文章引入"WinInet.dll" 库 (比如"在因特网内使用WinInet.dll在终端间做数据交换" 还有其他),但是他们不能真正满足我们的需求。
因为我更熟悉C#语言,我决定建立我自己的库,为了做到这个,我使用了文章 "使用非托管输出从C#向MQL5开放代码" 来帮助我解决兼容性问题。我使用很简单的接口创建了服务器,允许最多500个客户端同时访问(您的电脑上需要有 .NET framework 3.5或者更新的版本。在多数电脑上已经安装了。"Microsoft .NET Framework 3.5")。
#import "SocketServer.dll" // 使用C#语言开发的函数库(根据 https://www.mql5.com/zh/articles/249 文章的相关信息开发) string About(); // 函数库的“关于”信息. int SendToAll(string msg); // 给所有客户发文本信息. bool Stop(); // 停止服务器. bool StartListen(int port); // 开启服务器. 听所有进来的连接(最多500个客户端). // 所有客户连接使用异步线程开发. string ReadLogLine(); // 读取服务器记录 (可以包含错误和其他信息). // 读取是可选的. 服务器最多存100行记录. #import
服务器本身在后台使用单独线程运行,不论有多少客户端连接进来,都不会阻塞或者减慢MetaTrader 5的工作或者您的交易策略。
C# 源代码:
internal static void WaitForClients() { if (server != null) { Debug("无法开始侦听!服务器没有清空."); return; } try { IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, iPort); server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); server.Bind(localEndPoint); server.Listen(500); isServerClosed = false; isServerClosedOrClosing = false; while (!isServerClosedOrClosing) { allDone.Reset(); server.BeginAccept(new AsyncCallback(AcceptCallback), server); allDone.WaitOne(); } } catch (ThreadAbortException) { } catch (Exception e) { Debug("WaitForClients() 错误: " + e.Message); } finally { if (server != null) { server.Close(); server = null; } isServerClosed = true; isServerClosedOrClosing = true; } } internal static void AcceptCallback(IAsyncResult ar) { try { allDone.Set(); if (isServerClosedOrClosing) return; Socket listener = (Socket)ar.AsyncState; Socket client = listener.EndAccept(ar); if (clients != null) { lock (clients) { Array.Resize(ref clients, clients.Length + 1); clients[clients.Length - 1].socket = client; clients[clients.Length - 1].ip = client.RemoteEndPoint.ToString(); clients[clients.Length - 1].alive = true; } Debug("客户端已连接: " + clients[clients.Length - 1].ip); } } catch (Exception ex) { Debug("AcceptCallback() 错误: " + ex.Message); } }
如果您需要了解C#异步服务器套接字的话,我推荐您阅读Microsoft MSDN或者使用Google来搜索其他文章。
在MetaTrader 4中,我们需要不断地,而不仅仅是在新订单生成的时候收集信息,这样我们创建“脚本”而不是EA交易程序来完成它。而且,我们还要能够和我们的信号提供者 - MetaTrader 5建立套接字连接。
为了做到这些,我选择从MQL4代码库寻求帮助:"https://www.mql5.com/en/code/9296"。 我找到了一个很好的包含文件(WinSock.mqh) ,它允许我们通过很简单的方法使用套接字。尽管有些人抱怨它的稳定性,我认为它已经能够很好满足我的需求,并且我在测试中没有遇到任何问题。
#include <winsock.mqh> // 从MQL4主页下载 // 下载连接: https://www.mql5.com/zh/code/9296 // 文章: https://www.mql5.com/zh/code/download/9296
现在我们已经有了概念,我们所要做的就是确认交易被所有客户端挨个按照他们识别和执行的格式进行处理和传输。
5.1. 服务器端
就像我们所说的,它是一个EA交易程序,但是它并不关心它被添加到哪个货币图表上。
在启动阶段,它也启动了侦听线程来等待进入的连接:
int OnInit() { string str=""; Print(UTF8_to_ASCII(About())); //--- 启动服务器 Print("启动服务器,端口 ",InpPort,"..."); if(!StartListen(InpPort)) { PrintLogs(); Print("OnInit() - FAILED"); return -1; }
在这个版本里面,EA交易程序不关心连接的客户端,每次有交易发生时,它会给所有客户端发通知,就算没有连接的客户端也一样发。因为我们只需要了解交易,我们使用OnTrade()函数,而将删除OnTick()函数。 在这个函数内,我们查找最新的历史来决定这里是否有我们需要通知的交易。
参考我代码中的注释可以更好了解这一点:
//+------------------------------------------------------------------+ //| OnTrade() - 每次有交易相关活动发生 | //| 的时候. | //+------------------------------------------------------------------+ void OnTrade() { //--- 查找所有的新交易并报告给所有客户端 //--- 24 小时之前. datetime dtStart=TimeCurrent()-60*60*24; //--- 24 小时之后 (加入您生活在 GMT-<小时数>) datetime dtEnd=TimeCurrent()+60*60*24; //--- 选择最后24小时历史. if(HistorySelect(dtStart,dtEnd)) { //--- 遍历所有交易 (从最老到最新). for(int i=0;i<HistoryDealsTotal();i++) { //--- 获得交易订单. ulong ticket=HistoryDealGetTicket(i); //--- 如果是我们感兴趣的交易. if(HistoryDealGetInteger(ticket,DEAL_ENTRY)!=DEAL_ENTRY_STATE) { //Print("Entry type ok."); //--- 检查交易是否比先前报告的更新 if(HistoryDealGetInteger(ticket,DEAL_TIME)>g_dtLastDealTime) { //--- 如果仓位被部分关闭则检查我们是否需要重新开启 if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_OUT) { vUpdateEnabledSymbols(); } //--- 如果相反仓位开启,我们需要重启被禁用的交易品种. else if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT) { //--- 启用指定交易品种 vEnableSymbol(HistoryDealGetString(ticket,DEAL_SYMBOL)); } //--- 检查交易品种是否被启用 if(bIsThisSymbolEnabled(HistoryDealGetString(ticket,DEAL_SYMBOL))) { //--- 创建交易字符串并发送到所有连接的客户端 int cnt=SendToAll(sBuildDealString(ticket)); //--- 服务器出现技术问题 if(cnt<0) { Print("发送新交易失败!"); } //--- 如果没有发给任何人(cnt==0) 或者发给了某些人(cnt>0) else { //--- 更新最后成功发送交易时间. g_dtLastDealTime=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME); } } //--- 不做通知因为交易品种被禁用. else { //--- 更新我们不做通知的最后交易时间. g_dtLastDealTime=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME); } } } } } }
你会发现,当发现有新交易时,我们调用BuildDealString()函数来准备需要传输的数据。 所有数据都用文本格式传输,每个交易都用'<'开头,结尾用'>'。
这可以帮助我们把多个交易数据分开,因为根据TCP/IP协议,每次可能搜到多个交易数据。
//+------------------------------------------------------------------+ //| 这个函数返回交易数据字符串 | //| 例子: | //| EURUSD;BUY;IN;0.01;1.37294 | //| EURUSD;SELL;OUT;0.01;1.37310 | //| EURUSD;SELL;IN;0.01;1.37320 | //| EURUSD;BUY;INOUT;0.02;1.37294 | //+------------------------------------------------------------------+ string sBuildDealString(ulong ticket) { string deal=""; double volume=0; bool bFirstInOut=true; //--- 查找交易量. //--- 如果是 INOUT 必须只包含'IN'的交易量. if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT) { if(PositionSelect(HistoryDealGetString(ticket,DEAL_SYMBOL))) { volume=PositionGetDouble(POSITION_VOLUME); } else { Print("读取交易量失败!"); } } //--- 如果是 'IN' 或者 'OUT' 交易则使用指定交易量.. else { volume=HistoryDealGetDouble(ticket,DEAL_VOLUME); } //--- 生成交易字符串(格式例子: "<EURUSD;BUY;IN;0.01;1.37294>"). int iDealEntry=(int)HistoryDealGetInteger(ticket,DEAL_ENTRY); //--- 如果是 OUT 交易, 而且没有开启仓位了. if(iDealEntry==DEAL_ENTRY_OUT && !PositionSelect(HistoryDealGetString(ticket,DEAL_SYMBOL))) { //--- 为安全起见,我们检查当前交易品种是否有开启仓位. 如果没有,我们使用 //--- 新交易类型 - OUTALL. 这会保证在远端MetaTrader 5的仓位关闭时 //--- 账户内不会有开启的订单这种情况可能在客户端订单量被 //--- 映射为新的数值时,出现很小的手数 //--- 差异. iDealEntry=DEAL_ENTRY_OUTALL; // My own predefined value (this value should not colide with EMUN_DEAL_ENTRY values). } StringConcatenate(deal,"<",AccountInfoInteger(ACCOUNT_LOGIN),";", HistoryDealGetString(ticket,DEAL_SYMBOL),";", Type2String((ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE)),";", Entry2String(iDealEntry),";",DoubleToString(volume,2),";", DoubleToString(HistoryDealGetDouble(ticket,DEAL_PRICE), (int)SymbolInfoInteger(HistoryDealGetString(ticket,DEAL_SYMBOL),SYMBOL_DIGITS)),">"); Print("DEAL:",deal); return deal; }
当您阅读代码时,您可能会惊讶地看到新交易类型 - DEAL_ENTRY_OUTALL. 它是我自己创建的,当我解释了MetaTrader 4端订单量的处理后你就明白了。
另外一个你可能有兴趣的是OnTimer()函数。在初始化阶段,我调用EventSetTimer(1)使OnTimer()函数每秒都调用一次,在函数内部只有一行代码,用来打印服务器端的信息(记录):
//+------------------------------------------------------------------+ //| 每秒打印服务器记录(如果有的话) | //+------------------------------------------------------------------+ void OnTimer() { PrintLogs(); }
每次执行了服务器函数库中的功能后,应该调用这个函数 (PrintLogs) 来打印状态和错误信息。
在服务器端你也可以看到输入参数 StartupType:
enum ENUM_STARTUP_TYPE { STARTUP_TYPE_CLEAR, // CLEAR - 发送每一个账户中出现的新交易. STARTUP_TYPE_CONTINUE // CONTINUE - 在现有仓位没有关闭时不发送交易. }; //--- 输入参数 input ENUM_STARTUP_TYPE InpStartupType=STARTUP_TYPE_CONTINUE; // 启动类型
实际应用中,当把提供者增加到已经有开启仓位的账户的时候(比如跟随锦标赛),其中的信息可能误导客户端,增加了这个参数你就可以选择,你是接收已经有的交易信息还是只接收新开仓位。
这个参数在您第一次应用到某账户或者重新应用到之前运行过的账户,并且您重新启动了计算机,程序或者修改了代码的时候也很重要。
5.2. 客户端
在客户端,我们使用脚本来循环而不断地使用套接字接收函数。因为这个函数是“冻结”方式,脚本会被锁定直到从服务器接收到数据,所以不必担心处理器时间。
//--- 服务器正在运行批开始数据收集和处理 while(!IsStopped()) { Print("客户端:等待交易..."); ArrayInitialize(iBuffer,0); iRetVal=recv(iSocketHandle,iBuffer,ArraySize(iBuffer)<<2,0); if(iRetVal>0) { string sRawData=struct2str(iBuffer,iRetVal<<18); Print("Received("+iRetVal+"): "+sRawData);
这在停止客户端时会引起问题,当您点击“删除脚本”的时候,脚本并不会被删除。你需要点击两次,脚本会因为超时而被删除。如果使用接收函数时加上超时机制,这个问题可能被解决。但是因为我用的是代码库中的已有例子,我把这项工作留给原作者。
当我们接收到数据后,我们要分割数据并在交易在真实账户处理前验证它:
//--- 分割记录 string arrDeals[]; //--- 把原始数据分割成多个交易(如果我们收到了多个交易数据的话) int iDealsReceived=Split(sRawData,"<",10,arrDeals); Print("找到 ",iDealsReceived," 交易订单."); //--- 处理每个记录 //--- 遍历所有收到的交易 for(int j=0;j<iDealsReceived;j++) { //--- 把每笔交易分割得到数值 string arrValues[]; //--- 把每笔交易分割得到数值 int iValuesInDeal=Split(arrDeals[j],";",10,arrValues); //--- 确认收到的交易请求格式正确(数值个数正确) if(iValuesInDeal==6) { if(ProcessOrderRaw(arrValues[0],arrValues[1],arrValues[2], arrValues[3],arrValues[4], StringSubstr(arrValues[5],0,StringLen(arrValues[5])-1))) { Print("订单成功处理."); } else { Print("订单处理失败:\"",arrDeals[j],"\""); } } else { Print("收到的订单无效:\"",arrDeals[j],"\""); //--- 这是数组的最后一项 if(j==iDealsReceived-1) { //--- 这可能是下一个交易不完整的开头部分. sLeftOver=arrDeals[j]; } } }
//+------------------------------------------------------------------+ //| 处理收到的原始数据(文本格式) | //+------------------------------------------------------------------+ bool ProcessOrderRaw(string saccount,string ssymbol,string stype,string sentry,string svolume,string sprice) { //--- 清理 saccount= Trim(saccount); ssymbol = Trim(ssymbol); stype=Trim(stype); sentry=Trim(sentry); svolume= Trim(svolume); sprice = Trim(sprice); //--- 验证 if(!ValidateAccountNumber(saccount)){Print("Invalid account:",saccount);return(false);} if(!ValidateSymbol(ssymbol)){Print("Invalid symbol:",ssymbol);return(false);} if(!ValidateType(stype)){Print("Invalid type:",stype);return(false);} if(!ValidateEntry(sentry)){Print("Invalid entry:",sentry);return(false);} if(!ValidateVolume(svolume)){Print("Invalid volume:",svolume);return(false);} if(!ValidatePrice(sprice)){Print("Invalid price:",sprice);return(false);} //--- 转换 int account=StrToInteger(saccount); string symbol=ssymbol; int type=String2Type(stype); int entry=String2Entry(sentry); double volume= GetLotSize(StrToDouble(svolume),symbol); double price = NormalizeDouble(StrToDouble(sprice),(int)MarketInfo(ssymbol,MODE_DIGITS)); Print("DEAL[",account,"|",symbol,"|",Type2String(type),"|", Entry2String(entry),"|",volume,"|",price,"]"); //--- 执行 ProcessOrder(account,symbol,type,entry,volume,price); return(true); }
因为不是每个人都在他们账户中有10000美元,在客户端需要使用GetLotSize()函数重新计算订单手数,策略在服务器端运行时会进行资金管理,我们在客户端也需要做同样的处理。
我给您提供了“手数映射”- 客户端用户可以指定需要的手数(最小和最大),客户端脚本会做映射:
extern string _1 = "--- LOT MAPPING ---"; extern double InpMinLocalLotSize = 0.01; extern double InpMaxLocalLotSize = 1.00; // 建议比这个数值大 extern double InpMinRemoteLotSize = 0.01; extern double InpMaxRemoteLotSize = 15.00;
//+------------------------------------------------------------------+ //| 计算手数 | //+------------------------------------------------------------------+ double GetLotSize(string remote_lots, string symbol) { double dRemoteLots = StrToDouble(remote_lots); double dLocalLotDifference = InpMaxLocalLotSize - InpMinLocalLotSize; double dRemoteLotDifference = InpMaxRemoteLotSize - InpMinRemoteLotSize; double dLots = dLocalLotDifference * (dRemoteLots / dRemoteLotDifference); double dMinLotSize = MarketInfo(symbol, MODE_MINLOT); if(dLots<dMinLotSize) dLots=dMinLotSize; return (NormalizeDouble(dLots,InpVolumePrecision)); }
客户端支持4位和5位小数的经纪公司,也支持“普通手数” (0.1) 和“迷你手数”(0.01)。因为这个原因,我需要创建新的DEAL_ENTRY类型 - DEAL_OUTALL.
因为客户端做映射,很可能在小的手数没有关闭的时候引起状况。
void ProcessOrder(int account, string symbol, int type, int entry, double volume, double price) { if(entry==OP_IN) { DealIN(symbol,type,volume,price,0,0,account); } else if(entry==OP_OUT) { DealOUT(symbol, type, volume, price, 0, 0,account); } else if(entry==OP_INOUT) { DealOUT_ALL(symbol, type, account); DealIN(symbol,type,volume,price,0,0,account); } else if(entry==OP_OUTALL) { DealOUT_ALL(symbol, type, account); } }
5.3. MetaTrader 5 仓位 vs MetaTrader 4 订单
在开发的时候,我发现了另外一个问题 - 在MetaTrader 5中每个交易品种只能有一个仓位,而在MetaTrader 4中的处理是完全不同的。为了让它们尽可能相近,针对相同交易品种的新交易,我在MetaTrader 4中会开启多个订单,
每个新的'入'交易是一个新订单,而当有一个'出'的交易的时候,我创建了一个三步走的机制:
//+------------------------------------------------------------------+ //| 处理订单 OUT 类型 | //+------------------------------------------------------------------+ void DealOUT(string symbol, int cmd, double volume, double price, double stoploss, double takeprofit, int account) { int type = -1; int i=0; if(cmd==OP_SELL) type = OP_BUY; else if(cmd==OP_BUY) type = OP_SELL; string comment = "OUT."+Type2String(cmd); //--- 寻找相同订单大小且利润大于0的订单 for(i=0;i<OrdersTotal();i++) { if(OrderSelect(i,SELECT_BY_POS)) { if(OrderMagicNumber()==account) { if(OrderSymbol()==symbol) { if(OrderType()==type) { if(OrderLots()==volume) { if(OrderProfit()>0) { if(CloseOneOrder(OrderTicket(), symbol, type, volume)) { Print("已经找到订单大小相同且利润大于0的订单,并且执行完毕。"); return; } } } } } } } } //--- 寻找相同订单大小且不计利润的订单 for(i=0;i<OrdersTotal();i++) { if(OrderSelect(i,SELECT_BY_POS)) { if(OrderMagicNumber()==account) { if(OrderSymbol()==symbol) { if(OrderType()==type) { if(OrderLots()==volume) { if(CloseOneOrder(OrderTicket(), symbol, type, volume)) { Print("已经找到相同大小订单并已执行。"); return; } } } } } } } double volume_to_clear = volume; //--- 寻找订单大小小于所需并且利润大于0的订单 int limit = OrdersTotal(); for(i=0;i<limit;i++) { if(OrderSelect(i,SELECT_BY_POS)) { if(OrderMagicNumber()==account) { if(OrderSymbol()==symbol) { if(OrderType()==type) { if(OrderLots()<=volume_to_clear) { if(OrderProfit()>0) { if(CloseOneOrder(OrderTicket(), symbol, type, OrderLots())) { Print("找到订单大小小于所需且利润大于0的订单并且已经执行."); volume_to_clear-=OrderLots(); if(volume_to_clear==0) { Print("所有所需订单已经关闭."); return; } limit = OrdersTotal(); i = -1; // 因为它会在循环结束前增加,会结果为0. } } } } } } } } //--- 寻找所有手数小于所需的订单 limit = OrdersTotal(); for(i=0;i<limit;i++) { if(OrderSelect(i,SELECT_BY_POS)) { if(OrderMagicNumber()==account) { if(OrderSymbol()==symbol) { if(OrderType()==type) { if(OrderLots()<=volume_to_clear) { if(CloseOneOrder(OrderTicket(), symbol, type, OrderLots())) { Print("订单大小较小的订单已经找到并执行。"); volume_to_clear-=OrderLots(); if(volume_to_clear==0) { Print("所有所需订单都已关闭."); return; } limit = OrdersTotal(); i = -1; // 因为它会在循环结束前增加,会结果为0. } } } } } } } //--- 寻找订单大小较大的订单 for(i=0;i<OrdersTotal();i++) { if(OrderSelect(i,SELECT_BY_POS)) { if(OrderMagicNumber()==account) { if(OrderSymbol()==symbol) { if(OrderType()==type) { if(OrderLots()>=volume_to_clear) { if(CloseOneOrder(OrderTicket(), symbol, type, OrderLots())) { Print("订单大小较大的订单已经找到并执行."); volume_to_clear-=OrderLots(); if(volume_to_clear<0)//关闭太多订单了 { //开新订单抵消关闭多出的部分 DealIN(symbol,type,volume_to_clear,price,OrderStopLoss(),OrderTakeProfit(),account); } else if(volume_to_clear==0) { Print("所有所需订单都已关闭."); return; } } } } } } } } if(volume_to_clear!=0) { Print("一定大小的订单剩余未关闭: ",volume_to_clear); } }
我所开发并且附加在这里的文件肯定可以有所提高,比如用更好的客户服务器协议,聪明一些的通信机制和更好地执行订单,但是我的任务是验证可行性和创建质量可接受的程序,从而每个人可以以私人用途使用它们。
在跟随您自己的策略和所有MQL5锦标赛选手的策略下,它可以工作得很好。MQL4和MQL5所提供的性能和可能性都非常好,足以达到专业化和商业化的程度。我相信您只要用自己的个人电脑和您自己的交易策略,您就可以为MetaTrader 4和MetaTrader 5客户端做很好的信号提供者。
我很希望看到有人能提高我在这里提供的代码并回头提出自己的观点和推荐。如果您有问题我也很愿意回答。同时,我也正在运行测试跟随我喜欢的锦标赛选手,现在已经正常运行了一个星期,如果我发现任何问题,会提供最新信息。
Tsaktuo
请注意,如果您在您的真实帐户中使用前面描述的功能和程序,您需要对它可能造成的所有损失完全负责,您只应该在很好地测试和了解了这里提供的功能后才在您的真实帐户中交易。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...