A price can have the correct number of decimal places and still sit between valid trading increments. If an illustrative instrument displays two decimals but uses a tick size of 0.25, a price of 100.13 looks correctly formatted. It does not belong to the quarter-unit grid.
This guide solves price alignment before an MT5 order is prepared. It separates display precision from the trading increment, gives an exact-decimal rounding function, and shows why the rounded price must be checked again against the intended order and risk. This is distinct from rounding the number of lots, covered in the position-sizing guide.
Three units that answer different questions
MetaQuotes lists SYMBOL_DIGITS, SYMBOL_POINT and SYMBOL_TRADE_TICK_SIZE separately in its symbol properties reference. Digits describes decimal precision, point describes a point value, and tick size describes the minimum price change. Do not replace the latter with a power of ten merely because it matches on a familiar forex symbol.
For a synthetic specification with digits 2 and tick size 0.25, the display unit is 0.01 while successive tick-grid prices differ by 0.25. Values 100.00, 100.25 and 100.50 lie on a zero-origin grid; 100.13 does not. This grid assumption should be verified for the instrument. More complex venue price-band rules require a more complete validator.
MetaKit exposes digits and tick_size through GET /v1/accounts/{id}/symbols/{symbol}. Its response also includes stops_level and freeze_level. These are different restrictions: a grid-aligned price can still fail a distance or modification rule. Use the broker's exact symbol and fetch its specification rather than inferring the tick from a suffix or instrument nickname.
Choose the rounding direction before the implementation
There is no universally safe “nearest valid price” for an order. A ceiling increases a positive price; a floor decreases it. Whether that preserves your intention depends on the side, order type and the role of the price.
| Intended constraint | Possible grid operation | Recheck afterward |
|---|---|---|
| Buy limit must not exceed a chosen maximum | Floor | Distance below the current ask |
| Sell limit must not fall below a chosen minimum | Ceiling | Distance above the current bid |
| Buy stop must not trigger below a threshold | Ceiling | Distance above the current ask |
| Sell stop must not trigger above a threshold | Floor | Distance below the current bid |
Those are constraint-preserving examples, not universal trading recommendations. A stop loss has another consequence: rounding a long position's stop downward increases entry-to-stop distance. Rounding it upward may reduce that distance but violate a broker minimum. Silently picking one direction changes the trade specification.
An equally valid application policy is to reject off-grid inputs and require the strategy or user to choose again. That is often preferable when a price was explicitly approved. The pending-order explanation provides context for which side of the market each order belongs on.
Use decimal strings at the calculation boundary
Python's decimal documentation explains decimal arithmetic and the difference between constructing a decimal from text and from a binary float. Start with controlled decimal strings where possible. A string conversion after an earlier imprecise calculation cannot reconstruct precision that was already lost.
Also, quantize(Decimal("0.25")) alone does not mean “round to a multiple of 0.25.” It sets decimal exponent precision. The operation needed here is division by tick size, integral rounding in tick units, then multiplication by tick size.
This original Python helper accepts positive plain-decimal strings, a limited display precision and a zero-origin tick grid. It makes the direction explicit and returns a formatted string. It performs no API requests and places no trades.
import re
from decimal import Decimal, localcontext, ROUND_FLOOR, ROUND_CEILING
def align_price(price, tick_size, digits, mode="reject"):
if type(digits) is not int or not 0 <= digits <= 12:
raise ValueError("Unsupported display precision")
if mode not in ("reject", "floor", "ceiling"):
raise ValueError("Choose reject, floor or ceiling")
for value in (price, tick_size):
if (not isinstance(value, str) or len(value) > 40 or
not re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", value)):
raise ValueError("Use bounded plain-decimal strings")
with localcontext() as ctx:
ctx.prec = 90
p, tick = Decimal(price), Decimal(tick_size)
if p <= 0 or tick <= 0:
raise ValueError("Price and tick must be positive")
scaled_tick = tick * (Decimal(10) ** digits)
if scaled_tick != scaled_tick.to_integral_value():
raise ValueError("Tick is incompatible with display precision")
ticks = p / tick
if mode == "reject":
count = ticks.to_integral_value()
if count * tick != p:
raise ValueError("Off-grid price")
else:
rounding = ROUND_FLOOR if mode == "floor" else ROUND_CEILING
count = ticks.to_integral_value(rounding=rounding)
aligned = count * tick
if aligned <= 0:
raise ValueError("Rounding produced a nonpositive price")
quantum = Decimal(1).scaleb(-digits)
return format(aligned.quantize(quantum), "f")For price "100.13", tick "0.25" and digits 2, floor returns "100.00", ceiling returns "100.25", and reject raises an exception. For "1.083427" with tick "0.00001" and digits 5, floor returns "1.08342" and ceiling returns "1.08343". These are synthetic calculations; they are not current broker specifications.
Exercise already-aligned prices, non-power-of-ten ticks such as 0.05, a tick smaller than the declared display unit, zero values, unsupported modes and an input that floors to zero. Verify idempotence: aligning an already aligned result under the same rules must return the same value.
Recalculate distance and risk after alignment
Suppose an illustrative long entry is 100.50, the desired stop is 100.13, and the verified tick size is 0.25. The original distance is 0.37. Flooring the stop to 100.00 produces distance 0.50, about 35.14% larger. Ceiling to 100.25 produces distance 0.25, about 32.43% smaller. A lot size calculated using 0.37 no longer describes either amended stop.
Choose the permitted price, then redo the risk calculation using its actual distance. Include the broker's contract and tick-value conventions, costs and execution uncertainty. A grid-aligned stop is not a guaranteed fill price. The slippage guide explains that separate measurement.
Refresh the relevant quote before final validation. A valid pending-price relationship can become invalid while the application is calculating. Keep both proposed and aligned prices in the order preview so the change is reviewable. Do not automatically widen a stop or change position size merely to make a rejected request pass.
Fit the helper into a hosted API workflow
Keep this calculation in your application. The current MetaKit public trading handler checks several order constraints but does not provide this helper as an endpoint or promise to align every submitted price to tick_size. Read the current API documentation, preserve the accepted order schema, and handle a terminal rejection even after local checks succeed.
The helper returns text for exact calculation and display. MetaKit's current order schema uses JSON numbers for price fields; serialization into that contract is a separate boundary to test. Do not replace the documented numeric field with a string. Inspect the emitted request in a synthetic test, keep credentials out of logs, and use a demo environment for any later integration verification.
Sources and verification
Verified 21 October 2026. Symbol fields and trading-handler behavior were checked read-only against the current implementation. Decimal examples and risk-distance calculations were checked independently.
- MetaQuotes symbol properties, precision and trading increments.
- Python decimal documentation, decimal construction, precision and rounding operations.
- MetaKit API documentation, symbol and order contracts.