//+------------------------------------------------------------------+ //| Symbol.mqh| //| Copyright 2026, bandersoft | //| https://www.mql5.com/en/users/fats | //+------------------------------------------------------------------+ //| | //| Pip size, price/pip conversion, normalisation, broker checks. | //| | //+------------------------------------------------------------------+ #ifndef STOPFIRST_SYMBOL_MQH #define STOPFIRST_SYMBOL_MQH #include #include "Defs.mqh" //--- per-symbol pip size overrides, set through SetPipSize() string g_pipSymbols[]; double g_pipValues[]; //+------------------------------------------------------------------+ //| Find a symbol's override slot, or -1 | //| | //| Returns: the index into the override arrays, or -1 if the symbol | //| has no override. | //+------------------------------------------------------------------+ int PipOverrideIndex(const string symbol) { int total = ArraySize(g_pipSymbols); for(int i = 0; i < total; i++) if(g_pipSymbols[i] == symbol) return i; return -1; } //+------------------------------------------------------------------+ //| Declare what one pip is worth for a symbol | //| | //| The built-in rule below is the forex convention and is wrong for | //| instruments where a pip means something else - by 10x on gold | //| quoted to 2 digits, by 100x on crypto. No symbol property | //| distinguishes those cases, so the caller has to say: | //| | //| SetPipSize("XAUUSD", 0.1); | //| | //| Pass pipSize <= 0 to drop the override again. | //| Returns: nothing. | //+------------------------------------------------------------------+ void SetPipSize(string symbol, double pipSize) { if(symbol == "") symbol = _Symbol; int index = PipOverrideIndex(symbol); if(pipSize <= 0) { if(index < 0) return; //--- close the gap with the last entry, then shrink int last = ArraySize(g_pipSymbols) - 1; g_pipSymbols[index] = g_pipSymbols[last]; g_pipValues[index] = g_pipValues[last]; ArrayResize(g_pipSymbols, last); ArrayResize(g_pipValues, last); return; } if(index >= 0) { g_pipValues[index] = pipSize; return; } int size = ArraySize(g_pipSymbols); ArrayResize(g_pipSymbols, size + 1); ArrayResize(g_pipValues, size + 1); g_pipSymbols[size] = symbol; g_pipValues[size] = pipSize; } //+------------------------------------------------------------------+ //| Forget every pip size override | //| Returns: nothing. | //+------------------------------------------------------------------+ void ClearPipSizes(void) { ArrayResize(g_pipSymbols, 0); ArrayResize(g_pipValues, 0); } //+------------------------------------------------------------------+ //| Get the size of one pip, in price units | //| | //| This is the single definition of a pip for the whole library. | //| Everything that needs one calls here - no local copies. | //| | //| Returns: the override if one was set, otherwise point * 10 on a | //| 3- or 5-digit symbol and point on any other. | //| 0 if the symbol is unknown. | //+------------------------------------------------------------------+ double GetPipSize(string symbol = "") { if(symbol == "") symbol = _Symbol; int index = PipOverrideIndex(symbol); if(index >= 0) return g_pipValues[index]; CSymbolInfo symbolInfo; if(!symbolInfo.Name(symbol)) return 0; double point = symbolInfo.Point(); int digits = symbolInfo.Digits(); return (digits == 3 || digits == 5) ? point * 10 : point; } //+------------------------------------------------------------------+ //| Convert price difference to pips | //| | //| Returns: the difference in pips, not rounded - rounding to one | //| decimal would report a 0.04 pip spread as 0.0. 0 if the | //| symbol is unknown. | //+------------------------------------------------------------------+ double PriceToPips(double priceDiff, string symbol = "") { double pipSize = GetPipSize(symbol); if(pipSize == 0) return 0; //--- a unit conversion does not round: one decimal would report a //--- 0.04 pip spread as 0.0 return priceDiff / pipSize; } //+------------------------------------------------------------------+ //| Convert pips to price difference | //| | //| Returns: the distance in price units. 0 if the symbol is unknown. | //+------------------------------------------------------------------+ double PipsToPrice(double pips, string symbol = "") { double pipSize = GetPipSize(symbol); return pips * pipSize; } //+------------------------------------------------------------------+ //| Get spread in pips | //| | //| Returns: Ask - Bid in pips. 0 if the symbol is unknown, which is | //| also a plausible real spread. | //+------------------------------------------------------------------+ double GetSpreadPips(string symbol = "") { if(symbol == "") symbol = _Symbol; CSymbolInfo symbolInfo; if(!symbolInfo.Name(symbol)) return 0; //--- without fresh rates Ask and Bid can be stale or zero if(!symbolInfo.RefreshRates()) return 0; double spread = symbolInfo.Ask() - symbolInfo.Bid(); return PriceToPips(spread, symbol); } //+------------------------------------------------------------------+ //| Check if market is open for trading | //| | //| Returns: true if the server clock falls inside ANY of the symbol's| //| trading sessions for today. false if the symbol is | //| unknown, trading on it is disabled, or no session covers | //| the current time. | //+------------------------------------------------------------------+ bool IsMarketOpen(string symbol = "") { if(symbol == "") symbol = _Symbol; CSymbolInfo symbolInfo; if(!symbolInfo.Name(symbol)) return false; //--- a symbol can still be quoted while trading on it is switched off if(symbolInfo.TradeMode() == SYMBOL_TRADE_MODE_DISABLED) return false; MqlDateTime dt; //--- TimeTradeServer, not TimeCurrent: TimeCurrent is the time of the //--- last tick, which stops at Friday 23:59 all weekend and would read //--- as inside Friday's session on Saturday. TimeTradeServer keeps //--- running. TimeToStruct(TimeTradeServer(), dt); //--- SymbolInfoSessionTrade reports from/to as seconds since //--- midnight, so compare against the clock time of day int secondsOfDay = dt.hour * 3600 + dt.min * 60 + dt.sec; datetime from = 0, to = 0; //--- reading session 0 only would show every symbol with a midday //--- break - indices, metals, most CFDs - as closed during its second //--- session. Walk the sessions until the broker runs out. for(uint session = 0; session < 16; session++) { if(!SymbolInfoSessionTrade(symbol, (ENUM_DAY_OF_WEEK)dt.day_of_week, session, from, to)) break; //--- the end is exclusive: a session to 23:00 is closed AT 23:00. //--- "<=" said open at 23:00:00 sharp - the moment a new H1 bar //--- opens - and US30 rejected a trade as "market closed" there //--- (tester, 2026-10-14) if(secondsOfDay >= (int)from && secondsOfDay < (int)to) return true; } return false; } //+------------------------------------------------------------------+ //| Format price with correct digits | //| | //| Returns: the price as text at the symbol's digit count, or at 5 | //| decimals if the symbol is unknown. | //+------------------------------------------------------------------+ string FormatPrice(double price, string symbol = "") { if(symbol == "") symbol = _Symbol; CSymbolInfo symbolInfo; if(!symbolInfo.Name(symbol)) return DoubleToString(price, 5); return DoubleToString(price, symbolInfo.Digits()); } //+------------------------------------------------------------------+ //| Decimal places implied by a volume step (0.01 -> 2, 0.001 -> 3) | //| Returns: 0..8, defaulting to 2 for a nonsensical step. | //+------------------------------------------------------------------+ int VolumeDigits(const double volumeStep) { if(volumeStep <= 0) return 2; double scaled = volumeStep; for(int digits = 0; digits < 8; digits++) { if(MathAbs(scaled - MathRound(scaled)) < 1e-9) return digits; scaled *= 10.0; } return 8; } //+------------------------------------------------------------------+ //| Round a volume DOWN to the broker's step | //| | //| Rounding a position size up raises risk the caller never asked | //| for. The epsilon keeps 0.3 / 0.1 from landing on 2.9999... and | //| flooring to 0.2. | //| | //| Returns: the volume, floored to the step and rounded to the | //| step's own precision. | //+------------------------------------------------------------------+ double FloorToVolumeStep(const double volume, const double volumeStep) { if(volumeStep <= 0) return volume; double steps = volume / volumeStep; double rounded = MathRound(steps); if(MathAbs(steps - rounded) < 1e-7) steps = rounded; else steps = MathFloor(steps); return NormalizeDouble(steps * volumeStep, VolumeDigits(volumeStep)); } //+------------------------------------------------------------------+ //| Is this order type a buy of some kind? | //| Returns: true for BUY, BUY_LIMIT, BUY_STOP and BUY_STOP_LIMIT. | //+------------------------------------------------------------------+ bool IsBuySide(const ENUM_ORDER_TYPE orderType) { return orderType == ORDER_TYPE_BUY || orderType == ORDER_TYPE_BUY_LIMIT || orderType == ORDER_TYPE_BUY_STOP || orderType == ORDER_TYPE_BUY_STOP_LIMIT; } //+------------------------------------------------------------------+ //| Minimum distance a stop or pending price must keep from market | //| | //| SYMBOL_TRADE_STOPS_LEVEL is what the server enforces; | //| SYMBOL_TRADE_FREEZE_LEVEL is how close an existing order may sit | //| before it can no longer be modified. The larger of the two is the | //| conservative reading. | //| | //| A broker reporting 0 is using a dynamic, spread-derived level | //| rather than declaring "no constraint", so fall back to the | //| current spread instead of assuming anything goes. | //| | //| Returns: the distance in PRICE units, or 0 if the symbol is | //| unknown. | //+------------------------------------------------------------------+ double MinStopDistance(string symbol = "") { if(symbol == "") symbol = _Symbol; CSymbolInfo symbolInfo; if(!symbolInfo.Name(symbol)) return 0; symbolInfo.RefreshRates(); double point = symbolInfo.Point(); int levels = (int)MathMax(symbolInfo.StopsLevel(), symbolInfo.FreezeLevel()); double distance = levels * point; if(distance <= 0) { double spread = symbolInfo.Ask() - symbolInfo.Bid(); if(spread > 0) distance = spread; } return distance; } //+------------------------------------------------------------------+ //| Would the server accept these stop levels? | //| | //| Validates rather than adjusts, deliberately. Silently widening a | //| stop to make it legal changes the risk without telling the | //| caller, which is worse than refusing the order. | //| | //| Parameters: | //| price - the price stops are measured from: the entry for a | //| pending order, the CLOSING side for an open position | //| (Bid for a long, Ask for a short) | //| sl, tp - pass 0 for "not set", which always passes | //| | //| Returns: true if both levels clear MinStopDistance and sit on the | //| correct side of price. false otherwise, or if the symbol | //| is unknown. | //+------------------------------------------------------------------+ bool CheckStopLevels(string symbol, const ENUM_ORDER_TYPE orderType, const double price, const double sl, const double tp) { if(symbol == "") symbol = _Symbol; CSymbolInfo symbolInfo; if(!symbolInfo.Name(symbol)) return false; double distance = MinStopDistance(symbol); double tolerance = symbolInfo.Point() * 0.5; bool isBuy = IsBuySide(orderType); if(sl > 0) { double gap = isBuy ? (price - sl) : (sl - price); if(gap < distance - tolerance) return false; } if(tp > 0) { double gap = isBuy ? (tp - price) : (price - tp); if(gap < distance - tolerance) return false; } return true; } //+------------------------------------------------------------------+ //| Would the server accept this pending order price? | //| | //| A limit sits on the far side of the market, a stop on the near | //| side, and both must clear MinStopDistance. Getting the side wrong | //| is retcode 10015, and it is easy to do when a signal flips. | //| | //| Returns: true if the price is on the correct side at a legal | //| distance. false otherwise, for a non-pending type, and for | //| BUY_STOP_LIMIT and SELL_STOP_LIMIT, which are pending but | //| need a second (limit) price this function does not take. | //+------------------------------------------------------------------+ bool CheckPendingPrice(string symbol, const ENUM_ORDER_TYPE orderType, const double price) { if(symbol == "") symbol = _Symbol; CSymbolInfo symbolInfo; if(!symbolInfo.Name(symbol)) return false; if(!symbolInfo.RefreshRates()) return false; double distance = MinStopDistance(symbol); double tolerance = symbolInfo.Point() * 0.5; double gap = 0; switch(orderType) { case ORDER_TYPE_BUY_LIMIT: gap = symbolInfo.Ask() - price; break; case ORDER_TYPE_BUY_STOP: gap = price - symbolInfo.Ask(); break; case ORDER_TYPE_SELL_LIMIT: gap = price - symbolInfo.Bid(); break; case ORDER_TYPE_SELL_STOP: gap = symbolInfo.Bid() - price; break; default: return false; } return gap >= distance - tolerance; } //+------------------------------------------------------------------+ //| Is this volume tradable on this symbol? | //| Returns: true if within [min, max] and sitting on the volume | //| step. false otherwise, or if the symbol is unknown. | //+------------------------------------------------------------------+ bool IsVolumeValid(string symbol, const double volume) { if(symbol == "") symbol = _Symbol; CSymbolInfo symbolInfo; if(!symbolInfo.Name(symbol)) return false; if(volume <= 0) return false; if(volume < symbolInfo.LotsMin() - 1e-9) return false; if(volume > symbolInfo.LotsMax() + 1e-9) return false; double step = symbolInfo.LotsStep(); if(step <= 0) return false; return MathAbs(volume - FloorToVolumeStep(volume, step)) < 1e-9; } //+------------------------------------------------------------------+ //| Can positions be opened on this symbol right now? | //| | //| Close-only, short-only and long-only are all states a broker uses | //| routinely - around rollover, or on an expiring contract. | //| | //| Returns: true only for SYMBOL_TRADE_MODE_FULL. false otherwise, | //| or if the symbol is unknown. | //+------------------------------------------------------------------+ bool CanOpenPositions(string symbol = "") { if(symbol == "") symbol = _Symbol; CSymbolInfo symbolInfo; if(!symbolInfo.Name(symbol)) return false; return symbolInfo.TradeMode() == SYMBOL_TRADE_MODE_FULL; } //+------------------------------------------------------------------+ //| Normalize lot size to broker requirements | //| | //| Rounds DOWN to the volume step, then clamps into | //| [min, max]. Clamping up to the minimum is this function's stated | //| contract - the caller has already chosen a size and is asking for | //| the nearest legal one. CalcLotSizeByRisk deliberately does not do | //| that, because there the minimum may exceed the risk budget. | //| | //| Returns: a tradable lot size. The input unchanged if the symbol | //| is unknown. | //+------------------------------------------------------------------+ double NormalizeLots(double lots, string symbol = "") { if(symbol == "") symbol = _Symbol; CSymbolInfo symbolInfo; if(!symbolInfo.Name(symbol)) return lots; double minLot = symbolInfo.LotsMin(); double maxLot = symbolInfo.LotsMax(); double lotStep = symbolInfo.LotsStep(); if(lotStep <= 0) return lots; //--- floor to the step at the step's own precision, not a //--- hardcoded 2 decimals, which broke 0.001-step symbols lots = FloorToVolumeStep(lots, lotStep); lots = MathMax(minLot, MathMin(maxLot, lots)); return NormalizeDouble(lots, VolumeDigits(lotStep)); } #endif // STOPFIRST_SYMBOL_MQH //+------------------------------------------------------------------+