在构建交易策略时,除了精心设计入场、出场、止损和止盈规则外,确定每次交易使用多大的手数是风险管理中至关重要的一环。最简单直接的方法是设置一个固定的手数(例如,通过 extern
变量让用户输入),并在所有交易中都使用这个固定值。然而,这种方法没有考虑到账户资金的变化和单笔交易的潜在风险大小。
接下来的内容中,我们将探讨一种更为先进和专业的风险管理方法:基于风险的动态手数计算。这种方法的核心思想是,根据您预先设定的单笔交易最大可承受风险(通常以账户净值 Equity 的一个百分比来表示,例如 1%、2% 或 3%),结合当前交易的止损距离,来动态计算出本次交易应该使用的合适手数。
过度杠杆是导致账户亏损甚至爆仓的罪魁祸首,采用基于风险的手数计算方法,可以有效地控制单笔亏损对账户整体的影响。普遍推荐的风险控制标准是,确保任何单笔交易在触及止损时的最大亏损额,不超过您当前账户净值的 2-3%。
资金管理与动态手数计算
基于风险动态计算手数其核心思想是:根据您预设的单笔交易愿意承担的最大风险(通常表示为账户净值的一个百分比)以及该笔交易的止损距离(以 pips 为单位)来决定本次下单的手数大小。
计算步骤:
-
确定风险参数:
- 风险百分比: 通过外部变量让用户设定单笔交易最大亏损占账户净值的百分比,例如 2%。
- 止损点数: 获取当前交易信号对应的实际止损距离(pips)。这通常由交易策略逻辑动态计算得出,或由用户输入。
// 输入参数示例 extern double EquityPercent = 2.0; // 例如,风险设置为 2% // 注意:StopLossPips 通常不应是固定输入,而应由策略逻辑根据信号确定 // extern double StopLossPips = 50; // 仅为计算演示,实际应是动态
-
计算最大可亏损金额:
- 使用内置函数
AccountEquity()
获取当前账户的实时净值。 - 计算出以账户货币为单位的最大允许亏损金额:
double riskAmount = AccountEquity() * (EquityPercent / 100.0); // 示例: $10,000 净值 * (2.0 / 100.0) = $200
- 使用内置函数
-
计算每标准手每 Pip 的价值:
- 理解
MODE_TICKVALUE
:MarketInfo(Symbol(), MODE_TICKVALUE)
函数返回交易1标准手时,价格每变动一个 Point (最小价格单位) 所对应的账户货币价值。 - 换算为每 Pip 价值: 由于我们需要基于 Pips 计算手数,并且不同报价精度下 Pip 和 Point 的关系不同 (4位: 1 Pip = 1 Point; 5位: 1 Pip = 10 Points),我们需要将
MODE_TICKVALUE
返回的值统一换算成 “每标准手每波动 1 Pip 的价值”。
double tickValueRaw = MarketInfo(Symbol(), MODE_TICKVALUE); double tickValuePerPip = tickValueRaw; // 默认值 int digits = (int)MarketInfo(Symbol(), MODE_DIGITS); // 如果是 3 位或 5 位小数报价 (即 1 Pip = 10 Points) if (digits == 3 || digits == 5) { tickValuePerPip = tickValueRaw * 10.0; // 将每 Point 价值乘以 10,得到每 Pip 价值 } // 现在 tickValuePerPip 代表交易 1 标准手时,价格每波动 1 Pip 的盈亏金额 (账户货币单位)
- 理解
-
计算动态手数 (
calcLots
):- 公式: (最大允许亏损金额 / 止损点数) / (每标准手每 Pip 的价值)
- 代码实现:
double calcLots = 0.0; double currentStopLossPips = ...; // 获取当前交易的实际止损点数 // 必须确保止损点数和每 Pip 价值都有效(大于0)才能计算 if (currentStopLossPips > 0 && tickValuePerPip > 0) { calcLots = (riskAmount / currentStopLossPips) / tickValuePerPip; } else { Print("无法计算动态手数:止损点数或TickValue无效!"); // 此处应有错误处理,例如使用备用手数或放弃交易 }
- 示例:
riskAmount
= $200,currentStopLossPips
= 50,tickValuePerPip
= $10。则calcLots
= ($200 / 50) / $10 = 0.4 标准手。
动态手数优点: 保持单笔风险占净值的比例恒定,有助于控制账户回撤。手数随止损距离反向变化:止损越小,手数越大;止损越大,手数越小。特别适合止损距离相对明确或有逻辑依据的策略。能从较小的止损和/或较大的盈亏比中获益。如果策略使用非常大的止损或不设止损,动态计算出的手数可能过小而失去意义,此时固定手数可能是更好的选择。
结合固定与动态手数模式:
可以在 EA 中设置一个开关,让用户灵活选择。
// --- 输入参数 ---
extern bool UseDynamicLotSize = true; // 开关: true=动态手数, false=固定手数
extern double EquityPercent = 2.0; // 动态手数参数:风险%
// extern double StopLossPips = 50; // 不应是固定输入, 应由策略决定
extern double FixedLotSize = 0.1; // 固定手数参数
// --- 在 OnTick() 函数中确定最终手数 ---
double lotSizeToUse = 0.0;
double currentStopLossPips = ...; // 假设已获取当前交易的止损点数
if (UseDynamicLotSize)
{
// ... 执行上述动态手数计算逻辑,结果存入 lotSizeToUse ...
// ... 注意包含对 StopLossPips 和 tickValuePerPip 的有效性检查 ...
if (currentStopLossPips > 0 && tickValuePerPip > 0) {
lotSizeToUse = (riskAmount / currentStopLossPips) / tickValuePerPip;
} else {
Print("动态手数计算失败,使用固定手数替代.");
lotSizeToUse = FixedLotSize;
}
}
else // 使用固定手数
{
lotSizeToUse = FixedLotSize;
}
// --- 对 lotSizeToUse 进行验证和规范化 ---
// ... (见下文) ...
// --- 使用最终验证后的 lotSizeToUse 下单 ---
// OrderSend(..., lotSizeToUse, ...);
验证手数的有效性
计算或指定手数后,必须在调用 OrderSend()
前对其进行严格验证,以确保其符合经纪商的交易限制,否则下单会失败。验证步骤如下:
-
对比最小手数: 检查手数是否小于经纪商规定的最小允许交易量。
- 使用
MarketInfo(Symbol(), MODE_MINLOT)
获取最小手数。
double minLot = MarketInfo(Symbol(), MODE_MINLOT); if (lotSizeToUse < minLot) { Print("手数 ", lotSizeToUse, " 过小 (最小: ", minLot, "), 调整为最小值."); lotSizeToUse = minLot; // 强制设为最小手数 }
- 使用
-
对比最大手数: 检查手数是否超过经纪商规定的最大单笔交易量。
- 使用
MarketInfo(Symbol(), MODE_MAXLOT)
获取最大手数。
double maxLot = MarketInfo(Symbol(), MODE_MAXLOT); if (lotSizeToUse > maxLot) { Print("手数 ", lotSizeToUse, " 过大 (最大: ", maxLot, "), 调整为最大值."); lotSizeToUse = maxLot; // 强制设为最大手数 }
- 使用
-
检查并规范化手数步长: 确保手数符合经纪商要求的最小增量单位(步长),并将其规范化(通常是向下取整到最接近的有效步长倍数,或按指定小数位四舍五入)。
- 使用
MarketInfo(Symbol(), MODE_LOTSTEP)
获取手数步长 (例如 0.1 或 0.01)。 - 使用
NormalizeDouble()
进行规范化(四舍五入到指定小数位)。
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP); int lotDigits = 0; // 用于 NormalizeDouble 的小数位数 if (lotStep == 0.1) lotDigits = 1; // 迷你手,规范到 1 位小数 else if (lotStep == 0.01) lotDigits = 2; // 微型手,规范到 2 位小数 else if (lotStep == 0.001) lotDigits = 3;// (如果未来支持) // 可以添加更多 else if 处理其他可能的步长,或基于 log10 计算位数 if (lotDigits > 0) { lotSizeToUse = NormalizeDouble(lotSizeToUse, lotDigits); } // MQL5 更推荐的精确方法: 对齐到步长 // lotSizeToUse = MathRound(lotSizeToUse / lotStep) * lotStep; // 还需要再次检查 lotSizeToUse 是否小于 minLot (因为向下取整可能导致) if (lotSizeToUse < minLot) lotSizeToUse = minLot;
注意:
NormalizeDouble
是四舍五入。对于手数,有时需要向下取整到符合步长的倍数。MQL5 的MathRound(value / step) * step
是一种更精确的处理方式,能确保结果是lotStep
的整数倍。使用NormalizeDouble
后,或者特别是使用向下取整的方式后,可能需要再次检查结果是否低于minLot
。 - 使用
经过这三步验证和规范化,得到的 lotSizeToUse
才是最终可以安全传递给 OrderSend()
的有效手数。