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

​MQL4(23):封装手数计算与验证函数

在前面的章节中,我们逐步构建了包含下单、错误处理、价格验证、手数计算等功能的代码逻辑。现在,我们将学习如何将这些代码块封装成可重用的函数。这样做的好处是:

  • 将复杂的细节(如手数计算、价格验证等)隐藏在函数内部,使得 OnTick() 等核心函数的代码更简洁、更易于理解,更能聚焦于交易策略本身。
  • 提高可维护性,如果需要修改某项功能(例如手数计算方法),只需修改对应的函数即可,而无需在主代码中到处查找和修改。
  • 代码复用: 定义好的函数可以在同一个 EA 的不同地方调用,甚至可以通过 #include 文件的方式在多个不同的 EA 项目中共享。

封装手数计算逻辑 (CalcLotSize 函数)

我们将动态/固定手数计算逻辑封装成一个函数。

函数定义:

/*根据风险百分比或固定值计算交易手数
 * 参数:argUseDynamicLotSize  true: 使用动态手数; false: 使用固定手数
 * 参数:argEquityPercent      动态手数模式下的风险百分比 (例如 2.0 代表 2%)
 * 参数:argStopLossPips       动态手数模式下当前交易的止损点数 (pips)
 * 参数:argFixedLotSize       固定手数模式下使用的手数
 * 返回 double               计算出的、尚未验证的手数;若计算失败则返回固定手数或 0
 */
double CalcLotSize(bool argUseDynamicLotSize, double argEquityPercent, double argStopLossPips,
                   double argFixedLotSize)
{
    double calculatedLotSize = 0.0; // 用于存储计算结果

    if (argUseDynamicLotSize) // 如果启用动态手数
    {
        // 1. 计算风险金额
        double riskAmount = AccountEquity() * (argEquityPercent / 100.0);

        // 2. 计算每标准手每 Pip 的价值
        double tickValueRaw = MarketInfo(Symbol(), MODE_TICKVALUE);
        double tickValuePerPip = tickValueRaw;
        int digits = (int)MarketInfo(Symbol(), MODE_DIGITS);
        if (digits == 3 || digits == 5) tickValuePerPip *= 10.0;

        // 3. 计算手数 (确保除数有效)
        if (argStopLossPips > 0 && tickValuePerPip > 0)
        {
            calculatedLotSize = (riskAmount / argStopLossPips) / tickValuePerPip;
        }
        else // 计算条件不足,可能无法计算或无意义
        {
            Print("CalcLotSize 错误:无法计算动态手数 (止损点数或TickValue无效)。将使用固定手数: ", argFixedLotSize);
            calculatedLotSize = argFixedLotSize; // 使用固定手数作为后备
        }
    }
    else // 使用固定手数
    {
        calculatedLotSize = argFixedLotSize;
    }

    // 返回计算或指定的初步手数 (还需后续验证)
    return(calculatedLotSize);
}
  • 函数声明: double CalcLotSize(...) 定义了函数名、返回类型 (double) 和接收的参数列表。
  • 参数: 原来的外部变量 DynamicLotSize, EquityPercent, StopLoss, FixedLotSize 现在作为函数的输入参数 argDynamicLotSize, argEquityPercent, argStopLossPips, argFixedLotSize 传入。
  • 函数体: 内部逻辑与之前相同,但使用的是传入的参数值。增加了对 argStopLossPipstickValuePerPip 的有效性检查,并在计算失败时提供了回退到固定手数的逻辑。
  • 返回值: 通过 return(calculatedLotSize); 返回计算或指定的初步手数。

函数的位置: 这个函数定义可以放在 MQL4源文件的全局区域(即所有主函数之外),或者放在一个单独的 .mqh 包含文件中,然后在主 EA 文件顶部使用 #include "your_functions.mqh" 来引入。

如何调用 CalcLotSize 函数:

  1. 首先,在主 EA 文件中仍然需要定义相应的外部变量,以便用户可以设置参数:
    extern bool   UseDynamicLotSize = true;
    extern double EquityPercent     = 2.0;
    extern double FixedLotSize      = 0.1;
    // 注意:这里的 StopLoss 通常不应是固定输入,而是在OnTick() 中根据信号计算
    // extern double StopLossPips   = 50; // 仅为演示参数传递
    
  2. OnTick() 函数内部需要计算手数的地方,调用 CalcLotSize 函数,并将外部变量的值(或动态计算的值)作为实参 (arguments) 传递进去:
    void OnTick()
    {
        // ... 其他代码 ...
        double currentTradeStopLossPips = ...; // 获取当前交易信号的止损点数
    
        // 调用函数计算手数,并将结果存储到 OnTick() 的局部变量 lotSize 中
        double lotSize = CalcLotSize(UseDynamicLotSize, EquityPercent, currentTradeStopLossPips, FixedLotSize);
    
        // ... 后续使用 lotSize (在验证后) ...
    }
    
  • 参数传递: 调用时,外部变量 UseDynamicLotSize, EquityPercent, 以及变量 currentTradeStopLossPips, FixedLotSize 的值被复制并传递给函数内部对应的 arg* 参数。
  • 返回值: 函数计算出的结果通过 return 语句返回,并被赋值给 OnTick() 函数内部的局部变量 lotSize
  • 变量作用域: 请注意,OnTick() 函数中的 lotSizeCalcLotSize() 函数内部用于计算的 calculatedLotSize(或原文中的 LotSize)是两个独立的变量,它们位于不同的作用域(一个在 OnTick() 内,一个在 CalcLotSize 内),即使名称可能相同,也互不影响。

封装手数验证逻辑 (VerifyLotSize 函数)

同样地,我们可以将手数验证逻辑(检查最小/最大手数、根据步长规范化)封装成一个独立的函数。这样做的好处是,无论您将来采用何种方法计算手数(可能是 CalcLotSize,也可能是其他自定义方法),都可以调用这个通用的验证函数来确保最终手数的合规性。

函数定义:

/**
 * @brief 验证并规范化手数,确保其符合经纪商的最小/最大手数及手数步长要求
 * @param argLotSize    需要验证和规范化的手数
 * @return double       返回符合要求的手数;如果输入无效,可能返回 MinLot
 */
double VerifyLotSize(double argLotSize)
{
    string symbol = Symbol(); // 获取当前品种
    // 1. 验证最小手数
    double minLot = MarketInfo(symbol, MODE_MINLOT);
    if (argLotSize < minLot)
    {
        Print("手数 ", argLotSize, " < 最小手数 ", minLot, ". 调整为: ", minLot);
        argLotSize = minLot;
    }

    // 2. 验证最大手数
    double maxLot = MarketInfo(symbol, MODE_MAXLOT);
    if (argLotSize > maxLot)
    {
        Print("手数 ", argLotSize, " > 最大手数 ", maxLot, ". 调整为: ", maxLot);
        argLotSize = maxLot;
    }

    // 3. 根据手数步长进行规范化
    double lotStep = MarketInfo(symbol, MODE_LOTSTEP);
    // 使用 MathRound 更精确地对齐到步长 (向下取整到步长倍数)
    argLotSize = MathRound(argLotSize / lotStep) * lotStep;

    // 4. 再次检查规范化后是否低于最小手数 (因为 MathRound 可能向下取整)
    if (argLotSize < minLot)
    {
         argLotSize = minLot;
    }

    // 返回最终验证和规范化后的手数
    return(argLotSize);
}
  • 输入参数: 函数接收一个 double 类型的参数 argLotSize,代表需要验证的手数。
  • 函数体: 内部执行了检查最小手数、最大手数以及根据 MODE_LOTSTEP 进行规范化的逻辑。这里推荐使用 MathRound 进行步长对齐,比 NormalizeDouble 更能保证结果是步长的整数倍,并在最后再次检查是否低于 minLot
  • 返回值: 返回经过验证和规范化(调整)后的手数。

如何调用 VerifyLotSize 函数: 在 OnTick() 函数中,在计算出初步的手数之后,但在调用 OrderSend 之前,调用此函数:

void OnTick()
{
    // ... (计算得到初步手数 preliminaryLotSize) ...
    double preliminaryLotSize = CalcLotSize(UseDynamicLotSize, EquityPercent, currentTradeStopLossPips, FixedLotSize);

    // 调用验证函数处理初步手数
    double verifiedLotSize = VerifyLotSize(preliminaryLotSize);
    Print("初步手数: ", preliminaryLotSize, ", 验证后手数: ", verifiedLotSize); // 调试信息

    // ... (使用 verifiedLotSize 进行后续操作,如 OrderSend) ...
    if (verifiedLotSize > 0) // 确保手数有效
    {
        // OrderSend(..., verifiedLotSize, ...);
    }

}

这样,通过将特定任务封装到函数中,代码变得更加模块化、清晰和易于管理。

赞(0)
未经允许不得转载:图道交易 » ​MQL4(23):封装手数计算与验证函数
分享到