StopFirst-Core/Include/StopFirst/Symbol.mqh
RUSLAN SLOBODIANIUK 6ba79702fd Make the header comments self-contained
They referred to project documents that are not part of this repository.
Comments only; PriceToPips is now documented as not rounding, which is what
it does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:48:01 +03:00

474 lines
20 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 <Trade\SymbolInfo.mqh>
#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
//+------------------------------------------------------------------+