引言
“每笔风险 1%”是最常见的风控规则。规则本身很简单,难的是把它换算成手数:
- 每个品种、每家经纪商的点值都不一样。“1 个 pip = 10 美元”这种经验值,一离开主流外汇对就不灵了。
- 连经纪商自己给的 SYMBOL_TRADE_TICK_VALUE 都可能是错的。因为它,我的一个工具算出的黄金手数大了 10 倍。
- 手数要按交易量步长取整。取整方向错了,每一单都会比你设定的多担一点风险。
- 预算低于最小手数时,计算器照样显示“0.01”,却不告诉你这一单的风险已经超出预算。
- 还有,“1%”是谁的 1%?余额、净值、可用保证金,还是当天开始时的余额?做 prop 挑战时,这几个答案各不相同。
这篇文章一步步用 MQL5 写一个按风险反推手数的计算器:一个公式、一个靠得住的点值、九种风险基数、取整和兜底规则,再加上和终端结果的自检。文中有两个问题是我自己工具里真实出过的 bug,我会给出出错的代码、修法和测试结果。
全部代码都在脚本 RiskLotCalc.mq5 里,文末附完整源码。它只计算并打印到“专家”日志,从不开仓或改单。
1. 一个公式适用所有品种
仓位计算归根结底就一行:
手数 = 风险预算 / (止损点数 × 1 手每点价值)
难点在最后一项点值:1 手、价格每动 1 个点,按账户货币算值多少钱。别自己用合约大小和汇率去推,终端有两种方式直接给你。
1.1 常见做法:读品种规格
网上几乎所有示例,包括我自己以前的代码,都是这么写的:
double PointValueFromTickValue(const string sym)
{
const double tick_value = SymbolInfoDouble(sym,SYMBOL_TRADE_TICK_VALUE);
const double tick_size = SymbolInfoDouble(sym,SYMBOL_TRADE_TICK_SIZE);
const double point = SymbolInfoDouble(sym,SYMBOL_POINT);
if(tick_value <= 0.0 || tick_size <= 0.0 || point <= 0.0)
return 0.0;
return tick_value * point / tick_size;
} - SYMBOL_TRADE_TICK_VALUE :价格变动一个 tick 时 1 手的盈亏,以账户货币计。
- SYMBOL_TRADE_TICK_SIZE :最小价格变动。
- SYMBOL_POINT :报价最后一位的一个单位。
为什么要乘 point / tick_size ?大多数外汇对和黄金上 tick_size == point ,所以直接把 tick_value 当点值碰巧没问题。但在一些指数上不成立。下面是 2026 年 10 月 26 日在 MetaQuotes-Demo 上读到的三组真实规格:
| 品种 | Point | Tick size | Tick value | 合约大小 | 盈利货币 |
|---|---|---|---|---|---|
| EURUSD | 0.00001 | 0.00001 | 1.0 | 100000 | USD |
| XAUUSD | 0.01 | 0.01 | 0.1 | 100 | USD |
| HK50 | 0.1 | 0.5 | 0.5 | 1 | HKD |
HK50 报价精确到 0.1,但价格每次按 0.5 跳动。这里如果把 tick_value 直接当点值,会高估 5 倍。
另外两个加粗的数字,是下一节要讲的。
1.2 规格本身也会错
上面这个函数在我自己的两个工具(一个手数计算器、一个交易面板)里跑了好几个月。后来我在 MetaQuotes-Demo 上测 XAUUSD:合约大小 100 盎司,tick size 0.01,所以 1 手每个 tick 应该值 1.00 美元。服务器给的 tick value 却是 0.1。

图 1. MetaQuotes-Demo 上的 XAUUSD 规格:合约大小 100,Tick size 0.01,Tick value 0.1
后果很好算。风险 100 美元、止损 300 点:
用 tick_value 算的点值 = 0.1 → 手数 = 100 / (300 × 0.1) = 3.33 真实点值 = 1.0 → 手数 = 100 / (300 × 1.0) = 0.33
3.33 手打到止损,亏损约 1,000 美元。在 10,000 美元的账户上,你以为风险是 1%,实际是 10%。那个交易面板是会下单的,它真的会按这个手数把单子发出去。
这台服务器上的实际盈亏是按合约大小结算的。我之前在同一台服务器上按合约大小 100 回放过历史成交,每一笔都对得上。所以错的是 tick value,不是合约大小。
XAUUSD 不是个例。同一台服务器上,HK50 合约大小为 1,盈利货币是港币。用 tick value 算出的点值是 0.1,而终端的盈亏计算给出的是 0.01275 美元。相差约 7.8 倍,正好是港币兑美元的汇率。看起来 tick value 是按港币给的,没有换算成账户货币。这次算出的手数偏小,属于安全方向,但仍然是错的。
1.3 更好的做法:直接问终端怎么结算盈亏
OrderCalcProfit() 用的就是 MetaTrader 5 结算真实成交的那套计算,品种的计算模式和到账户货币的换算它都会处理。所以直接问它:1 手跌 1000 点亏多少,再除以 1000:
double PointValuePerLot(const string sym)
{
const double fallback = PointValueFromTickValue(sym);
const double point = SymbolInfoDouble(sym,SYMBOL_POINT);
const double price = SymbolInfoDouble(sym,SYMBOL_BID); // last known quote when the market is closed
const double probe = 1000.0;
double loss = 0.0;
if(point <= 0.0 || price <= probe * point ||
!OrderCalcProfit(ORDER_TYPE_BUY,sym,1.0,price,price - probe * point,loss))
return fallback;
if(loss >= 0.0)
{
PrintFormat("%s: OrderCalcProfit returned %.2f (conversion rate not ready?) - falling back to tick_value, run again shortly",
sym,loss);
return fallback;
}
const double pv = -loss / probe;
if(fallback > 0.0 && MathAbs(pv - fallback) / pv > 0.05)
PrintFormat("%s: point value from OrderCalcProfit %.5f differs from broker tick_value %.5f - using %.5f",
sym,pv,fallback,pv);
return pv;
} 几个设计取舍:
- OrderCalcProfit 是主数据源,tick value 只做兜底。 只有在没有报价或调用失败时才用兜底值。
- 不一致就报出来。 两个值相差超过 5%,往日志打一行,用户就能看出经纪商的规格有问题。
- 用 1000 点去试,不用 1 点。 1 个点的亏损很小,再除回去会放大舍入误差。
- 算一次缓存起来,但要刷新。 别每个 tick 都调用。切换品种时重算,并用定时器定期刷新(第 6 节会讲为什么只算一次不够)。
例子:余额 10,000 美元,风险 1% = 100 美元,EURUSD 止损 300 点(30 pips),点值 1.00:
手数 = 100 / (300 × 1.00) = 0.333… → 0.33 手 → 实际风险 = 0.33 × 300 × 1.00 = 99 美元
XAUUSD 止损 300 点(价格 3.00 美元)时,真实点值同样是 1.00,所以也是 0.33 手。
2. 1% 是谁的 1%?九种风险基数
公式里的风险预算,决定了你的风控口径。我的工具支持九种定义方式:
| 模式 | 风险预算 | 适用场景 |
|---|---|---|
| 固定金额 | 你输入的金额 | 每笔固定最大亏损 |
| 余额 % | 余额 × % | 经典的固定比例法 |
| 净值 % | 净值 × % | 有持仓时更保守:浮亏会缩小基数 |
| 可用保证金 % | 可用保证金 × % | 持仓多时保护保证金 |
| 自定义基数 % | 你的基数 × % | 出金之后,或按 prop 账户初始规模算 |
| 当日开始余额 % | 今天 00:00 的余额 × % | 按当日起始余额计算的 prop 规则 |
| 本周开始余额 % | 周一 00:00 的余额 × % | 周风险上限 |
| 本月开始余额 % | 1 号 00:00 的余额 × % | 月风险上限 |
| 手动手数 | 反算:输入手数,输出风险 | 检查某个手数实际承担多少风险 |
余额和净值差多少?余额 10,000 美元,有一笔持仓浮亏 1,500 美元:
- 余额的 1% → 100 美元
- 净值的 1% → 85 美元
浮亏越深,按净值算的规则就会自动把新仓位压得越小。
代码就是一个 switch :
double RiskBudget(const ENUM_RISK_BASE mode)
{
switch(mode)
{
case RISK_FIXED_MONEY: return InpRiskMoney;
case RISK_PCT_BALANCE: return AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPct / 100.0;
case RISK_PCT_EQUITY: return AccountInfoDouble(ACCOUNT_EQUITY) * InpRiskPct / 100.0;
case RISK_PCT_FREE_MARGIN: return AccountInfoDouble(ACCOUNT_MARGIN_FREE) * InpRiskPct / 100.0;
case RISK_PCT_CUSTOM: return InpCustomBase * InpRiskPct / 100.0;
case RISK_PCT_PREV_DAY: return BalanceAsOf(DayStart()) * InpRiskPct / 100.0;
case RISK_PCT_PREV_WEEK: return BalanceAsOf(WeekStart()) * InpRiskPct / 100.0;
case RISK_PCT_PREV_MONTH: return BalanceAsOf(MonthStart()) * InpRiskPct / 100.0;
default: return 0.0;
}
} 前五种直接读账户属性。后三种需要过去某个时刻的余额,而 MQL5 没有现成的函数。
3. 过去某个时刻的余额:倒推
思路:T 时刻的余额 = 当前余额 − T 之后所有的余额变动。
double BalanceAsOf(const datetime since)
{
const double bal = AccountInfoDouble(ACCOUNT_BALANCE);
if(since <= 0 || !HistorySelect(since,TimeCurrent()))
return bal;
double delta = 0.0;
const int n = HistoryDealsTotal();
for(int i = 0; i < n; i++)
{
const ulong ticket = HistoryDealGetTicket(i);
if(ticket == 0)
continue;
delta += HistoryDealGetDouble(ticket,DEAL_PROFIT)
+ HistoryDealGetDouble(ticket,DEAL_SWAP)
+ HistoryDealGetDouble(ticket,DEAL_COMMISSION);
}
return bal - delta;
} 有三个细节:
- 盈利、隔夜利息、手续费都要减。 只减盈利,在高手续费账户上会越算越偏。
- 入金和出金也会被倒推回去。 它们在历史里是 DEAL_TYPE_BALANCE 类型的成交,金额在 DEAL_PROFIT 里。中午入金 5,000 美元,“当日开始余额”仍是入金前的数,这通常正是 prop 规则的意思。如果只想要交易结果,按 DEAL_TYPE 过滤。
- 别每个 tick 都跑。 HistorySelect() 不是免费的。启动时和换日时算一次,缓存起来。
周期起点用的是服务器时间。 TimeCurrent() 返回服务器时间,所以 DayStart() 是服务器的午夜。很多 prop 公司按自己的时区换日,不一定是经纪商的午夜。用“当日开始余额”去对照 prop 规则前,先确认那家公司的一天从几点开始。各家的规则也不一样:有的看余额,有的看余额和净值中较高的那个。
第 7 节测试时账户余额是 99,822.23 美元,三个历史基数都算出 100,000 美元:差额正好是当天早些时候已平仓交易的结果。
4. 取整和兜底:多出来的风险从这里溜进来
公式算出的是原始手数,比如 0.3333。经纪商只接受 SYMBOL_VOLUME_STEP 的整数倍,并且在 SYMBOL_VOLUME_MIN 和 SYMBOL_VOLUME_MAX 之间。有三条规则。
规则 1:永远向下取整。 四舍五入会把 0.335 变成 0.34,每一单都多担风险。风控工具永远不该超预算:
double res = MathFloor(lot / step + 1e-8) * step;
( + 1e-8 的来由见第 5 节。)
规则 2:最小手数压过预算时要明说。 小账户经常碰到:
余额 1,000 美元,风险 0.5% = 5 美元,XAUUSD 止损 800 点,点值 1.00 原始手数 = 5 / (800 × 1.00) = 0.00625 → 低于最小手数 0.01
两种选择:拒绝,或者抬到最小手数。我的工具选择抬上去,因为用户通常无论如何都会下单,同时给出警告:这一单实际风险是 0.01 × 800 × 1.00 = 8 美元,占余额 0.8%,是预算的 1.6 倍。
规则 3:显示实际风险,而不是预算。 取整和兜底之后,用最终手数重新算一遍风险:
const double risk = lot * sl_points * point_value; // not the budget
很多计算器在风险栏里显示的是你输入的数(5 美元),而不是这一单实际会亏的钱(8 美元)。差别只有一行代码。
5. 第二个真实 bug:少一档的浮点陷阱
规则 1 里的 + 1e-8 来自我修过的另一个 bug。原来那行是:
double res = MathFloor(lot / step) * step; // buggy
看着没错,但 0.29 在二进制浮点里没法精确表示:
0.29 / 0.01 = 28.999999999999996 → MathFloor → 28 → 0.28 0.57 / 0.01 = 56.99999999999999 → 0.56 1.13 / 0.01 = 112.99999999999999 → 1.12 2.30 / 0.01 = 229.99999999999997 → 2.29
步长 0.01 时,0.01 到 10.00 的 1000 个手数里有 125 个会少一档。按风险算手数时只少 0.01 手,而且是安全方向,所以很容易漏掉。手动模式下就很明显了:用户输入 0.29,工具显示 0.28,还提示手数已按步长调整。在会下单的面板里,单子就按 0.28 手发出去;对 0.58 手做 50% 部分平仓,会比一半少平一档。
修法:向下取整前加一个很小的 epsilon。 1e-8 远大于 double 除法的误差(约 1e-14),又远小于一个步长。0.29 保持 0.29,而真正的小数如 0.289 仍然取整到 0.28,永远不会超预算。
6. 公式还没算进去的东西
上面的“实际风险”是按止损价平仓的结果,有两项成本没算:
- 点差。 买单按 Ask 开、按 Bid 平,一开仓就先亏一个点差。
- 手续费。 按手收,通常开平两边都收。
真实亏损约等于 公式风险 + 点差 + 手续费。手续费高或止损很近时,这部分不能忽略。简单的修正是把当前点差加到止损距离上,或者从预算里扣掉预计的手续费。
冷门品种在开盘前还可能报出为零的 tick value 和报价。这时计算器应该显示“暂不可用”并稍后重试,而不是除以零算出一个巨大的数。
OrderCalcProfit 也有它的薄弱时刻。 我切换终端语言后重启了 MetaTrader 5,马上在 HK50 上跑脚本。 OrderCalcProfit() 返回成功,结果却是 0.00。脚本退回用 tick value,手数从 260 变成了 33。30 秒后同一个脚本给出了正确结果。HK50 以港币计价,我猜是港币兑美元的换算报价还没加载好。教训有两条:把结果为零当成“还没准备好”,报出来并稍后重算;也别在 OnInit() 里算一次点值就永久缓存,要用定时器刷新。
下面是“专家”日志,第一次是重启后马上跑的(那时的版本还不会打印“returned 0”警告):
15:42:25 HK50 point value per lot=0.10000 USD min lot=1.00 step=1.00 SL=300 points 15:42:25 RISK_PCT_BALANCE budget= 998.22 raw lot=33.2741 lot=33.00 actual risk= 990.00 (0.99%) OrderCalcProfit=-0.00 (floored to step)
30 秒后同一个脚本:
15:42:54 HK50: point value from OrderCalcProfit 0.01275 differs from broker tick_value 0.10000 - using 0.01275 15:42:54 HK50 point value per lot=0.01275 USD min lot=1.00 step=1.00 SL=300 points 15:42:54 RISK_PCT_BALANCE budget= 998.22 raw lot=260.9732 lot=260.00 actual risk= 994.50 (1.00%) OrderCalcProfit=-994.32 (floored to step)
7. 完整脚本与测试结果
RiskLotCalc.mq5 (完整源码见文末)可以挂在任何图表上。它对当前品种打印九种模式各自的预算、原始手数、最终手数和实际风险,并用 OrderCalcProfit() 按止损价逐一核对最终手数。经纪商的 tick value 对不上时,会多打一行提示。它从不下单。
输入参数:
| 参数 | 默认值 | 含义 |
|---|---|---|
| InpSlPoints | 300 | 止损距离(点) |
| InpRiskPct | 1.0 | % 类模式的风险百分比 |
| InpRiskMoney | 100 | 固定金额模式的预算 |
| InpCustomBase | 10000 | 自定义 % 模式的基数 |
| InpManualLot | 0.29 | 手动模式的手数(0.29 用来演示取整陷阱) |
以下结果来自 2026 年 10 月 26 日 MetaQuotes-Demo 模拟账户,余额 99,822.23 美元,默认参数。
XAUUSD。 第一行报告了点值不一致。三个历史基数都是 100,000 美元。手动手数 0.29 保持 0.29。每一行都和 OrderCalcProfit() 精确到分。
XAUUSD: point value from OrderCalcProfit 1.00000 differs from broker tick_value 0.10000 - using 1.00000 XAUUSD point value per lot=1.00000 USD min lot=0.01 step=0.01 SL=300 points RISK_FIXED_MONEY budget= 100.00 raw lot=0.3333 lot=0.33 actual risk= 99.00 (0.10%) OrderCalcProfit=-99.00 (floored to step) RISK_PCT_BALANCE budget= 998.22 raw lot=3.3274 lot=3.32 actual risk= 996.00 (1.00%) OrderCalcProfit=-996.00 (floored to step) RISK_PCT_EQUITY budget= 998.22 raw lot=3.3274 lot=3.32 actual risk= 996.00 (1.00%) OrderCalcProfit=-996.00 (floored to step) RISK_PCT_FREE_MARGIN budget= 998.22 raw lot=3.3274 lot=3.32 actual risk= 996.00 (1.00%) OrderCalcProfit=-996.00 (floored to step) RISK_PCT_CUSTOM budget= 100.00 raw lot=0.3333 lot=0.33 actual risk= 99.00 (0.10%) OrderCalcProfit=-99.00 (floored to step) RISK_PCT_PREV_DAY budget= 1000.00 raw lot=3.3333 lot=3.33 actual risk= 999.00 (1.00%) OrderCalcProfit=-999.00 (floored to step) RISK_PCT_PREV_WEEK budget= 1000.00 raw lot=3.3333 lot=3.33 actual risk= 999.00 (1.00%) OrderCalcProfit=-999.00 (floored to step) RISK_PCT_PREV_MONTH budget= 1000.00 raw lot=3.3333 lot=3.33 actual risk= 999.00 (1.00%) OrderCalcProfit=-999.00 (floored to step) RISK_MANUAL_LOT budget= 0.00 raw lot=0.2900 lot=0.29 actual risk= 87.00 (0.09%) OrderCalcProfit=-87.00
HK50。 点值 0.01275 美元,带不一致警告。最小手数是 1.00,所以手动的 0.29 被抬到 1.00,并提示 WARNING: raised to min lot, actual risk is above budget ,这就是第 4 节的规则 2。公式风险 99.45 对比 OrderCalcProfit() 的 99.43:两分钱的差来自港币按两个不同价格换算。
HK50: point value from OrderCalcProfit 0.01275 differs from broker tick_value 0.10000 - using 0.01275 HK50 point value per lot=0.01275 USD min lot=1.00 step=1.00 SL=300 points RISK_FIXED_MONEY budget= 100.00 raw lot=26.1438 lot=26.00 actual risk= 99.45 (0.10%) OrderCalcProfit=-99.43 (floored to step) RISK_PCT_BALANCE budget= 998.22 raw lot=260.9732 lot=260.00 actual risk= 994.50 (1.00%) OrderCalcProfit=-994.32 (floored to step) RISK_PCT_EQUITY budget= 998.22 raw lot=260.9732 lot=260.00 actual risk= 994.50 (1.00%) OrderCalcProfit=-994.32 (floored to step) RISK_PCT_FREE_MARGIN budget= 998.22 raw lot=260.9732 lot=260.00 actual risk= 994.50 (1.00%) OrderCalcProfit=-994.32 (floored to step) RISK_PCT_CUSTOM budget= 100.00 raw lot=26.1438 lot=26.00 actual risk= 99.45 (0.10%) OrderCalcProfit=-99.43 (floored to step) RISK_PCT_PREV_DAY budget= 1000.00 raw lot=261.4379 lot=261.00 actual risk= 998.32 (1.00%) OrderCalcProfit=-998.14 (floored to step) RISK_PCT_PREV_WEEK budget= 1000.00 raw lot=261.4379 lot=261.00 actual risk= 998.32 (1.00%) OrderCalcProfit=-998.14 (floored to step) RISK_PCT_PREV_MONTH budget= 1000.00 raw lot=261.4379 lot=261.00 actual risk= 998.32 (1.00%) OrderCalcProfit=-998.14 (floored to step) RISK_MANUAL_LOT budget= 0.00 raw lot=0.2900 lot=1.00 actual risk= 3.82 (0.00%) OrderCalcProfit=-3.82 WARNING: raised to min lot, actual risk is above budget
EURUSD。 规格正常:没有警告,每一行都和 OrderCalcProfit() 完全一致。
EURUSD point value per lot=1.00000 USD min lot=0.01 step=0.01 SL=300 points RISK_FIXED_MONEY budget= 100.00 raw lot=0.3333 lot=0.33 actual risk= 99.00 (0.10%) OrderCalcProfit=-99.00 (floored to step) RISK_PCT_BALANCE budget= 998.22 raw lot=3.3274 lot=3.32 actual risk= 996.00 (1.00%) OrderCalcProfit=-996.00 (floored to step) RISK_PCT_EQUITY budget= 998.22 raw lot=3.3274 lot=3.32 actual risk= 996.00 (1.00%) OrderCalcProfit=-996.00 (floored to step) RISK_PCT_FREE_MARGIN budget= 998.22 raw lot=3.3274 lot=3.32 actual risk= 996.00 (1.00%) OrderCalcProfit=-996.00 (floored to step) RISK_PCT_CUSTOM budget= 100.00 raw lot=0.3333 lot=0.33 actual risk= 99.00 (0.10%) OrderCalcProfit=-99.00 (floored to step) RISK_PCT_PREV_DAY budget= 1000.00 raw lot=3.3333 lot=3.33 actual risk= 999.00 (1.00%) OrderCalcProfit=-999.00 (floored to step) RISK_PCT_PREV_WEEK budget= 1000.00 raw lot=3.3333 lot=3.33 actual risk= 999.00 (1.00%) OrderCalcProfit=-999.00 (floored to step) RISK_PCT_PREV_MONTH budget= 1000.00 raw lot=3.3333 lot=3.33 actual risk= 999.00 (1.00%) OrderCalcProfit=-999.00 (floored to step) RISK_MANUAL_LOT budget= 0.00 raw lot=0.2900 lot=0.29 actual risk= 87.00 (0.09%) OrderCalcProfit=-87.00
结论
仓位计算就是一行算术,靠不靠得住全在细节:
- 点值从 OrderCalcProfit() 取,别只靠 SYMBOL_TRADE_TICK_VALUE ,不一致就记日志。
- 结果为零当成“还没准备好”,缓存的值要定期刷新。
- 想清楚你说的“1%”是哪个,并记住历史基数用的是服务器时间。
- 永远向下取整,带上 epsilon。
- 最小手数压过预算时要明说,并始终显示最终手数的实际风险。
手数决定单笔交易最多亏多少。下次我会讲风控的另一半:达到当日亏损上限就停止交易。
如果脚本在你的经纪商某个品种上报出了不一致,欢迎在评论里贴出品种名和那两个数值。
完整源码:RiskLotCalc.mq5
保存为 MQL5/Scripts/RiskLotCalc.mq5 ,在 MetaEditor 里编译,然后拖到任意图表上。它只打印,从不下单。
//+------------------------------------------------------------------+ //| RiskLotCalc.mq5 //| https://edgehone.com/ | //| Lot size from risk: 9 risk bases, step rounding / min-lot clamp, | //| cross-check with OrderCalcProfit. Prints only, never trades. | //+------------------------------------------------------------------+ #property script_show_inputs #property version "1.0" enum ENUM_RISK_BASE { RISK_FIXED_MONEY, // Fixed money RISK_PCT_BALANCE, // % of balance RISK_PCT_EQUITY, // % of equity RISK_PCT_FREE_MARGIN, // % of free margin RISK_PCT_CUSTOM, // % of custom base RISK_PCT_PREV_DAY, // % of balance at day start RISK_PCT_PREV_WEEK, // % of balance at week start RISK_PCT_PREV_MONTH, // % of balance at month start RISK_MANUAL_LOT // Manual lot (risk in reverse) }; input double InpSlPoints = 300; // Stop loss distance (points) input double InpRiskPct = 1.0; // Risk percent input double InpRiskMoney = 100; // Risk money (fixed money mode) input double InpCustomBase = 10000; // Custom base input double InpManualLot = 0.29; // Manual lot (0.29 shows the floating-point trap) //--- Point value from the broker's symbol spec: tick_value * point / tick_size (fallback only) double PointValueFromTickValue(const string sym) { const double tick_value = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE); const double tick_size = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE); const double point = SymbolInfoDouble(sym, SYMBOL_POINT); if (tick_value <= 0.0 || tick_size <= 0.0 || point <= 0.0) return 0.0; // some exotic symbols report tick_value=0 before the market opens return tick_value * point / tick_size; } //--- Account-currency value of 1 point for 1.0 lot. // Primary: let the terminal compute the loss of 1 lot over 1000 points, then divide by 1000. // On MetaQuotes-Demo XAUUSD reports tick_value=0.1 (contract 100 implies 1.0), // so trusting tick_value alone gives a lot size 10x too large. double PointValuePerLot(const string sym) { const double fallback = PointValueFromTickValue(sym); const double point = SymbolInfoDouble(sym, SYMBOL_POINT); const double price = SymbolInfoDouble(sym, SYMBOL_BID); // last known quote when the market is closed const double probe = 1000.0; double loss = 0.0; if (point <= 0.0 || price <= probe * point || !OrderCalcProfit(ORDER_TYPE_BUY, sym, 1.0, price, price - probe * point, loss)) return fallback; // Right after a terminal restart OrderCalcProfit can succeed but return 0 for // cross-currency symbols (conversion rate not loaded yet). Say so instead of // silently falling back to a tick_value that may be wrong. if (loss >= 0.0) { PrintFormat("%s: OrderCalcProfit returned %.2f (conversion rate not ready?) - falling back to tick_value, run again shortly", sym, loss); return fallback; } const double pv = -loss / probe; if (fallback > 0.0 && MathAbs(pv - fallback) / pv > 0.05) PrintFormat("%s: point value from OrderCalcProfit %.5f differs from broker tick_value %.5f - using %.5f", sym, pv, fallback, pv); return pv; } //--- Decimal places of the volume step (0.01 -> 2, 0.1 -> 1, 1 -> 0) int VolumeDigits(const double step) { int digits = 0; double s = step; while (digits < 8 && MathAbs(s - MathRound(s)) > 1e-8) { s *= 10.0; digits++; } return digits; } //--- Floor to the volume step (never exceed the budget). May return less than // the broker minimum; the caller decides what to do with that. double FloorToStep(const string sym, const double lot) { const double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP); const double vmax = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX); if (step <= 0.0 || lot <= 0.0) return 0.0; // 0.29 / 0.01 is 28.999999999999996 in double; without the epsilon it floors to 0.28 double res = MathFloor(lot / step + 1e-8) * step; if (res > vmax) res = MathFloor(vmax / step + 1e-8) * step; return NormalizeDouble(res, VolumeDigits(step)); } //--- Balance at a past moment = current balance - every deal after that moment // (deposits/withdrawals are DEAL_TYPE_BALANCE deals and are rolled back too) double BalanceAsOf(const datetime since) { const double bal = AccountInfoDouble(ACCOUNT_BALANCE); if (since <= 0 || !HistorySelect(since, TimeCurrent())) return bal; double delta = 0.0; const int n = HistoryDealsTotal(); for (int i = 0; i < n; i++) { const ulong ticket = HistoryDealGetTicket(i); if (ticket == 0) continue; delta += HistoryDealGetDouble(ticket, DEAL_PROFIT) + HistoryDealGetDouble(ticket, DEAL_SWAP) + HistoryDealGetDouble(ticket, DEAL_COMMISSION); } return bal - delta; } //--- Period starts: note these are 00:00 in SERVER time datetime DayStart() { MqlDateTime t; TimeToStruct(TimeCurrent(), t); t.hour = 0; t.min = 0; t.sec = 0; return StructToTime(t); } datetime WeekStart() { const datetime day = DayStart(); MqlDateTime t; TimeToStruct(day, t); int dow = t.day_of_week; // 0 = Sunday if (dow == 0) dow = 7; // week starts on Monday return day - (datetime)((dow - 1) * 86400); } datetime MonthStart() { MqlDateTime t; TimeToStruct(TimeCurrent(), t); t.day = 1; t.hour = 0; t.min = 0; t.sec = 0; return StructToTime(t); } //--- Risk budget in account currency double RiskBudget(const ENUM_RISK_BASE mode) { switch (mode) { case RISK_FIXED_MONEY: return InpRiskMoney; case RISK_PCT_BALANCE: return AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPct / 100.0; case RISK_PCT_EQUITY: return AccountInfoDouble(ACCOUNT_EQUITY) * InpRiskPct / 100.0; case RISK_PCT_FREE_MARGIN: return AccountInfoDouble(ACCOUNT_MARGIN_FREE) * InpRiskPct / 100.0; case RISK_PCT_CUSTOM: return InpCustomBase * InpRiskPct / 100.0; case RISK_PCT_PREV_DAY: return BalanceAsOf(DayStart()) * InpRiskPct / 100.0; case RISK_PCT_PREV_WEEK: return BalanceAsOf(WeekStart()) * InpRiskPct / 100.0; case RISK_PCT_PREV_MONTH: return BalanceAsOf(MonthStart()) * InpRiskPct / 100.0; default: return 0.0; } } void OnStart() { const string sym = _Symbol; const double pv = PointValuePerLot(sym); if (pv <= 0.0) { Print("Symbol spec not available (tick_value/tick_size is 0), try again after the market opens: ", sym); return; } if (InpSlPoints <= 0.0) { Print("Stop loss distance must be > 0"); return; } const double vmin = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN); const double point = SymbolInfoDouble(sym, SYMBOL_POINT); const double ask = SymbolInfoDouble(sym, SYMBOL_ASK); const double bal = AccountInfoDouble(ACCOUNT_BALANCE); PrintFormat("%s point value per lot=%.5f %s min lot=%.2f step=%.2f SL=%.0f points", sym, pv, AccountInfoString(ACCOUNT_CURRENCY), vmin, SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP), InpSlPoints); for (int m = RISK_FIXED_MONEY; m <= RISK_MANUAL_LOT; m++) { const ENUM_RISK_BASE mode = (ENUM_RISK_BASE)m; double budget = 0.0, raw = 0.0, lot = 0.0; string note = ""; if (mode == RISK_MANUAL_LOT) { raw = InpManualLot; lot = FloorToStep(sym, raw); } else { budget = RiskBudget(mode); if (budget <= 0.0) { PrintFormat("%-22s invalid risk budget", EnumToString(mode)); continue; } raw = budget / (InpSlPoints * pv); lot = FloorToStep(sym, raw); } if (lot < vmin) { lot = vmin; note = " WARNING: raised to min lot, actual risk is above budget"; } else if (lot < raw - 1e-9) note = " (floored to step)"; // Show the ACTUAL risk of the final lot, not the budget const double risk = lot * InpSlPoints * pv; // Cross-check with the terminal's own OrderCalcProfit: loss if closed at the SL price double check = 0.0; const bool ok = (ask > 0.0 && OrderCalcProfit(ORDER_TYPE_BUY, sym, lot, ask, ask - InpSlPoints * point, check)); PrintFormat("%-22s budget=%9.2f raw lot=%.4f lot=%.2f actual risk=%8.2f (%.2f%%) OrderCalcProfit=%s%s", EnumToString(mode), budget, raw, lot, risk, (bal > 0.0 ? risk / bal * 100.0 : 0.0), (ok ? DoubleToString(check, 2) : "n/a"), note); } } //+------------------------------------------------------------------+


