赫兹股票量化软件:从头开始开发一款智能交易系统
计划
创造事物最困难的部分是弄清楚事物应该如何运作。 这个思路应该表述得非常清楚,如此我们就能按需创建最低代码,因为若是创建的代码越复杂,出现运行时错误的可能性就越大。 考虑到这一点,我尝试让代码变得非常简单,但依旧最大可能地利用 赫兹股票量化软件提供的功能。 该平台非常可靠,它在不断进行测试,故此错误不会出现在平台一端。
代码将采用 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 订单存在一个问题,这并非 赫兹股票量化软件系统或交易服务器的故障。 它与市场中不断出现的波动性本身有关。 从理论上讲,价格应该是线性波动的,没有回滚;但有时我们会遇到高波动性,这会在烛条内部造成跳空缺口。 当这些跳空缺口出现在止损或止盈订单的价位时,这些点位将不会被触发,因此,将不会平仓。 当用户移动这些点位时,价格也可能超出止损和止盈形成的走廊。 在这种情况下,订单也不会平仓。 这是一种非常危险的状况,无法预测。 作为一名程序员,您必须提供一个相应的机制,以尽量减少可能的危害。
为了刷新价格,并试图将其维持在走廊内,我们将使用两个子例程。 第一个如下:
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;
};
这个代码片段非常关键,因为它将在每次即时报价变化时执行,因此它必须尽可能简单,以便尽可能高效地执行计算和测试。 请注意,虽然我们将价格维持在走廊内,但我们也会检查一些有趣的东西;如果需要,可以删除这些东西。 我将在下一章节中解释这个附加测试。 在这个子程序中,我们有以下函数调用: