β
Normalize Price (NormalizePriceAsync)ΒΆ
Sugar method: Normalizes price to symbol tick size (strict normalization, not just digits). Ensures price is valid for order placement.
API Information:
- Extension method:
MT5Service.NormalizePriceAsync(...)(fromMT5ServiceExtensions) - Package: Part of
mt5_term_apilibrary - Underlying calls:
SymbolInfoDoubleAsync(SymbolTradeTickSize)
Method SignatureΒΆ
public static class MT5ServiceExtensions
{
public static async Task<double> NormalizePriceAsync(
this MT5Service svc,
string symbol,
double price,
int timeoutSec = 10,
CancellationToken ct = default);
}
π½ InputΒΆ
| Parameter | Type | Description |
|---|---|---|
svc |
MT5Service |
MT5Service instance (extension method) |
symbol |
string |
Symbol name (e.g., "EURUSD") |
price |
double |
Price to normalize |
timeoutSec |
int |
Timeout in seconds (default: 10) |
ct |
CancellationToken |
Cancellation token |
β¬οΈ Output β doubleΒΆ
Returns the normalized price aligned to symbol's tick size.
Example:
- Input:
1.100005 - Tick size:
0.00001 - Output:
1.10001(rounded to nearest tick)
Exception: Throws InvalidOperationException if tick size is 0 or negative.
π¬ Just the essentialsΒΆ
- What it is. Strict price normalization using actual tick size (SYMBOL_TRADE_TICK_SIZE), not just rounding by digits.
- Why you need it. Broker rejects orders with invalid price precision. This ensures price is valid before sending to MT5.
- Sanity check. Returns price rounded to nearest valid tick. ALWAYS normalize calculated prices before order placement.
π― PurposeΒΆ
Use it before order placement:
- Normalize calculated SL/TP prices.
- Validate prices from user input.
- Ensure pending order prices are valid.
- Fix prices after calculations.
- Avoid "invalid price" errors.
π§© Notes & TipsΒΆ
- Tick size vs Point: Tick size can differ from point size. Always use tick size for normalization.
- Strict normalization: Uses SYMBOL_TRADE_TICK_SIZE property, not SYMBOL_POINT or digits.
- Rounding: Rounds to NEAREST valid tick, not floor/ceiling.
- Required: ALWAYS normalize before:
OrderSendAsync(),OrderModifyAsync(), pending order placement. - Performance: Tick size doesn't change. Cache if normalizing many prices.
- Formula:
normalized = round(price / tickSize) * tickSize
π§ Under the HoodΒΆ
This sugar method gets tick size and normalizes:
var deadline = DateTime.UtcNow.AddSeconds(timeoutSec);
// Get SYMBOL_TRADE_TICK_SIZE (not SYMBOL_POINT!)
var result = await svc.SymbolInfoDoubleAsync(symbol,
SymbolInfoDoubleProperty.SymbolTradeTickSize, deadline, ct);
double tickSize = result.Value;
// Validate tick size
if (tickSize <= 0)
throw new InvalidOperationException("SymbolTradeTickSize is 0");
// Normalize to nearest tick
double steps = Math.Round(price / tickSize);
return steps * tickSize;
What it improves:
- Strict normalization - uses tick size, not just digits
- Automatic validation - throws if tick size invalid
- Simple API - just pass symbol and price
- Guaranteed valid price - broker will accept it
π Low-Level AlternativeΒΆ
WITHOUT sugar (wrong approach):
// β Many developers do this (WRONG!):
var digits = await svc.GetDigitsAsync("EURUSD");
double normalized = Math.Round(price, digits); // Not strict enough!
WITHOUT sugar (correct but verbose):
var deadline = DateTime.UtcNow.AddSeconds(10);
var result = await svc.SymbolInfoDoubleAsync("EURUSD",
SymbolInfoDoubleProperty.SymbolTradeTickSize, deadline, ct);
double tickSize = result.Value;
if (tickSize <= 0)
throw new InvalidOperationException("Invalid tick size");
double normalized = Math.Round(price / tickSize) * tickSize;
WITH sugar:
Benefits:
- β 6 lines β 1 line
- β Uses correct property (tick size, not point or digits)
- β Automatic validation
- β Clearer intent
π Usage ExamplesΒΆ
1) Basic price normalizationΒΆ
// svc β MT5Service instance
double rawPrice = 1.100005;
double normalized = await svc.NormalizePriceAsync("EURUSD", rawPrice);
Console.WriteLine($"Raw: {rawPrice}");
Console.WriteLine($"Normalized: {normalized}");
// Output:
// Raw: 1.100005
// Normalized: 1.10001
2) Normalize SL before order placementΒΆ
var point = await svc.GetPointAsync("EURUSD");
double entryPrice = 1.10000;
double rawSL = entryPrice - (100 * point); // Might have rounding errors
// Normalize before sending
double normalizedSL = await svc.NormalizePriceAsync("EURUSD", rawSL);
Console.WriteLine($"Entry: {entryPrice}");
Console.WriteLine($"Raw SL: {rawSL}");
Console.WriteLine($"Normalized SL: {normalizedSL}");
// Use normalizedSL in order
3) Normalize TP after calculationΒΆ
var point = await svc.GetPointAsync("GBPUSD");
double entryPrice = 1.25000;
double rawTP = entryPrice + (200 * point);
// Normalize to valid tick
double normalizedTP = await svc.NormalizePriceAsync("GBPUSD", rawTP);
Console.WriteLine($"Entry: {entryPrice}");
Console.WriteLine($"Raw TP: {rawTP}");
Console.WriteLine($"Normalized TP: {normalizedTP}");
4) Validate user input priceΒΆ
string userInput = "1.100055";
if (double.TryParse(userInput, out double userPrice))
{
double normalized = await svc.NormalizePriceAsync("EURUSD", userPrice);
if (Math.Abs(normalized - userPrice) > 0.0000001)
{
Console.WriteLine($"β Price adjusted from {userPrice} to {normalized}");
}
else
{
Console.WriteLine($"β Price {userPrice} is valid");
}
// Use normalized price
}
5) Normalize pending order priceΒΆ
var tick = await svc.SymbolInfoTickAsync("EURUSD", ...);
var point = await svc.GetPointAsync("EURUSD");
// Place Buy Limit 50 points below current Ask
double rawPrice = tick.Ask - (50 * point);
double normalizedPrice = await svc.NormalizePriceAsync("EURUSD", rawPrice);
Console.WriteLine($"Current Ask: {tick.Ask}");
Console.WriteLine($"Raw Limit: {rawPrice}");
Console.WriteLine($"Normalized: {normalizedPrice}");
// Use normalizedPrice for pending order
6) Normalize price ladderΒΆ
var point = await svc.GetPointAsync("EURUSD");
double basePrice = 1.10000;
int levels = 5;
Console.WriteLine("Normalized Price Ladder:");
Console.WriteLine("βββββββββββββββββββββββββββββββββ");
for (int i = 0; i <= levels; i++)
{
double rawPrice = basePrice + (i * 10 * point);
double normalized = await svc.NormalizePriceAsync("EURUSD", rawPrice);
Console.WriteLine($"Level {i}: {normalized:F5}");
}
7) Fix prices from external sourceΒΆ
// Prices from external API or calculation
double[] externalPrices = { 1.100005, 1.100123, 1.099987 };
Console.WriteLine("Fixing External Prices:");
Console.WriteLine("βββββββββββββββββββββββββββββββββ");
foreach (var rawPrice in externalPrices)
{
double normalized = await svc.NormalizePriceAsync("EURUSD", rawPrice);
Console.WriteLine($"{rawPrice:F6} β {normalized:F5}");
}
8) Batch normalizationΒΆ
async Task<double[]> NormalizeBatch(string symbol, double[] prices)
{
var result = new double[prices.Length];
for (int i = 0; i < prices.Length; i++)
{
result[i] = await svc.NormalizePriceAsync(symbol, prices[i]);
}
return result;
}
// Usage
double[] rawPrices = { 1.10005, 1.10015, 1.10025 };
double[] normalized = await NormalizeBatch("EURUSD", rawPrices);
Console.WriteLine("Batch Normalization:");
for (int i = 0; i < rawPrices.Length; i++)
{
Console.WriteLine($" {rawPrices[i]:F6} β {normalized[i]:F5}");
}
9) Compare normalization vs roundingΒΆ
var digits = await svc.GetDigitsAsync("EURUSD");
double rawPrice = 1.100055;
// Method 1: Round by digits (WRONG for orders!)
double roundedByDigits = Math.Round(rawPrice, digits);
// Method 2: Normalize by tick size (CORRECT!)
double normalizedByTick = await svc.NormalizePriceAsync("EURUSD", rawPrice);
Console.WriteLine($"Raw price: {rawPrice:F6}");
Console.WriteLine($"Rounded by digits: {roundedByDigits:F5}");
Console.WriteLine($"Normalized by tick: {normalizedByTick:F5}");
if (roundedByDigits != normalizedByTick)
{
Console.WriteLine("β Methods give different results!");
}
10) Cached normalization for performanceΒΆ
// Cache tick size for multiple normalizations
Dictionary<string, double> tickSizeCache = new();
async Task<double> NormalizeCached(string symbol, double price)
{
if (!tickSizeCache.ContainsKey(symbol))
{
var tickSize = (await svc.SymbolInfoDoubleAsync(symbol,
SymbolInfoDoubleProperty.SymbolTradeTickSize,
DateTime.UtcNow.AddSeconds(10),
default)).Value;
tickSizeCache[symbol] = tickSize;
}
double tick = tickSizeCache[symbol];
if (tick <= 0) throw new InvalidOperationException("Invalid tick size");
return Math.Round(price / tick) * tick;
}
// Usage - first call fetches, subsequent calls use cache
var p1 = await NormalizeCached("EURUSD", 1.10005); // Fetches tick size
var p2 = await NormalizeCached("EURUSD", 1.10015); // Uses cache
var p3 = await NormalizeCached("EURUSD", 1.10025); // Uses cache
Console.WriteLine($"Normalized: {p1:F5}, {p2:F5}, {p3:F5}");
π Related MethodsΒΆ
π¦ Low-level method used internally:
SymbolInfoDoubleAsync(SymbolTradeTickSize)- Get tick size for normalization
π¬ Other sugar methods:
GetPointAsync()- Get point size (for calculations, not normalization)GetDigitsAsync()- Get digits (for display only, NOT for normalization)GetSymbolSnapshot()- Get tick, point, digits, and marginNormalizeVolumeAsync()- Normalize volume (similar concept for volumes)- All order placement methods should use this to normalize SL/TP prices
β οΈ Common PitfallsΒΆ
-
Using Math.Round(price, digits) instead
-
Assuming tick size equals point size
-
Not normalizing calculated prices
// β WRONG - sending calculated price directly var point = await svc.GetPointAsync("EURUSD"); double slPrice = entry - (100 * point); // OrderSendAsync(..., sl: slPrice, ...); // Might be invalid! // β CORRECT - normalize before sending var point = await svc.GetPointAsync("EURUSD"); double slPrice = await svc.NormalizePriceAsync("EURUSD", entry - (100 * point)); // OrderSendAsync(..., sl: slPrice, ...); -
Normalizing volumes with this method
-
Not checking for errors
// β WRONG - no error handling double normalized = await svc.NormalizePriceAsync("EURUSD", price); // β CORRECT - handle potential errors try { double normalized = await svc.NormalizePriceAsync("EURUSD", price); // Use normalized price } catch (InvalidOperationException ex) { Console.WriteLine($"Normalization failed: {ex.Message}"); // Handle error }