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>
474 lines
20 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+
|