Position Sizing in MQL5: Nine Risk Bases and Why You Should Not Trust SYMBOL_TRADE_TICK_VALUE
Introduction
"Risk 1% per trade" is the most common risk rule there is. The rule itself is simple. Turning it into a lot size is where things go wrong:
- The value of one point is different for every symbol and every broker. Rules of thumb like "1 pip = $10" stop working the moment you leave the major forex pairs.
- Even the broker's own SYMBOL_TRADE_TICK_VALUE can be wrong. Because of it, one of my tools calculated a gold position that was 10 times too large.
- The lot has to be rounded to the volume step. Round in the wrong direction and every trade carries a little more risk than you asked for.
- When the budget is below the minimum lot, a calculator happily shows "0.01" without telling you that the trade now risks more than your budget.
- And "1%" of what? Balance, equity, free margin, or the balance at the start of the day? For prop firm challenges these give different answers.
This post builds a risk-based lot calculator in MQL5 step by step: one formula, a point value you can trust, nine risk bases, rounding and clamping rules, and a self-check against the terminal. Two of the problems described here were real bugs in my own tools. I show the broken code, the fix, and the test results.
All code is collected in the script RiskLotCalc.mq5 , listed in full at the end of the post. It only calculates and prints to the Experts log. It never opens or modifies trades.
1. One formula for every symbol
Position sizing comes down to one line:
lot = risk budget / (stop loss in points × value of 1 point for 1 lot)
The hard part is the last term, the point value: how much money one point of price movement is worth for one lot, in account currency. Do not derive it yourself from contract size and exchange rates. The terminal can give it to you in two ways.
1.1 The usual way: the symbol specification
Almost every example on the web, including my own earlier code, does this:
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 : profit or loss of 1 lot when price moves by one tick, in account currency.
- SYMBOL_TRADE_TICK_SIZE : the minimal price change.
- SYMBOL_POINT : one unit of the last quoted digit.
Why multiply by point / tick_size ? On most forex pairs and on gold, tick_size == point , so using tick_value directly as the point value happens to work. On some indices it does not. Here are three real specifications from MetaQuotes-Demo, read on 26 October 2026:
| Symbol | Point | Tick size | Tick value | Contract size | Profit currency |
|---|---|---|---|---|---|
| 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 is quoted to 0.1, but its price moves in steps of 0.5. Using tick_value as the point value here would overstate it 5 times.
The other two bold numbers are the subject of the next section.
1.2 The specification can be wrong
The function above ran for months in two of my own tools, a lot calculator and a trading panel. Then I tested XAUUSD on MetaQuotes-Demo. The contract size is 100 ounces and the tick size is 0.01, so one tick on one lot should be worth $1.00. The server reports a tick value of 0.1.

Image 1. XAUUSD specification on MetaQuotes-Demo: Contract size 100, Tick size 0.01, Tick value 0.1
The consequence is easy to calculate. With a $100 risk and a 300-point stop:
point value from tick_value = 0.1 → lot = 100 / (300 × 0.1) = 3.33 real point value = 1.0 → lot = 100 / (300 × 1.0) = 0.33
If 3.33 lots hit the stop, the loss is about $1,000. On a $10,000 account you think you are risking 1%, and you are actually risking 10%. The trading panel opens orders, so it would have sent exactly that order.
Actual profit and loss on this server is settled by contract size. I had already replayed historical trades on the same server with a contract size of 100 and every trade matched. So the wrong number is the tick value, not the contract.
XAUUSD is not the only case. On the same server HK50 has a contract size of 1 and its profit currency is the Hong Kong dollar. The tick value gives a point value of 0.1, while the terminal's profit calculation gives 0.01275 USD. The difference is about 7.8 times, which is the HKD/USD exchange rate. It looks as if the tick value is in Hong Kong dollars and was never converted to the account currency. This time the lot comes out smaller, which is the safe direction, but it is still wrong.
1.3 A better way: ask the terminal how it settles profit
OrderCalcProfit() uses the same calculation that MetaTrader 5 applies to real deals. It handles the symbol's calculation mode and the conversion to account currency. So ask it how much 1 lot loses over 1000 points, and divide by 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;
} The design decisions:
- OrderCalcProfit is the primary source, tick value is only a fallback. The fallback is used only when there is no quote or the call fails.
- Report a disagreement. If the two values differ by more than 5%, print one line to the log. Then the user can see that the broker's specification has a problem.
- Probe with 1000 points, not 1. The loss over a single point is tiny, and dividing it back amplifies rounding errors.
- Calculate once and cache, but refresh. Do not call it on every tick. Recalculate when the symbol changes and on a timer (section 6 explains why a one-time calculation is not enough).
Example: balance $10,000, risk 1% = $100, EURUSD stop 300 points (30 pips), point value 1.00:
lot = 100 / (300 × 1.00) = 0.333… → 0.33 lots → actual risk = 0.33 × 300 × 1.00 = $99
XAUUSD with a 300-point stop ($3.00 in price) has the same real point value of 1.00, so it also gives 0.33 lots.
2. One percent of what? Nine risk bases
The risk budget in the formula defines your risk policy. My tools support nine ways to define it:
| Mode | Risk budget | When to use it |
|---|---|---|
| Fixed money | the amount you enter | a fixed maximum loss per trade |
| % of balance | Balance × % | the classic fixed-fractional rule |
| % of equity | Equity × % | more conservative when positions are open: floating losses shrink the base |
| % of free margin | Free margin × % | many open positions, protecting margin |
| % of custom base | your base × % | after withdrawals, or sizing by a prop account's initial size |
| % of balance at day start | balance at 00:00 today × % | prop firm rules based on the day's starting balance |
| % of balance at week start | balance at Monday 00:00 × % | weekly risk limits |
| % of balance at month start | balance on the 1st at 00:00 × % | monthly risk limits |
| Manual lot | reverse: lot in, risk out | checking how much a given lot really risks |
How different are balance and equity? With a balance of $10,000 and an open position at −$1,500:
- 1% of balance → $100
- 1% of equity → $85
The deeper the floating loss, the more an equity-based rule automatically reduces new positions.
The code is a single 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;
}
} The first five read account properties directly. The last three need the balance at a moment in the past, and MQL5 has no function for that.
3. Balance at a past moment: work backwards
The idea: balance at time T = current balance − every balance change after 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;
} Three details matter:
- Subtract profit, swap and commission. Using profit alone drifts on accounts with high commissions.
- Deposits and withdrawals are rolled back too. They appear in the history as DEAL_TYPE_BALANCE deals, with the amount in DEAL_PROFIT . If you deposit $5,000 at noon, "balance at day start" is still the figure before the deposit, which is what prop firm rules usually mean. If you want trading results only, filter by DEAL_TYPE .
- Do not run it on every tick. HistorySelect() is not free. Calculate it at start-up and when the day changes, then cache it.
The period start is in server time. TimeCurrent() returns server time, so DayStart() is midnight on the server. Many prop firms reset their day in their own time zone, which is not necessarily the broker's midnight. Before using the day-start base to follow a prop firm rule, check when that firm's day actually starts. The rules also differ between firms: some use balance, some use the higher of balance and equity.
In the test in section 7 the account balance is $99,822.23. All three historical bases came out as $100,000: the difference is exactly the result of the trades closed earlier that day.
4. Rounding and clamping: where extra risk creeps in
The formula produces a raw lot such as 0.3333. The broker accepts only multiples of SYMBOL_VOLUME_STEP , between SYMBOL_VOLUME_MIN and SYMBOL_VOLUME_MAX . Three rules apply.
Rule 1: always round down. Rounding to nearest turns 0.335 into 0.34 and adds risk to every trade. A risk tool should never go over budget:
double res = MathFloor(lot / step + 1e-8) * step;
(The + 1e-8 is explained in section 5.)
Rule 2: say so when the minimum lot overrides the budget. Small accounts hit this constantly:
balance $1,000, risk 0.5% = $5, XAUUSD stop 800 points, point value 1.00 raw lot = 5 / (800 × 1.00) = 0.00625 → below the 0.01 minimum
There are two options: refuse, or raise the lot to the minimum. My tools raise it, because the user will usually trade anyway, and show a warning at the same time: this trade really risks 0.01 × 800 × 1.00 = $8, which is 0.8% of the balance and 1.6 times the budget.
Rule 3: show the actual risk, not the budget. After rounding and clamping, calculate the risk again from the final lot:
const double risk = lot * sl_points * point_value; // not the budget
Many calculators show the number you typed ($5) in the risk field rather than what the trade will actually lose ($8). The difference is one line of code.
5. The second real bug: a floating-point trap that loses one step
The + 1e-8 in rule 1 comes from another bug I fixed. The original line was:
double res = MathFloor(lot / step) * step; // buggy
It looks correct, but 0.29 cannot be represented exactly in binary floating point:
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
With a step of 0.01, 125 of the 1000 lot sizes from 0.01 to 10.00 lose one step. In risk-based sizing this only costs 0.01 lot, in the safe direction, so it is easy to miss. In manual mode it is obvious: the user types 0.29, the tool shows 0.28 and reports that the lot was adjusted to the step. In a panel that trades, the order is sent with 0.28 lots, and a 50% partial close of 0.58 closes one step less than half.
The fix: add a small epsilon before flooring. 1e-8 is far larger than the error of a double division (around 1e-14) and far smaller than one step. 0.29 stays 0.29, and a real fraction such as 0.289 still floors to 0.28, so the budget is never exceeded.
6. What the formula still leaves out
The "actual risk" above is the result of closing at the stop price. Two costs are not included:
- Spread. A buy opens at Ask and closes at Bid, so the spread is lost immediately.
- Commission. Charged per lot, usually on both sides.
The real loss is roughly formula risk + spread + commission. With high commissions or tight stops this is not negligible. A simple adjustment is to add the current spread to the stop distance, or to subtract the expected commission from the budget.
Exotic symbols can also report a tick value and quotes of zero before their market opens. A calculator should then show "not available" and retry later, not a huge number produced by a division by zero.
OrderCalcProfit has its own weak moment. After switching the terminal language I restarted MetaTrader 5 and immediately ran the script on HK50. OrderCalcProfit() returned success with a result of 0.00. The script fell back to the tick value, and the lot changed from 260 to 33. Thirty seconds later the same script gave the correct result. HK50 is priced in Hong Kong dollars, so my guess is that the HKD/USD conversion quotes were not loaded yet. The lessons: treat a zero result as "not ready yet", report it and calculate again later; and do not compute the point value once in OnInit() and cache it forever, refresh it on a timer.
Here is the Experts log, first run right after the restart (an earlier build that did not yet print the "returned 0" warning):
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)
And the same script 30 seconds later:
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. The complete script and test results
RiskLotCalc.mq5 (full source below) runs on any chart. For the current symbol it prints the budget, raw lot, final lot and actual risk for all nine modes, and cross-checks each final lot with OrderCalcProfit() at the stop price. When the broker's tick value disagrees, it prints an extra line. It never trades.
Inputs:
| Input | Default | Meaning |
|---|---|---|
| InpSlPoints | 300 | stop loss distance in points |
| InpRiskPct | 1.0 | risk percent for the % modes |
| InpRiskMoney | 100 | budget for the fixed money mode |
| InpCustomBase | 10000 | base for the custom % mode |
| InpManualLot | 0.29 | lot for manual mode (0.29 shows the rounding trap) |
Results from a MetaQuotes-Demo demo account on 26 October 2026, balance $99,822.23, default inputs.
XAUUSD. The first line reports the point value disagreement. All three historical bases come out as $100,000. The manual lot of 0.29 stays 0.29. Every line matches OrderCalcProfit() to the cent.
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. Point value 0.01275 USD, with the disagreement warning. The minimum lot is 1.00, so the manual 0.29 is raised to 1.00 with WARNING: raised to min lot, actual risk is above budget , which is rule 2 from section 4. Formula risk 99.45 against OrderCalcProfit() 99.43: the two cents come from converting HKD at two different prices.
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. A normal specification: no warning, and every line matches OrderCalcProfit() exactly.
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
Conclusion
Position sizing is one line of arithmetic. Reliability is in the details:
- Get the point value from OrderCalcProfit() , not from SYMBOL_TRADE_TICK_VALUE alone, and log any disagreement.
- Treat a zero result as "not ready", and refresh the cached value.
- Decide which "1%" you mean, and remember that historical bases use server time.
- Always round down, with an epsilon.
- When the minimum lot overrides the budget, say so, and always display the actual risk of the final lot.
Lot size controls how much a single trade can lose. Next time I will cover the other half of risk control: stopping trading when the daily loss limit is reached.
If the script shows a disagreement for a symbol on your broker, post the symbol and the two values in the comments.
Full source: RiskLotCalc.mq5
Save it as MQL5/Scripts/RiskLotCalc.mq5 , compile it in MetaEditor and drop it on any chart. It prints only and never trades.
//+------------------------------------------------------------------+ //| 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); } } //+------------------------------------------------------------------+


