保持敬畏之心
交易是一场持久战

​MQL4(22):逐渐完善 - 为EA结构添砖加瓦

现在我们将把本章学习到的所有高级功能——包括订单修改、交易上下文检查、预定义变量刷新以及动态手数计算与验证等等整合到之前那个简单的移动平均线交叉 EA 中。

修订后 EA 的结构与代码片段:

  1. 文件头部与全局设定:

    #property copyright "(版权信息)" // 版权声明
    #include <stdlib.mqh>                // 包含标准库,用于 ErrorDescription()
    
    // --- 输入参数 (用户可调) ---
    extern bool   UseDynamicLotSize = true;  // true: 使用动态手数; false: 使用固定手数
    extern double EquityPercent     = 2.0;   // 动态手数 - 风险百分比 (%)
    extern double FixedLotSize      = 0.1;   // 固定手数 - 手数值
    extern double StopLossPips      = 50;    // 止损点数 (pips) - 用于动态手数计算和设置
    extern double TakeProfitPips    = 100;   // 止盈点数 (pips)
    extern int    Slippage          = 5;     // 允许滑点 (pips) - 会被转换为 points
    extern int    MagicNumber       = 123;   // 魔术数字
    extern int    FastMAPeriod      = 10;    // 快速均线
    extern int    SlowMAPeriod      = 20;    // 慢速均线
    
    // --- 全局变量 ---
    int    g_BuyTicket       = 0;     // 存储当前活动的买单订单号 (0 表示无)
    int    g_SellTicket      = 0;     // 存储当前活动的卖单订单号 (0 表示无)
    double g_onePipValue     = 0.0;   // 存储 1 pip 对应的价格值
    int    g_slippagePoints  = 0;   // 存储转换后的滑点 points 值
    int    g_lastErrorCode   = 0;   // 存储最近一次交易操作的错误代码
    
    • 主要变化: 引入了 #include <stdlib.mqh>。增加了控制手数模式和计算的外部变量 (UseDynamicLotSize, EquityPercent, FixedLotSize)。添加了全局变量 g_lastErrorCode 用于错误处理。对全局变量添加了 g_ 前缀以提高可读性。
  2. OnTick() 函数开头 (手数计算与验证):

    void OnTick() {
        // --- 1. 计算指标 ---
        double FastMA = iMA(NULL, 0, FastMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 0);
        double SlowMA = iMA(NULL, 0, SlowMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 0);
    
        // --- 2. 计算并验证本次交易使用的手数 ---
        double lotSizeToUse = 0.0; // 初始化将用于下单的最终手数
    
        if (UseDynamicLotSize) { // 如果启用动态手数模式
            // a. 计算风险金额
            double riskAmount = AccountEquity() * (EquityPercent / 100.0);
    
            // b. 计算每标准手每 Pip 的价值
            // (需要 PipPoint() 函数, 此处假设 g_onePipValue 已在 init 中计算好)
            double tickValuePerPip = MarketInfo(Symbol(), MODE_TICKVALUE);
            int digits = (int)MarketInfo(Symbol(), MODE_DIGITS);
    
            if (digits == 3 || digits == 5) {
                tickValuePerPip *= 10.0; // 调整为每 Pip 价值
            }
    
            // c. 计算动态手数 (需要知道本次交易的止损点数 StopLossPips)
            if (StopLossPips > 0 && tickValuePerPip > 0) {
                // 使用外部输入的 StopLossPips
                lotSizeToUse = (riskAmount / StopLossPips) / tickValuePerPip;
            } else {
                lotSizeToUse = FixedLotSize; // 计算失败用固定值
                Print("动态手数计算失败 (StopLossPips或TickValue无效),使用固定手数 ", FixedLotSize);
            }
        } else { // 使用固定手数模式
            lotSizeToUse = FixedLotSize;
        }
    
        // d. 验证手数是否在 Min/Max 范围内并符合 LotStep 要求
        double minLot = MarketInfo(Symbol(), MODE_MINLOT);
        double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);
        double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
    
        if (lotSizeToUse < minLot) {
            lotSizeToUse = minLot;
        }
        if (lotSizeToUse > maxLot) {
            lotSizeToUse = maxLot;
        }
    
        // 使用 MathRound 进行更精确的步长规范化
        lotSizeToUse = MathRound(lotSizeToUse / lotStep) * lotStep;
    
        // 再次检查是否低于 MinLot, 因为 MathRound 可能会导致低于 minLot (例如 lotSizeToUse 非常接近 minLot 但不是其倍数)
        if (lotSizeToUse < minLot) {
            lotSizeToUse = minLot;
        }
    
        // Print("最终使用手数: ", lotSizeToUse); // 调试输出
    
        // --- 3. 执行交易逻辑 (使用 lotSizeToUse) ---
        // ... (接下来的买入/卖出逻辑块) ...
    }
    
    • 将之前讨论的手数计算(动态或固定)和验证(最小/最大/步长)逻辑放在 start() 函数的起始部分。
    • 这里使用了外部输入的 StopLossPips 来计算动态手数,实际应用中通常应根据当前信号动态确定止损距离再计算手数。
    • 增加了对除零错误的检查,并在计算失败时回退到固定手数。
    • 使用了更精确的 MathRound 方法进行手数步长规范化(推荐在 MQL5 中使用,MQL4 中 NormalizeDouble 更常见但行为是四舍五入)。
    • 计算和验证后的最终有效手数存储在 lotSizeToUse 中,供后续交易逻辑使用。
  3. OnTick() 函数 – 买入逻辑块 (完整增强版):

    // --- 买入逻辑 ---
    if (FastMA > SlowMA && g_BuyTicket == 0) // 条件:金叉且无买单
    {
        // a. 尝试平掉反向卖单 (调用封装函数,包含错误处理和票号更新)
        bool sellClosedOrNotExist = CloseOppositeOrder(g_SellTicket, OP_SELL); // 假设 CloseOppositeOrder 返回 bool
    
        // b. 只有当反向单已处理完毕 (或不存在),才进行开仓
        if (sellClosedOrNotExist)
        {
            // c. 等待交易上下文 & 刷新价格
            if (!WaitForTradeContext()) return(0); // 封装函数,等待失败则本轮 tick 退出
            RefreshRates();
    
            // d. 发送不带 SL/TP 的买单 (ECN 兼容)
            int ticket = OrderSend(Symbol(), OP_BUY, lotSizeToUse, Ask, g_slippagePoints, 0, 0,
                                   "MA Cross Buy", MagicNumber, 0, Green);
    
            // e. 处理开单结果
            if (ticket == -1) { // 开单失败
                HandleTradeError("开立买单", GetLastError()); // 调用通用错误处理函数
                // g_BuyTicket 保持 0
            } else { // 开单成功
                g_BuyTicket = ticket; // 记录新票号
                Print("成功开立买单 #", g_BuyTicket);
                g_SellTicket = 0; // 确认开仓成功后,清除反向票号记录
    
                // f. 为新订单添加 SL/TP (调用封装函数,包含验证和错误处理)
                ModifyOrderWithSLTP(g_BuyTicket, StopLossPips, TakeProfitPips);
            }
        } // end if sellClosedOrNotExist
    } // end if (买入条件)
    
    // --- 卖出逻辑块 (结构类似,方向相反) ---
    // ...
    // return(0);
    // } // end start()
    
    // --- 需要添加/包含以下辅助函数 ---
    // bool CloseOppositeOrder(int& ticketVar, int orderTypeToClose) { ... }
    // bool WaitForTradeContext() { ... while(IsTradeContextBusy)... return true/false; }
    // void HandleTradeError(string operationDesc, int errorCode) { ... GetLastError, Alert + Print ... }
    // bool ModifyOrderWithSLTP(int ticket, double slPips, double tpPips) { ... OrderSelect, 计算 SL/TP 价格, 验证 StopLevel, OrderModify, HandleTradeError ... }
    // double PipPoint(string currencySymbol) { ... }
    // int GetSlippage(string currencySymbol, int slippageInPips) { ... }
    
    • 核心流程: 检查买入条件 -> 处理反向单 -> 等待交易上下文 -> 刷新价格 -> 发送无 SL/TP 的市价单 -> 处理开单结果 -> (如果成功) -> 记录新票号并清除反向票号 -> 为新订单添加 SL/TP(内部包含选择、计算、验证、修改、错误处理)。
    • 增强点体现:
      • 交易上下文检查: 在交易操作前调用。
      • 价格刷新: 在读取 Ask/Bid 前调用。
      • 错误处理: 交易操作后都有错误检查和处理(建议封装)。
      • 账户兼容: 通过 OrderModify 添加 SL/TP。
      • SL/TP 验证: 在添加 SL/TP 的逻辑中实现(建议封装)。
      • 状态管理: 在开仓成功后,才清除反向订单的票号记录 (g_SellTicket = 0)。

以下是如何将之前增强版的 EA 代码修改为使用挂单(以设置买入止损单 Buy Stop 为例)的逻辑和代码片段:

1. 修改处理反向订单的逻辑:

在尝试设置新的买入挂单之前,必须先检查并处理(关闭或删除)任何可能存在的反向订单(即卖单或卖出挂单)。这需要根据 OrderType() 来决定调用 OrderClose() 还是 OrderDelete(),并且为这两种操作都加上交易上下文检查和错误处理。

// --- 处理反向卖单或卖挂单 ---
bool sellClosedOrNotExist = true; // 标记反向单是否已处理完毕或不存在
if (g_SellTicket > 0) // 如果全局变量记录了反向订单号
{
    if (OrderSelect(g_SellTicket, SELECT_BY_TICKET)) // 尝试选中该订单
    {
        if (OrderCloseTime() == 0) // 确认订单当前是活动的(未关闭/未删除)
        {
            int orderType = OrderType(); // 获取订单类型
            bool result = false;       // 操作结果标志
            string actionDesc = "";    // 操作描述,用于日志

            // 等待交易上下文空闲
            if (!WaitForTradeContext()) { // 假设 WaitForTradeContext 是封装好的等待函数
                Print("等待交易上下文超时,无法处理反向订单 #", g_SellTicket);
                sellClosedOrNotExist = false; // 标记处理失败
            } else {
                // 根据订单类型执行相应操作
                if (orderType == OP_SELL) // 如果是已成交的市价卖单
                {
                    RefreshRates(); // 获取最新价格
                    result = OrderClose(g_SellTicket, OrderLots(), Ask, g_slippagePoints, Red); // 平仓
                    actionDesc = "平掉市价卖单";
                }
                else if (orderType == OP_SELLSTOP || orderType == OP_SELLLIMIT) // 如果是未成交的卖出挂单
                {
                    result = OrderDelete(g_SellTicket, Red); // 删除挂单
                    actionDesc = "删除卖出挂单";
                }
                else {
                    // 类型不符或其他情况,可能直接清除记录
                    Print("订单 #", g_SellTicket, " 类型为 ", orderType, ", 非预期反向订单,清除记录.");
                    result = true; // 视为已处理(因为不再是需要平仓/删除的目标)
                }

                // 处理操作结果
                if (!result && actionDesc != "") // 如果尝试了操作但失败了
                {
                    HandleTradeError(actionDesc, GetLastError()); // 调用统一错误处理函数
                    sellClosedOrNotExist = false; // 标记处理失败,不能继续开新单
                }
                else if (result) // 操作成功
                {
                    Print(actionDesc, " #", g_SellTicket, " 成功.");
                    g_SellTicket = 0; // 清除全局变量中的记录
                }
            } // end if context available
        } else {
            // 订单已被关闭或取消
            g_SellTicket = 0; // 清除记录
        }
    } else {
        // 无法选中订单(例如,订单号无效或已被手动处理)
        g_SellTicket = 0; // 清除记录
    }
} // end if g_SellTicket > 0

2. 设置新的买入止损挂单:

关键区别在于:挂单可以直接在 OrderSend() 中设置 SL/TP,不需要后续的 OrderModify() 步骤。但下单前的价格计算和验证流程更为复杂。

// --- 设置新的买入止损挂单 ---
if (sellClosedOrNotExist) // 必须在反向单处理完毕后才能继续
{
    // a. 计算停止级别对应的价格距离和缓冲距离
    double stopLevelDist = MarketInfo(Symbol(), MODE_STOPLEVEL) * Point;
    double priceBuffer = 5 * g_onePipValue; // 5 pips 缓冲

    // b. 计算并验证挂单价格 (Pending Price)
    RefreshRates(); // 获取最新 Ask 价用于计算上边界
    double upperStopLevelMarket = Ask + stopLevelDist; // 挂单价必须高于此边界
    // (示例:基于当前 K 线最高价 High[0] 设置)
    double pendingPrice = NormalizeDouble(High[0] + (PendingPips * g_onePipValue), Digits);
    // 验证: 挂单价是否满足距离 Ask 的最小距离要求
    if (pendingPrice <= upperStopLevelMarket)
    {
        pendingPrice = NormalizeDouble(upperStopLevelMarket + priceBuffer, Digits); // 自动调整
        Print("买入止损挂单价无效 (距离 Ask 过近),自动调整为: ", pendingPrice);
    }

    // c. 计算相对于挂单价格的 SL 和 TP
    double buyStopLoss = 0;
    double buyTakeProfit = 0;
    if (StopLossPips > 0) buyStopLoss = NormalizeDouble(pendingPrice - (StopLossPips * g_onePipValue), Digits);
    if (TakeProfitPips > 0) buyTakeProfit = NormalizeDouble(pendingPrice + (TakeProfitPips * g_onePipValue), Digits);

    // d. 验证 SL 和 TP 是否满足相对于挂单价格的最小距离 (不考虑点差)
    // 重新计算相对于挂单价的边界
    double lowerStopLevelPending = NormalizeDouble(pendingPrice - stopLevelDist, Digits);
    double upperStopLevelPending = NormalizeDouble(pendingPrice + stopLevelDist, Digits);
    bool adjusted = false;
    // 验证买单 SL (必须 <= lowerStopLevelPending)
    if (buyStopLoss > 0 && buyStopLoss >= lowerStopLevelPending) // 如果 SL 高于(或等于)下边界,无效
    {
        buyStopLoss = NormalizeDouble(lowerStopLevelPending - priceBuffer, Digits); // 调整到边界之下
        adjusted = true;
    }
    // 验证买单 TP (必须 >= upperStopLevelPending)
    if (buyTakeProfit > 0 && buyTakeProfit <= upperStopLevelPending) // 如果 TP 低于(或等于)上边界,无效
    {
        buyTakeProfit = NormalizeDouble(upperStopLevelPending + priceBuffer, Digits); // 调整到边界之上
        adjusted = true;
    }
    if (adjusted) Print("买入止损挂单的 SL/TP 因 StopLevel 被自动调整.");

    // e. 等待交易上下文并发送挂单指令 (包含 SL/TP)
    if (!WaitForTradeContext()) return(0);
    // 假设 lotSizeToUse 已计算并验证
    int ticket = OrderSend(Symbol(), OP_BUYSTOP, lotSizeToUse, pendingPrice, g_slippagePoints,
                           buyStopLoss, buyTakeProfit, "MA Cross Buy Stop", MagicNumber, 0, Green);

    // f. 处理挂单设置结果
    if (ticket == -1) {
        HandleTradeError("设置买入止损挂单", GetLastError());
        // g_BuyTicket 保持 0
    } else {
        g_BuyTicket = ticket; // 记录新挂单的订单号
        // g_SellTicket 必须为 0 (因为前面已处理)
        Print("成功设置买入止损挂单 #", g_BuyTicket, " at ", pendingPrice);
    }
} // end if sellClosedOrNotExist

逻辑解释 (挂单):

  • 处理反向单: 代码块首先检查并处理(平仓或删除)可能存在的反向卖单或卖挂单,并包含错误处理。
  • 挂单流程:
    • 计算与验证挂单价: 计算目标挂单价(示例基于 High[0]),然后检查它是否满足距离当前 Ask 价的最小停止级别要求,并在必要时自动调整。
    • 计算 SL/TP: 基于(可能已调整的)挂单价计算止损和止盈价。
    • 验证 SL/TP: 再次检查计算出的 SL 和 TP 是否满足相对于挂单价格的最小停止级别要求(注意此时比较的基准是挂单价,且不考虑点差),并在必要时调整。
    • 下单: 等待交易上下文后,调用 OrderSend() 发送 OP_BUYSTOP 挂单,直接包含已验证和调整的挂单价、SL、TP。
    • 错误处理: 对 OrderSend() 结果进行错误处理。
    • 状态更新: 成功设置挂单后,记录订单号到 g_BuyTicket,并确保 g_SellTicket 为 0。

结论: 尽管增加了这些错误处理、价格验证和动态手数计算等复杂功能,使得代码量增大,但这些增强版的EA所使用的核心交易策略(移动平均线交叉)与之前的简单版本是完全相同的。这些新增的代码主要是为了让EA更加健壮、安全、并能适应不同的环境。

在接下来的教学中,我们将学习如何运用函数来封装这些复杂的逻辑,从而达到重用代码、简化结构、提高可读性和可维护性的目的。

赞(0)
未经允许不得转载:图道交易 » ​MQL4(22):逐渐完善 - 为EA结构添砖加瓦
分享到