在前面的章节中,我们逐步构建了包含下单、错误处理、价格验证、手数计算等功能的代码逻辑。现在,我们将学习如何将这些代码块封装成可重用的函数。这样做的好处是:
- 将复杂的细节(如手数计算、价格验证等)隐藏在函数内部,使得
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
传入。 - 函数体: 内部逻辑与之前相同,但使用的是传入的参数值。增加了对
argStopLossPips
和tickValuePerPip
的有效性检查,并在计算失败时提供了回退到固定手数的逻辑。 - 返回值: 通过
return(calculatedLotSize);
返回计算或指定的初步手数。
函数的位置: 这个函数定义可以放在 MQL4源文件的全局区域(即所有主函数之外),或者放在一个单独的 .mqh
包含文件中,然后在主 EA 文件顶部使用 #include "your_functions.mqh"
来引入。
如何调用 CalcLotSize
函数:
- 首先,在主 EA 文件中仍然需要定义相应的外部变量,以便用户可以设置参数:
extern bool UseDynamicLotSize = true; extern double EquityPercent = 2.0; extern double FixedLotSize = 0.1; // 注意:这里的 StopLoss 通常不应是固定输入,而是在OnTick() 中根据信号计算 // extern double StopLossPips = 50; // 仅为演示参数传递
- 在
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()
函数中的lotSize
和CalcLotSize()
函数内部用于计算的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, ...);
}
}
这样,通过将特定任务封装到函数中,代码变得更加模块化、清晰和易于管理。