从头开始开发一款智能交易系统
概述
金融市场的新用户数量不断增加。 或许他们当中的许多人甚至不知道订单系统是如何工作的。 然而,也有一些用户真的想知道发生了什么。 他们试图了解这一切是如何运作的,从而能够掌控局势。
当然,MetaTrader 5 提供了对交易持仓的高度控制。 然而,对于经验不足的用户来说,仅用手动功能下订单可能会非常困难和危险。 甚至,如果有人想交易期货合约,在几乎没有时间下单的情况下,这样交易很可能会演变为一场噩梦;因为您必须及时正确地填写所有字段,但这显然需要时间,而若填写出错,您可能会错失良机,甚至赔钱。
现在,如果我们使用智能交易系统(EA)能否让事情变得更容易呢? 在这种情况下,您可以指定一些细节,例如杠杆率,或者您所能承受的损失,以及您想赚多少(以货币计算,而非含义模糊的“点数”或“点值”)。 然后在图表上用鼠标指定入场的价位,并标记是买入还是卖出...
计划
创造事物最困难的部分是弄清楚事物应该如何运作。 这个思路应该表述得非常清楚,如此我们就能按需创建最低代码,因为若是创建的代码越复杂,出现运行时错误的可能性就越大。 考虑到这一点,我尝试让代码变得非常简单,但依旧最大可能地利用 MetaTrader 5 提供的功能。 该平台非常可靠,它在不断进行测试,故此错误不会出现在平台一端。
代码将采用 OOP(面向对象编程)。 这种方法能够隔离代码,并促进其维护和未来的开发,预防我们想要添加新功能,并进行改进。
尽管本文讨论的 EA 是出于在 B3(巴西交易所)上进行交易而设计的,特别是为期货(迷你指数和迷你美元)交易而设计的,但只需略微修改即可扩展到所有市场。 为了另事情变得更简单,且不必列举或检查交易资产,我们将使用以下枚举:
enum eTypeSymbolFast {WIN, WDO, OTHER};
如果您想交易其它资产,需用到某些特殊功能,请将其添加到枚举之中。 这也需要在代码中做一些微小的修改,但用枚举会更容易一些,因为它还降低了出错的可能性。 代码中一个有趣的部分是 AdjustPrice 函数:
double AdjustPrice(const double arg) { double v0, v1; if(m_Infos.TypeSymbol == OTHER) return arg; v0 = (m_Infos.TypeSymbol == WDO ? round(arg * 10.0) : round(arg)); v1 = fmod(round(v0), 5.0); v0 -= ((v1 != 0) || (v1 != 5) ? v1 : 0); return (m_Infos.TypeSymbol == WDO ? v0 / 10.0 : v0); };
此函数将调整价格中用到的数值,从而在图表准确定位价格线。 为什么我们不能简单地在图表上放一条线呢? 这是因为一些资产在价格之间存在一定的阶梯。 对于 WDO (迷你美元) 这个阶梯是 0.5 个点。 对于 WIN (迷你指数) 个阶梯是 5 个点,而对于股票,它是 0.01 个点。 换言之,不同资产的点数值不同。 它会把价格调整为正确的即时报价数值,从而该数值能在订单中正确使用,否则填写有错的订单会被服务器拒绝。
若无此函数,可能很难知道订单中所采用的数值是否正确。 故而,服务器就会通知订单填写错误,并阻止其执行。 现在,我们继续讨论智能交易系统的核心函数:CreateOrderPendent。 函数如下:
ulong CreateOrderPendent(const bool IsBuy, const double Volume, const double Price, const double Take, const double Stop, const bool DayTrade = true) { double last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST); ZeroMemory(TradeRequest); ZeroMemory(TradeResult); TradeRequest.action = TRADE_ACTION_PENDING; TradeRequest.symbol = m_szSymbol; TradeRequest.volume = Volume; TradeRequest.type = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP)); TradeRequest.price = NormalizeDouble(Price, m_Infos.nDigits); TradeRequest.sl = NormalizeDouble(Stop, m_Infos.nDigits); TradeRequest.tp = NormalizeDouble(Take, m_Infos.nDigits); TradeRequest.type_time = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC); TradeRequest.stoplimit = 0; TradeRequest.expiration = 0; TradeRequest.type_filling = ORDER_FILLING_RETURN; TradeRequest.deviation = 1000; TradeRequest.comment = "Order Generated by Experts Advisor."; if(!OrderSend(TradeRequest, TradeResult)) { MessageBox(StringFormat("Error Number: %d", TradeResult.retcode), "Nano EA"); return 0; }; return TradeResult.order; };
该函数非常简单,就是为了安全而设计的。 我们将在这里创建一个 OCO(一笔取消其它)订单,该订单将被发送到交易服务器。 请注意,我们使用的是 LIMIT(限价) 或 STOP(破位) 订单。 这是因为这类订单更简单,即使在价格突然波动的情况下也能保证执行。
所采用用的订单类型取决于交易工具的执行价格和当前价格,以及您入场操作是买入还是卖出。 这是通过以下方式实现的:
TradeRequest.type = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
通过在以下代码行中指定交易工具,也可以创建 CROSS(交叉)订单:
TradeRequest.symbol = m_szSymbol;
但在这样做时,您还需要添加一些代码,以便通过交叉订单系统处理持仓或挂单,因为您会有一个“错误”的图表。 我们来看一个示例。 您可以在完整指数图表(IND)上交易迷你指数(WIN),但若您在 IND 图表上使用 MetaTrader 5 时,它不会显示持仓或 WIN 挂单。 因此,有必要添加代码,从而令订单可见。 这可以通过读取持仓数值,并在图表上用线条示意来实现。 这在交易和跟踪品种交易历史时非常有用。 例如,当您使用 CROSS(交叉)订单时,您可以依据 WIN$ 图表(迷你指数历史图表)交易 WIN(迷你指数)。
接下来,请注意以下代码行:
TradeRequest.price = NormalizeDouble(Price, m_Infos.nDigits); TradeRequest.sl = NormalizeDouble(Stop, m_Infos.nDigits); TradeRequest.tp = NormalizeDouble(Take, m_Infos.nDigits);
这三行将创建OCO订单止损水平和持仓未平仓价格。 如果您交易的是短线订单(可能只持续几秒钟),不使用 OCO 订单是不可取的,因为波动会令价格在点位间跳转时,没有明确的方向。 当您采用 OCO 时,交易服务器自身会关注我们的仓位。 OCO 订单如下所示。
在编辑窗口中,相同的订单如下所示:
一旦填完所有必填字段后,服务器将接管订单。 一旦达到最大盈利或最大亏损,系统将平仓。 但若您没有指定最大盈利或最大亏损,订单可能会一直保持,直到另一个事件发生。 如果订单类型设置为日内交易,系统将在交易日结束时关闭。 否则,该笔持仓将继续持有,直到您手动平仓,或者直到没有更多资金来保有持仓。
一些智能交易系统使用订单来平仓:一旦开仓,就会发送一笔逆反的订单,在指定的点位平仓,且交易量相同。 但在某些情况下,这可能不起作用,因为如果资产在交易期间出于某种原因进入拍卖,则挂单可能会被取消,并应予以替换。 这将另 EA 操作复杂化,因为您需要加入检查哪些订单处于有效状态,哪些订单处于无效状态;如果出现任何错误,若无任何标准则 EA 将会一笔接一笔地发送订单。
void Initilize(int nContracts, int FinanceTake, int FinanceStop, color cp, color ct, color cs, bool b1) { string sz0 = StringSubstr(m_szSymbol = _Symbol, 0, 3); double v1 = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) / SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.Id = ChartID(); m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER)); m_Infos.nDigits = (int) SymbolInfoInteger(m_szSymbol, SYMBOL_DIGITS); m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN)); m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume); m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume); m_Infos.IsDayTrade = b1; CreateHLine(m_Infos.szHLinePrice, m_Infos.cPrice = cp); CreateHLine(m_Infos.szHLineTake, m_Infos.cTake = ct); CreateHLine(m_Infos.szHLineStop, m_Infos.cStop = cs); ChartSetInteger(m_Infos.Id, CHART_COLOR_VOLUME, m_Infos.cPrice); ChartSetInteger(m_Infos.Id, CHART_COLOR_STOP_LEVEL, m_Infos.cStop); };
上面的例程负责初始化用户指示的 EA 数据 — 它创建一笔 OCO 订单。 我们只需要在这个程序中做以下修改。
m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER));
在此,如果您需要一些特定的信息,我们将在当前品种的基础上添加交易品种类型。
m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN)); m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume); m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume);
以上三行是为了正确创建订单而进行的必要调整。 nContracts 是一个杠杆系数,选取 1、2、3 等值。 换句话说,您不需要知道要交易品种的最小交易量。 您真正需要的就是指出这个最小交易量的杠杆系数。 例如,如果所需的最小交易量为 5 份合同,并且您指定的杠杆系数为 3,则系统将开立 15 份合约的订单。 基于用户指定的参数,另外两行相应地设置了止盈和止损。 级别随订单交易量调整:如果订单增加,级别降低,反之亦然。 有了这段代码,您在开仓时就不必进行计算 — EA 会自行计算所有东西:您指示 EA 交易的金融工具,杠杆系数,您想赚多少钱,准备亏损多少钱,而 EA 将为您创建一笔相应的订单。
inline void MoveTo(int X, int Y, uint Key) { int w = 0; datetime dt; bool bEClick, bKeyBuy, bKeySell; double take = 0, stop = 0, price; bEClick = (Key & 0x01) == 0x01; //Left mouse button click bKeyBuy = (Key & 0x04) == 0x04; //Pressed SHIFT bKeySell = (Key & 0x08) == 0x08; //Pressed CTRL ChartXYToTimePrice(m_Infos.Id, X, Y, w, dt, price); ObjectMove(m_Infos.Id, m_Infos.szHLinePrice, 0, 0, price = (bKeyBuy != bKeySell ? AdjustPrice(price) : 0)); ObjectMove(m_Infos.Id, m_Infos.szHLineTake, 0, 0, take = price + (m_Infos.TakeProfit * (bKeyBuy ? 1 : -1))); ObjectMove(m_Infos.Id, m_Infos.szHLineStop, 0, 0, stop = price + (m_Infos.StopLoss * (bKeyBuy ? -1 : 1))); if((bEClick) && (bKeyBuy != bKeySell)) CreateOrderPendent(bKeyBuy, m_Infos.Volume, price, take, stop, m_Infos.IsDayTrade); ObjectSetInteger(m_Infos.Id, m_Infos.szHLinePrice, OBJPROP_COLOR, (bKeyBuy != bKeySell ? m_Infos.cPrice : clrNONE)); ObjectSetInteger(m_Infos.Id, m_Infos.szHLineTake, OBJPROP_COLOR, (take > 0 ? m_Infos.cTake : clrNONE)); ObjectSetInteger(m_Infos.Id, m_Infos.szHLineStop, OBJPROP_COLOR, (stop > 0 ? m_Infos.cStop : clrNONE)); };
上述代码将显示要创建的订单。 它使用鼠标来显示订单将要放置的价位。 您还要通知 EA 是想买入(按住 SHIFT 键),还是想卖出(按住 CTRL 键)。 一旦单击鼠标左键后,此时将创建一笔挂单。
如果您需要显示更多数据,例如盈亏平衡点,请将相关对象添加到代码之中。
现在我们拥有了一个完整的 EA,它可以工作,并创建 OCO 订单。 但这里的一切并非都是完美的...
问题出在 OCO 订单
OCO 订单存在一个问题,这并非 MetaTrader 5 系统或交易服务器的故障。 它与市场中不断出现的波动性本身有关。 从理论上讲,价格应该是线性波动的,没有回滚;但有时我们会遇到高波动性,这会在烛条内部造成跳空缺口。 当这些跳空缺口出现在止损或止盈订单的价位时,这些点位将不会被触发,因此,将不会平仓。 当用户移动这些点位时,价格也可能超出止损和止盈形成的走廊。 在这种情况下,订单也不会平仓。 这是一种非常危险的状况,无法预测。 作为一名程序员,您必须提供一个相应的机制,以尽量减少可能的危害。
为了刷新价格,并试图将其维持在走廊内,我们将使用两个子例程。 第一个如下:
void UpdatePosition(void) { for(int i0 = PositionsTotal() - 1; i0 >= 0; i0--) if(PositionGetSymbol(i0) == m_szSymbol) { m_Take = PositionGetDouble(POSITION_TP); m_Stop = PositionGetDouble(POSITION_SL); m_IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY; m_Volume = PositionGetDouble(POSITION_VOLUME); m_Ticket = PositionGetInteger(POSITION_TICKET); } };
它将在 OnTrade 中被调用,即 MetaTrader 5 在每次持仓变化时调用的函数。 下一个要用到的子例程则由 OnTick 调用。 它检查并确保价格在走廊范围内,或在 OCO 订单的范围内。 其如下所示:
inline bool CheckPosition(const double price = 0, const int factor = 0) { double last; if(m_Ticket == 0) return false; last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST); if(m_IsBuy) { if((last > m_Take) || (last < m_Stop)) return ClosePosition(); if((price > 0) && (price >= last)) return ClosePosition(factor); } else { if((last < m_Take) || (last > m_Stop)) return ClosePosition(); if((price > 0) && (price <= last)) return ClosePosition(factor); } return false; };
这个代码片段非常关键,因为它将在每次即时报价变化时执行,因此它必须尽可能简单,以便尽可能高效地执行计算和测试。 请注意,虽然我们将价格维持在走廊内,但我们也会检查一些有趣的东西;如果需要,可以删除这些东西。 我将在下一章节中解释这个附加测试。 在这个子程序中,我们有以下函数调用:
bool ClosePosition(const int arg = 0) { double v1 = arg * m_VolMinimal; if(!PositionSelectByTicket(m_Ticket)) return false; ZeroMemory(TradeRequest); ZeroMemory(TradeResult); TradeRequest.action = TRADE_ACTION_DEAL; TradeRequest.type = (m_IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY); TradeRequest.price = SymbolInfoDouble(m_szSymbol, (m_IsBuy ? SYMBOL_BID : SYMBOL_ASK)); TradeRequest.position = m_Ticket; TradeRequest.symbol = m_szSymbol; TradeRequest.volume = ((v1 == 0) || (v1 > m_Volume) ? m_Volume : v1); TradeRequest.deviation = 1000; if(!OrderSend(TradeRequest, TradeResult)) { MessageBox(StringFormat("Error Number: %d", TradeResult.retcode), "Nano EA"); return false; } else m_Ticket = 0; return true; };
该函数将按指定交易量平仓,并起到保护作用。 然而,请不要忘记,您必须连接服务器,因为该函数在 MetaTrader 5 客户端中运行 — 如果与服务器的连接失败,该函数将完全无效。
查看最后两段代码,从中可以发现我们能够在某个点位按照给定的交易量了结。 如此这般操作,我们即可部分平仓,亦可降低爆仓。 我们来看看如何使用这个函数。
处理部分订单
部分订单是许多交易者喜欢并利用的东西。 智能交易系统允许部分平仓操作,但我不会展示如何实现这样的代码,因为部分订单应该是另一个问题的主题。 然而,如果您打算实现部分平仓的操作,只需调用 CheckPosition 例程,指定执行订单的价格和交易量,而 EA 则完成其余的操作。
我要说的是,部分订单是一种特殊情况,因为它们是非常独立的,很难创建一个通用的解决方案来满足所有人。 在此使用动态数组并不合适,因为您可能会摇摆不定 — 它在日间交易时,您不能关闭 EA。 如果出于任何原因需要关闭 EA,则数组解决方案将无法工作。 您需要用到一些存储介质,其中的数据格式将取决于您将如何处理这些数据。
无论如何,您都应该尽可能避免利用开仓订单来部分关闭,因为如此做会导致头痛的风险巨大。 我来解释一下:假设您用 3 倍杠杆开立多头仓位,且您打算以 1 倍杠杆持仓的同时,赚取 2 倍盈利。 这可以通过以 2 倍杠杆做空来实现。 然而,如果您的 EA 发送了一笔市价卖单,那么可能会发生这种情况,在卖单实际执行之前,波动会导致价格上涨,并触发您的止盈。 在这种情况下,EA 将在不利方向开立一笔新的空头持仓。 或者,您可以发送一笔 Sell Limit 或 Sell Stop,以以 2 倍杠杆降低持仓。 这似乎是一个适当的解决方案。 但是,如果在价格触及部分平仓点位之前发出了另一笔订单,您可能遭遇一个非常不爽的惊喜:持仓将被停止,且稍晚一点,订单再次开立持仓,并会增加损失。 如果波动性变得很强,情况将与我们前面提到的一样。
因此,在我看来,作为一名程序员,部分订单的最佳选择是模仿发送市价订单。 但您务必非常小心,不要超过持仓的交易量。 在该 EA 中,我就是这样做的。 如果您愿意,可以实现其它得部分平仓方法。
结束语
为交易创建一款智能交易系统并不像一些人想象的那么简单;与编程时经常遇到的其它一些问题相比,这或许非常简单,然而,构建足够稳定和可靠的系统来从事冒险是一项艰巨的任务。 在本文中,我提出了一些建议,可以让那些开始使用 MetaTrader 5,但不具备编写 EA 所需知识之人的生活更轻松。 这是一个良好的开端,因为该 EA 不开单,只是以更可靠的方式帮助下订单。 一旦放下订单,EA 不会进行任何操作,接下来 MetaTrader 5 接手操作,除了上面提到的代码片段。
本文中介绍的智能交易系统可以遵照各种方式进行改进,以便处理参数集合,但这将需要更多代码,使其更独立于MetaTrader 5。
该 EA 的巨大成功在于它利用 MetaTrader 5 本身来执行代码中没有的动作,因此它非常稳定可靠。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10085