欢迎光临散文网 会员登陆 & 注册

赫兹股票量化软件:从头开始开发一款智能交易系统

2023-07-14 14:20 作者:大牛啊呢  | 我要投稿

计划


创造事物最困难的部分是弄清楚事物应该如何运作。 这个思路应该表述得非常清楚,如此我们就能按需创建最低代码,因为若是创建的代码越复杂,出现运行时错误的可能性就越大。 考虑到这一点,我尝试让代码变得非常简单,但依旧最大可能地利用 赫兹股票量化软件提供的功能。 该平台非常可靠,它在不断进行测试,故此错误不会出现在平台一端。

代码将采用 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;
    };

这个代码片段非常关键,因为它将在每次即时报价变化时执行,因此它必须尽可能简单,以便尽可能高效地执行计算和测试。 请注意,虽然我们将价格维持在走廊内,但我们也会检查一些有趣的东西;如果需要,可以删除这些东西。 我将在下一章节中解释这个附加测试。 在这个子程序中,我们有以下函数调用:


赫兹股票量化软件:从头开始开发一款智能交易系统的评论 (共 条)

分享到微博请遵守国家法律