2026-07-26 23:08:32 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| TradeChecks.mqh |
|
|
|
|
|
//| AnimateDread |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Pre-trade validation library implementing every runtime check from |
|
|
|
|
|
//| "The Checks a Trading Robot Must Pass Before Publication in the |
|
|
|
|
|
//| Market" (https://www.mql5.com/en/articles/2555). One function per |
|
|
|
|
|
//| rule, all free functions prefixed TC*, so any call site (money |
|
|
|
|
|
//| management, signal SL/TP shaping, the expert's trade paths) can |
|
|
|
|
|
//| apply the same rule without duplicating the symbol-property math. |
|
|
|
|
|
//| |
|
|
|
|
|
//| Article section -> function map: |
|
|
|
|
|
//| #2 insufficient funds ......... TCCheckMoneyForTrade / |
|
|
|
|
|
//| TCFitVolumeToFreeMargin |
|
|
|
|
|
//| #3 invalid volumes ........... TCCheckVolumeValue / |
|
|
|
|
|
//| TCNormalizeVolume |
|
|
|
|
|
//| #4 pending-order count limit .. TCIsNewOrderAllowed |
|
|
|
|
|
//| #5 per-symbol volume limit .... TCSymbolVolumeAllowed / |
|
|
|
|
|
//| TCApplySymbolVolumeLimit |
|
|
|
|
|
//| #6 SYMBOL_TRADE_STOPS_LEVEL ... TCCheckStops / TCAdjustStops / |
|
|
|
|
|
//| TCCheckPendingPrice |
|
|
|
|
|
//| #7 SYMBOL_TRADE_FREEZE_LEVEL .. TCFreezeOkForPosition / |
|
|
|
|
|
//| TCFreezeOkForOrder |
|
|
|
|
|
//| #8 insufficient history ....... TCHasEnoughHistory |
|
|
|
|
|
//| #9 array out of range ......... TCIndexOk |
|
|
|
|
|
//| #10 zero divide ................ TCSafeDivide |
|
|
|
|
|
//| #11 no-op modification ......... TCPositionModifyIsMeaningful / |
|
|
|
|
|
//| TCOrderModifyIsMeaningful |
|
|
|
|
|
//| #14 invalid function params .... TCSymbolIsTradeable |
|
|
|
|
|
//| #16 CPU / memory ............... TCWarnIfSlow / TCMemoryUsedMb |
|
|
|
|
|
//| |
|
|
|
|
|
//| Sections #12 (no DLL imports) and #13 (custom indicators embedded |
|
|
|
|
|
//| as resources) are build-time rules, already handled by the |
|
|
|
|
|
//| WARRIOR_MARKET_BUILD switch - see Warrior_EA.mq5's #resource block |
|
|
|
|
|
//| and Variables\IndicatorResources.mqh. #15 (access violation) has |
|
|
|
|
|
//| no runtime check by definition; the NULL-pointer/index guards here |
|
|
|
|
|
//| and at the call sites are what prevent it. |
|
|
|
|
|
//| |
|
|
|
|
|
//| Logging: every rejection is reported through TCLog(), which |
|
|
|
|
|
//| throttles per message key so a condition that repeats on every |
|
|
|
|
|
//| tick (e.g. "not enough free margin") writes one journal line per |
|
|
|
|
|
//| TC_LOG_THROTTLE_SECONDS instead of thousands - the article's own |
|
|
|
|
|
//| "output an error message instead of calling OrderSend()" advice, |
|
|
|
|
|
//| without the journal flood that makes the log unreadable. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
#ifndef WARRIOR_TRADECHECKS_MQH
|
|
|
|
|
#define WARRIOR_TRADECHECKS_MQH
|
|
|
|
|
|
|
|
|
|
//--- one journal line per distinct message key per this many seconds
|
|
|
|
|
#define TC_LOG_THROTTLE_SECONDS 60
|
|
|
|
|
//--- volume comparisons are done in units of 1/1000 of a volume step, so floating-point
|
|
|
|
|
//--- representation error in e.g. 0.1/0.01 can never make a legal volume look off-step
|
|
|
|
|
#define TC_VOLUME_EPSILON_FRAC 0.001
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Throttled journal output. `key` identifies the CONDITION (not the |
|
|
|
|
|
//| formatted text), so a message whose numbers change every tick |
|
|
|
|
|
//| still collapses to one line per throttle window. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
void TCLog(const string key, const string message)
|
|
|
|
|
{
|
|
|
|
|
static string s_keys[];
|
|
|
|
|
static datetime s_times[];
|
|
|
|
|
datetime now = TimeCurrent();
|
|
|
|
|
int total = ArraySize(s_keys);
|
|
|
|
|
for(int i = 0; i < total; i++)
|
|
|
|
|
{
|
|
|
|
|
if(s_keys[i] != key)
|
|
|
|
|
continue;
|
|
|
|
|
if(now - s_times[i] < TC_LOG_THROTTLE_SECONDS)
|
|
|
|
|
return;
|
|
|
|
|
s_times[i] = now;
|
|
|
|
|
Print(message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
ArrayResize(s_keys, total + 1);
|
|
|
|
|
ArrayResize(s_times, total + 1);
|
|
|
|
|
s_keys[total] = key;
|
|
|
|
|
s_times[total] = now;
|
|
|
|
|
Print(message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #10 - zero divide. |
|
|
|
|
|
//| Returns `fallback` instead of raising the runtime error whenever |
|
|
|
|
|
//| the denominator is zero or not a finite number. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
double TCSafeDivide(const double numerator, const double denominator, const double fallback = 0.0)
|
|
|
|
|
{
|
|
|
|
|
if(!MathIsValidNumber(numerator) || !MathIsValidNumber(denominator) || denominator == 0.0)
|
|
|
|
|
return fallback;
|
|
|
|
|
double result = numerator / denominator;
|
|
|
|
|
return MathIsValidNumber(result) ? result : fallback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #9 - array out of range. |
|
|
|
|
|
//| The index rule the article states verbatim: an index may not be |
|
|
|
|
|
//| negative and must be strictly less than ArraySize(). |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCIndexOk(const int index, const int size)
|
|
|
|
|
{
|
|
|
|
|
return(index >= 0 && index < size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Symbol property accessors. All of them return a safe default when |
|
|
|
|
|
//| the broker has not synced the property yet, so a caller never |
|
|
|
|
|
//| divides by / compares against a garbage 0. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
double TCPoint(const string symbol)
|
|
|
|
|
{
|
|
|
|
|
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
|
|
|
return (point > 0.0) ? point : _Point;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int TCDigits(const string symbol)
|
|
|
|
|
{
|
|
|
|
|
return((int)SymbolInfoInteger(symbol, SYMBOL_DIGITS));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Price formatter for the diagnostics below. Deliberately builds |
|
|
|
|
|
//| the string with DoubleToString() rather than a "%.*f" format: |
|
|
|
|
|
//| StringFormat() does not support printf's star-precision form, so |
|
|
|
|
|
//| a "%.*f" would print the digit count as a separate argument and |
|
|
|
|
|
//| silently shift every remaining placeholder by one. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
string TCPrice(const string symbol, const double price)
|
|
|
|
|
{
|
|
|
|
|
return DoubleToString(price, TCDigits(symbol));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double TCVolumeMin(const string symbol)
|
|
|
|
|
{
|
|
|
|
|
return SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double TCVolumeMax(const string symbol)
|
|
|
|
|
{
|
|
|
|
|
return SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double TCVolumeStep(const string symbol)
|
|
|
|
|
{
|
|
|
|
|
double step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
|
|
|
|
//--- a 0 step would make every "is it a multiple of the step" test a division by zero (article #10)
|
|
|
|
|
return (step > 0.0) ? step : TCVolumeMin(symbol);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #6/#7 - the two broker distance levels, in price units. |
|
|
|
|
|
//| |
|
|
|
|
|
//| SYMBOL_TRADE_STOPS_LEVEL is the minimum distance between an |
|
|
|
|
|
//| order/stop price and the current market price. Many brokers |
|
|
|
|
|
//| publish 0 here and instead enforce a floating, spread-derived |
|
|
|
|
|
//| limit, so the effective minimum used throughout this library is |
|
|
|
|
|
//| max(stops level, current spread) - checking against a literal 0 |
|
|
|
|
|
//| would let SL/TP sit right on top of the market and be rejected |
|
|
|
|
|
//| server-side with "Invalid stops". |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
double TCStopsLevel(const string symbol)
|
|
|
|
|
{
|
|
|
|
|
double point = TCPoint(symbol);
|
|
|
|
|
double stops = (double)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL) * point;
|
|
|
|
|
double spread = (double)SymbolInfoInteger(symbol, SYMBOL_SPREAD) * point;
|
|
|
|
|
return MathMax(stops, spread);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double TCFreezeLevel(const string symbol)
|
|
|
|
|
{
|
|
|
|
|
return((double)SymbolInfoInteger(symbol, SYMBOL_TRADE_FREEZE_LEVEL) * TCPoint(symbol));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| The distance a NEW stop/limit price must respect. Brokers set the |
|
|
|
|
|
//| two levels independently and either can be the larger, so a price |
|
|
|
|
|
//| is only safe once it clears BOTH - see the same reasoning in |
|
|
|
|
|
//| Trailing\TrailingATR.mqh::AdjustStopLoss(). |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
double TCMinStopDistance(const string symbol)
|
|
|
|
|
{
|
|
|
|
|
return MathMax(TCStopsLevel(symbol), TCFreezeLevel(symbol));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #14 - passing invalid parameters to functions. |
|
|
|
|
|
//| Every other check here reads symbol properties; if the symbol is |
|
|
|
|
|
//| not selected/known, those reads return zeros and each downstream |
|
|
|
|
|
//| rule silently degenerates into "always passes". Verify the symbol |
|
|
|
|
|
//| is real, selected, and currently open for the trade mode we need |
|
|
|
|
|
//| before trusting anything else. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCSymbolIsTradeable(const string symbol, string &description)
|
|
|
|
|
{
|
|
|
|
|
if(symbol == "")
|
|
|
|
|
{
|
|
|
|
|
description = "empty symbol name";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if(!SymbolInfoInteger(symbol, SYMBOL_SELECT))
|
|
|
|
|
{
|
|
|
|
|
//--- a symbol absent from Market Watch has no live quote and no synced properties
|
|
|
|
|
if(!SymbolSelect(symbol, true))
|
|
|
|
|
{
|
|
|
|
|
description = "symbol " + symbol + " could not be selected in Market Watch";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ENUM_SYMBOL_TRADE_MODE mode = (ENUM_SYMBOL_TRADE_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE);
|
|
|
|
|
if(mode == SYMBOL_TRADE_MODE_DISABLED)
|
|
|
|
|
{
|
|
|
|
|
description = "trading is disabled for " + symbol;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if(mode == SYMBOL_TRADE_MODE_CLOSEONLY)
|
|
|
|
|
{
|
|
|
|
|
description = "symbol " + symbol + " is close-only right now";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
|
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
|
|
|
if(bid <= 0.0 || ask <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "no live quote for " + symbol + " yet (bid/ask are 0)";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #3 - invalid volumes in trade operations. |
|
|
|
|
|
//| Report-only form: states exactly why a volume is illegal without |
|
|
|
|
|
//| changing it, mirroring the article's CheckVolumeValue(). |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCCheckVolumeValue(const string symbol, const double volume, string &description)
|
|
|
|
|
{
|
|
|
|
|
if(!MathIsValidNumber(volume) || volume <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("volume %.8f is not a valid positive number", volume);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
double min_volume = TCVolumeMin(symbol);
|
|
|
|
|
if(volume < min_volume)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("volume %.8f is below the minimum SYMBOL_VOLUME_MIN=%.8f", volume, min_volume);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
double max_volume = TCVolumeMax(symbol);
|
|
|
|
|
if(max_volume > 0.0 && volume > max_volume)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("volume %.8f exceeds the maximum SYMBOL_VOLUME_MAX=%.8f", volume, max_volume);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
double step = TCVolumeStep(symbol);
|
|
|
|
|
if(step > 0.0)
|
|
|
|
|
{
|
|
|
|
|
//--- "volume must be a multiple of SYMBOL_VOLUME_STEP", compared with a tolerance so
|
|
|
|
|
//--- binary representation error in e.g. 0.07/0.01 is not mistaken for an off-step volume
|
|
|
|
|
double steps = MathRound(volume / step);
|
|
|
|
|
double residual = MathAbs(volume - steps * step);
|
|
|
|
|
if(residual > step * TC_VOLUME_EPSILON_FRAC)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("volume %.8f is not a multiple of SYMBOL_VOLUME_STEP=%.8f", volume, step);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #3, corrective form. Snaps `volume` onto the volume grid |
|
|
|
|
|
//| and into [min,max], then re-verifies the result with |
|
|
|
|
|
//| TCCheckVolumeValue() so a correction can never itself emit an |
|
|
|
|
|
//| illegal volume. |
|
|
|
|
|
//| |
|
|
|
|
|
//| Order matters: the step snap happens FIRST and the min/max clamp |
|
|
|
|
|
//| SECOND. Snapping after clamping can push the value straight back |
|
|
|
|
|
//| out of range whenever max is not itself an exact multiple of the |
|
|
|
|
|
//| step (a rounded-up snap of a max-clamped volume exceeds max), and |
|
|
|
|
|
//| both min and max are themselves guaranteed-legal volumes, so |
|
|
|
|
|
//| clamping last always lands on a legal value. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCNormalizeVolume(const string symbol, double &volume, string &description)
|
|
|
|
|
{
|
|
|
|
|
if(!MathIsValidNumber(volume))
|
|
|
|
|
{
|
|
|
|
|
description = "volume is not a valid number";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
double min_volume = TCVolumeMin(symbol);
|
|
|
|
|
double max_volume = TCVolumeMax(symbol);
|
|
|
|
|
double step = TCVolumeStep(symbol);
|
|
|
|
|
if(min_volume <= 0.0 || step <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "broker volume properties for " + symbol + " are not available yet (min/step are 0)";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
//--- Snap DOWN to the grid: rounding up could exceed the free margin already verified upstream.
|
|
|
|
|
//--- The epsilon is not cosmetic - 0.03/0.01 evaluates to 2.9999999999999996 in binary floating
|
|
|
|
|
//--- point, so a bare MathFloor() would silently drop an already-legal volume a whole step.
|
|
|
|
|
double snapped = MathFloor(volume / step + TC_VOLUME_EPSILON_FRAC) * step;
|
|
|
|
|
if(snapped < min_volume)
|
|
|
|
|
snapped = min_volume;
|
|
|
|
|
if(max_volume > 0.0 && snapped > max_volume)
|
|
|
|
|
snapped = max_volume;
|
|
|
|
|
//--- kill the residue MathFloor leaves behind (0.1*3 = 0.30000000000000004) before it reaches OrderSend
|
|
|
|
|
int volume_digits = (int)MathMax(0.0, MathCeil(-MathLog10(step)));
|
|
|
|
|
snapped = NormalizeDouble(snapped, volume_digits);
|
|
|
|
|
if(!TCCheckVolumeValue(symbol, snapped, description))
|
|
|
|
|
return false;
|
|
|
|
|
volume = snapped;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #2 - insufficient funds to perform a trade operation. |
|
|
|
|
|
//| Report-only form: the article's CheckMoneyForTrade() verbatim - |
|
|
|
|
|
//| compute the required margin with OrderCalcMargin() and compare it |
|
|
|
|
|
//| against ACCOUNT_MARGIN_FREE before ever calling OrderSend(). |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCCheckMoneyForTrade(const string symbol, const double lots, const ENUM_ORDER_TYPE type, string &description)
|
|
|
|
|
{
|
|
|
|
|
double price = SymbolInfoDouble(symbol, (type == ORDER_TYPE_BUY || type == ORDER_TYPE_BUY_LIMIT ||
|
|
|
|
|
type == ORDER_TYPE_BUY_STOP) ? SYMBOL_ASK : SYMBOL_BID);
|
|
|
|
|
if(price <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "no live quote for " + symbol + ", cannot evaluate margin";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
double margin = 0.0;
|
|
|
|
|
if(!OrderCalcMargin(type, symbol, lots, price, margin))
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("OrderCalcMargin() failed for %s %.8f lots, error %d", symbol, lots, GetLastError());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
|
|
|
|
if(margin > free_margin)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("not enough money: %.8f lots of %s needs %.2f margin, only %.2f free",
|
|
|
|
|
lots, symbol, margin, free_margin);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #2, corrective form. Steps `lots` down one SYMBOL_VOLUME_ |
|
|
|
|
|
//| STEP at a time until the required margin fits inside the free |
|
|
|
|
|
//| margin, or reports failure once the minimum volume still does not |
|
|
|
|
|
//| fit. Decrementing by the symbol's own step (rather than a |
|
|
|
|
|
//| hard-coded 0.01) is what keeps every intermediate value ON the |
|
|
|
|
|
//| volume grid - a fixed 0.01 decrement produces off-step volumes on |
|
|
|
|
|
//| any symbol whose step is 0.1 or 1.0, and loops ~100x too many |
|
|
|
|
|
//| times on the way down. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCFitVolumeToFreeMargin(const string symbol, double &lots, const ENUM_ORDER_TYPE type, string &description)
|
|
|
|
|
{
|
|
|
|
|
if(!TCNormalizeVolume(symbol, lots, description))
|
|
|
|
|
return false;
|
|
|
|
|
double step = TCVolumeStep(symbol);
|
|
|
|
|
double min_volume = TCVolumeMin(symbol);
|
|
|
|
|
//--- hard bound on the walk: it can never need more iterations than there are steps
|
|
|
|
|
//--- between the requested volume and the minimum, +1 for the final min test
|
|
|
|
|
int max_iterations = (int)MathCeil(TCSafeDivide(lots - min_volume, step, 0.0)) + 1;
|
|
|
|
|
for(int i = 0; i <= max_iterations; i++)
|
|
|
|
|
{
|
|
|
|
|
if(TCCheckMoneyForTrade(symbol, lots, type, description))
|
|
|
|
|
return true;
|
|
|
|
|
if(lots <= min_volume)
|
|
|
|
|
break;
|
|
|
|
|
lots -= step;
|
|
|
|
|
if(lots < min_volume)
|
|
|
|
|
lots = min_volume;
|
|
|
|
|
if(!TCNormalizeVolume(symbol, lots, description))
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
description = StringFormat("free margin cannot cover even the minimum %.8f lots of %s (%s)",
|
|
|
|
|
min_volume, symbol, description);
|
|
|
|
|
lots = 0.0;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #4 - limiting the number of pending orders. |
|
|
|
|
|
//| ACCOUNT_LIMIT_ORDERS is the account's cap on simultaneously |
|
|
|
|
|
//| placed pending orders; 0 means "no limitation". |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCIsNewOrderAllowed(string &description)
|
|
|
|
|
{
|
|
|
|
|
int max_allowed = (int)AccountInfoInteger(ACCOUNT_LIMIT_ORDERS);
|
|
|
|
|
if(max_allowed == 0)
|
|
|
|
|
{
|
|
|
|
|
description = "";
|
|
|
|
|
return true; // no limitation on this account
|
|
|
|
|
}
|
|
|
|
|
int orders = OrdersTotal();
|
|
|
|
|
if(orders < max_allowed)
|
|
|
|
|
{
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
description = StringFormat("account pending-order limit reached: %d of %d ACCOUNT_LIMIT_ORDERS in place",
|
|
|
|
|
orders, max_allowed);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #5 - limiting the number of lots by a specific symbol. |
|
|
|
|
|
//| SYMBOL_VOLUME_LIMIT caps the AGGREGATE volume of open positions |
|
|
|
|
|
//| plus pending orders on one symbol in one direction. Returns the |
|
|
|
|
|
//| volume still available for `type`'s direction, or -1.0 when the |
|
|
|
|
|
//| broker imposes no limit at all. |
|
|
|
|
|
//| |
|
|
|
|
|
//| Buy-side and sell-side volume are counted separately, per the |
|
|
|
|
|
//| SYMBOL_VOLUME_LIMIT documentation ("...in one direction (buy or |
|
|
|
|
|
//| sell)"): a full short book must not eat into the buy-side |
|
|
|
|
|
//| allowance. This deliberately counts EVERY position and order on |
|
|
|
|
|
//| the symbol, not only this EA's magic number - the broker's limit |
|
|
|
|
|
//| applies to the account as a whole, so manual trades and other |
|
|
|
|
|
//| experts on the same symbol consume the same allowance. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
double TCSymbolVolumeAllowed(const string symbol, const ENUM_ORDER_TYPE type)
|
|
|
|
|
{
|
|
|
|
|
double limit = SymbolInfoDouble(symbol, SYMBOL_VOLUME_LIMIT);
|
|
|
|
|
if(limit <= 0.0)
|
|
|
|
|
return(-1.0); // no limitation for this symbol
|
|
|
|
|
|
|
|
|
|
bool want_buy_side = (type == ORDER_TYPE_BUY || type == ORDER_TYPE_BUY_LIMIT || type == ORDER_TYPE_BUY_STOP);
|
|
|
|
|
double used = 0.0;
|
|
|
|
|
//--- open positions on this symbol, same side
|
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
|
|
|
{
|
|
|
|
|
ulong ticket = PositionGetTicket(i);
|
|
|
|
|
if(ticket == 0 || !PositionSelectByTicket(ticket))
|
|
|
|
|
continue;
|
|
|
|
|
if(PositionGetString(POSITION_SYMBOL) != symbol)
|
|
|
|
|
continue;
|
|
|
|
|
bool position_is_buy = ((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
|
|
|
|
|
if(position_is_buy == want_buy_side)
|
|
|
|
|
used += PositionGetDouble(POSITION_VOLUME);
|
|
|
|
|
}
|
|
|
|
|
//--- pending orders on this symbol, same side
|
|
|
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
|
|
|
{
|
|
|
|
|
ulong ticket = OrderGetTicket(i);
|
|
|
|
|
if(ticket == 0)
|
|
|
|
|
continue;
|
|
|
|
|
if(OrderGetString(ORDER_SYMBOL) != symbol)
|
|
|
|
|
continue;
|
|
|
|
|
ENUM_ORDER_TYPE order_type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
|
|
|
|
|
bool order_is_buy = (order_type == ORDER_TYPE_BUY || order_type == ORDER_TYPE_BUY_LIMIT ||
|
|
|
|
|
order_type == ORDER_TYPE_BUY_STOP);
|
|
|
|
|
if(order_is_buy == want_buy_side)
|
|
|
|
|
used += OrderGetDouble(ORDER_VOLUME_CURRENT);
|
|
|
|
|
}
|
|
|
|
|
double available = limit - used;
|
|
|
|
|
return (available > 0.0) ? available : 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #5, corrective form. Trims `lots` down to whatever |
|
|
|
|
|
//| SYMBOL_VOLUME_LIMIT still allows in this direction, then |
|
|
|
|
|
//| re-normalizes onto the volume grid. Fails (lots = 0) when the |
|
|
|
|
|
//| remaining allowance cannot even cover SYMBOL_VOLUME_MIN. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCApplySymbolVolumeLimit(const string symbol, double &lots, const ENUM_ORDER_TYPE type, string &description)
|
|
|
|
|
{
|
|
|
|
|
double available = TCSymbolVolumeAllowed(symbol, type);
|
|
|
|
|
if(available < 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "";
|
|
|
|
|
return true; // no SYMBOL_VOLUME_LIMIT on this symbol
|
|
|
|
|
}
|
|
|
|
|
if(available < TCVolumeMin(symbol))
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("SYMBOL_VOLUME_LIMIT for %s is exhausted in this direction (%.8f lots left, "
|
|
|
|
|
"minimum is %.8f)", symbol, available, TCVolumeMin(symbol));
|
|
|
|
|
lots = 0.0;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if(lots > available)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("volume trimmed from %.8f to %.8f by SYMBOL_VOLUME_LIMIT on %s", lots, available, symbol);
|
|
|
|
|
lots = available;
|
|
|
|
|
string normalize_error;
|
|
|
|
|
if(!TCNormalizeVolume(symbol, lots, normalize_error))
|
|
|
|
|
{
|
|
|
|
|
description = normalize_error;
|
|
|
|
|
lots = 0.0;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #2/#3/#4/#5 combined gate. Everything a volume must clear |
|
|
|
|
|
//| before OrderSend(): a tradeable symbol, a legal volume on the |
|
|
|
|
|
//| grid, the per-symbol aggregate limit, and enough free margin. Call |
|
|
|
|
|
//| sites that size a lot only need this one function. |
|
|
|
|
|
//| `lots` is corrected in place and set to 0.0 on rejection. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCValidateVolumeForTrade(const string symbol, double &lots, const ENUM_ORDER_TYPE type, string &description)
|
|
|
|
|
{
|
|
|
|
|
if(!TCSymbolIsTradeable(symbol, description))
|
|
|
|
|
{
|
|
|
|
|
lots = 0.0;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if(!TCNormalizeVolume(symbol, lots, description))
|
|
|
|
|
{
|
|
|
|
|
lots = 0.0;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
double requested = lots;
|
|
|
|
|
if(!TCApplySymbolVolumeLimit(symbol, lots, type, description))
|
|
|
|
|
return false;
|
|
|
|
|
if(!TCFitVolumeToFreeMargin(symbol, lots, type, description))
|
|
|
|
|
return false;
|
|
|
|
|
//--- On success `description` is cleared by whichever check ran last, so any note left by an
|
|
|
|
|
//--- EARLIER corrective step (a SYMBOL_VOLUME_LIMIT trim, say) would be lost before the caller
|
|
|
|
|
//--- ever saw it. Report the net correction instead - that is the part a caller wants logged.
|
|
|
|
|
if(lots != requested)
|
|
|
|
|
description = StringFormat("volume corrected from %.8f to %.8f for %s %s",
|
|
|
|
|
requested, lots, symbol, EnumToString(type));
|
|
|
|
|
else
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #6 - TakeProfit and StopLoss within SYMBOL_TRADE_STOPS_ |
|
|
|
|
|
//| LEVEL. The article's rule is that both levels are measured against |
|
|
|
|
|
//| the price of the OPPOSITE operation - a long is closed at Bid, a |
|
|
|
|
|
//| short at Ask - and each must sit at least stops-level points away |
|
|
|
|
|
//| on the correct side. For pending orders the reference is the |
|
|
|
|
|
//| order's own activation price instead of the market price. |
|
|
|
|
|
//| |
|
|
|
|
|
//| An SL or TP of 0.0 means "not set" and is skipped, not rejected. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCCheckStops(const string symbol, const ENUM_ORDER_TYPE type, const double order_price,
|
|
|
|
|
const double sl, const double tp, string &description)
|
|
|
|
|
{
|
2026-07-27 11:51:45 -04:00
|
|
|
if(!MathIsValidNumber(order_price) || order_price < 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "order price is not a finite non-negative number";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if(sl != 0.0 && (!MathIsValidNumber(sl) || sl < 0.0))
|
|
|
|
|
{
|
|
|
|
|
description = "stop loss is not a finite non-negative number";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if(tp != 0.0 && (!MathIsValidNumber(tp) || tp < 0.0))
|
|
|
|
|
{
|
|
|
|
|
description = "take profit is not a finite non-negative number";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-07-26 23:08:32 -04:00
|
|
|
double stops = TCStopsLevel(symbol);
|
|
|
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
|
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
|
|
|
if(bid <= 0.0 || ask <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "no live quote for " + symbol + ", cannot validate stops";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool is_buy_side = (type == ORDER_TYPE_BUY || type == ORDER_TYPE_BUY_LIMIT || type == ORDER_TYPE_BUY_STOP);
|
|
|
|
|
bool is_pending = (type != ORDER_TYPE_BUY && type != ORDER_TYPE_SELL);
|
|
|
|
|
//--- market orders: measure from the price the position would be CLOSED at (the opposite side).
|
|
|
|
|
//--- pending orders: measure from the order's own activation price.
|
|
|
|
|
double reference = is_pending ? order_price : (is_buy_side ? bid : ask);
|
|
|
|
|
if(is_pending && reference <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "pending order price is 0, cannot validate stops";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool tp_too_close = (tp != 0.0) && (is_buy_side ? (tp - reference < stops) : (reference - tp < stops));
|
|
|
|
|
bool sl_too_close = (sl != 0.0) && (is_buy_side ? (reference - sl < stops) : (sl - reference < stops));
|
|
|
|
|
if(tp_too_close)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("take profit %s is closer than the %s stops level to the %s reference price %s",
|
|
|
|
|
TCPrice(symbol, tp), TCPrice(symbol, stops),
|
|
|
|
|
EnumToString(type), TCPrice(symbol, reference));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if(sl_too_close)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("stop loss %s is closer than the %s stops level to the %s reference price %s",
|
|
|
|
|
TCPrice(symbol, sl), TCPrice(symbol, stops),
|
|
|
|
|
EnumToString(type), TCPrice(symbol, reference));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #6, corrective form. Pushes an SL/TP that violates the |
|
|
|
|
|
//| stops level out to exactly the minimum legal distance, keeping it |
|
|
|
|
|
//| on the correct side, and normalizes to the symbol's digits. |
|
|
|
|
|
//| Returns false only when the levels cannot be made legal at all |
|
|
|
|
|
//| (no quote, or a pending order with no price). |
|
|
|
|
|
//| |
|
|
|
|
|
//| Deliberately WIDENS rather than rejects: a stop that is merely too |
|
|
|
|
|
//| tight for the broker is still a valid trade idea, and silently |
|
|
|
|
|
//| dropping the setup would make the EA appear to ignore its own |
|
|
|
|
|
//| signals on wide-spread symbols. Callers that need the original |
|
|
|
|
|
//| risk to be honoured exactly should re-check reward:risk after |
|
|
|
|
|
//| calling this - CExpertSignalCustom::OpenParams() does. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCAdjustStops(const string symbol, const ENUM_ORDER_TYPE type, const double order_price,
|
|
|
|
|
double &sl, double &tp, string &description)
|
|
|
|
|
{
|
2026-07-27 11:51:45 -04:00
|
|
|
if(!MathIsValidNumber(order_price) || order_price < 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "order price is not a finite non-negative number";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if(sl != 0.0 && (!MathIsValidNumber(sl) || sl < 0.0))
|
|
|
|
|
{
|
|
|
|
|
description = "stop loss is not a finite non-negative number";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if(tp != 0.0 && (!MathIsValidNumber(tp) || tp < 0.0))
|
|
|
|
|
{
|
|
|
|
|
description = "take profit is not a finite non-negative number";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-07-26 23:08:32 -04:00
|
|
|
double stops = TCMinStopDistance(symbol);
|
|
|
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
|
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
|
|
|
int digits = TCDigits(symbol);
|
|
|
|
|
if(bid <= 0.0 || ask <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "no live quote for " + symbol + ", cannot adjust stops";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
bool is_buy_side = (type == ORDER_TYPE_BUY || type == ORDER_TYPE_BUY_LIMIT || type == ORDER_TYPE_BUY_STOP);
|
|
|
|
|
bool is_pending = (type != ORDER_TYPE_BUY && type != ORDER_TYPE_SELL);
|
|
|
|
|
double reference = is_pending ? order_price : (is_buy_side ? bid : ask);
|
|
|
|
|
if(reference <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "reference price is 0, cannot adjust stops";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
double original_sl = sl;
|
|
|
|
|
double original_tp = tp;
|
|
|
|
|
if(is_buy_side)
|
|
|
|
|
{
|
|
|
|
|
if(tp != 0.0 && tp - reference < stops)
|
|
|
|
|
tp = NormalizeDouble(reference + stops, digits);
|
|
|
|
|
if(sl != 0.0 && reference - sl < stops)
|
|
|
|
|
sl = NormalizeDouble(reference - stops, digits);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if(tp != 0.0 && reference - tp < stops)
|
|
|
|
|
tp = NormalizeDouble(reference - stops, digits);
|
|
|
|
|
if(sl != 0.0 && sl - reference < stops)
|
|
|
|
|
sl = NormalizeDouble(reference + stops, digits);
|
|
|
|
|
}
|
|
|
|
|
if(sl != original_sl || tp != original_tp)
|
|
|
|
|
description = StringFormat("stops widened to the %s broker minimum (%s): sl %s -> %s, tp %s -> %s",
|
|
|
|
|
symbol, TCPrice(symbol, stops),
|
|
|
|
|
DoubleToString(original_sl, digits), DoubleToString(sl, digits),
|
|
|
|
|
DoubleToString(original_tp, digits), DoubleToString(tp, digits));
|
|
|
|
|
else
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #6, pending-order price. A limit/stop order's own |
|
|
|
|
|
//| activation price must also sit at least stops-level points away |
|
|
|
|
|
//| from the current market, on the side its type implies. This is the |
|
|
|
|
|
//| check CExpertTrade::Buy()/Sell() applies when it decides between a |
|
|
|
|
|
//| market fill and a pending order; exposing it here lets a caller |
|
|
|
|
|
//| know IN ADVANCE which of the two it is about to get. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCCheckPendingPrice(const string symbol, const ENUM_ORDER_TYPE type, const double price, string &description)
|
|
|
|
|
{
|
|
|
|
|
double stops = TCStopsLevel(symbol);
|
|
|
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
|
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
|
|
|
if(bid <= 0.0 || ask <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "no live quote for " + symbol;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
bool ok = true;
|
|
|
|
|
switch(type)
|
|
|
|
|
{
|
|
|
|
|
case ORDER_TYPE_BUY_LIMIT:
|
|
|
|
|
ok = (ask - price >= stops);
|
|
|
|
|
break;
|
|
|
|
|
case ORDER_TYPE_BUY_STOP:
|
|
|
|
|
ok = (price - ask >= stops);
|
|
|
|
|
break;
|
|
|
|
|
case ORDER_TYPE_SELL_LIMIT:
|
|
|
|
|
ok = (price - bid >= stops);
|
|
|
|
|
break;
|
|
|
|
|
case ORDER_TYPE_SELL_STOP:
|
|
|
|
|
ok = (bid - price >= stops);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
description = "";
|
|
|
|
|
return true; // market orders have no activation price to validate
|
|
|
|
|
}
|
|
|
|
|
if(!ok)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("pending price %s for %s is inside the %s stops level (bid %s / ask %s)",
|
|
|
|
|
TCPrice(symbol, price), EnumToString(type), TCPrice(symbol, stops),
|
|
|
|
|
TCPrice(symbol, bid), TCPrice(symbol, ask));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #7 - attempt to modify/close a POSITION inside |
|
|
|
|
|
//| SYMBOL_TRADE_FREEZE_LEVEL. While the market is within freeze-level |
|
|
|
|
|
//| points of a position's SL or TP, the server refuses to modify or |
|
|
|
|
|
//| close it, so the request must not be sent at all. |
|
|
|
|
|
//| |
|
|
|
|
|
//| Buy position: TakeProfit - Bid >= freeze AND Bid - StopLoss >= freeze
|
|
|
|
|
//| Sell position: Ask - TakeProfit >= freeze AND StopLoss - Ask >= freeze
|
|
|
|
|
//| |
|
|
|
|
|
//| A 0.0 SL or TP is not set and imposes no freeze restriction. A |
|
|
|
|
|
//| 0 freeze level (the common case) makes this a no-op that always |
|
|
|
|
|
//| passes. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCFreezeOkForPosition(const string symbol, const ENUM_POSITION_TYPE position_type,
|
|
|
|
|
const double sl, const double tp, string &description)
|
|
|
|
|
{
|
|
|
|
|
double freeze = TCFreezeLevel(symbol);
|
|
|
|
|
if(freeze <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
|
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
|
|
|
if(bid <= 0.0 || ask <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "no live quote for " + symbol + ", cannot evaluate the freeze level";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
bool is_long = (position_type == POSITION_TYPE_BUY);
|
|
|
|
|
double reference = is_long ? bid : ask;
|
|
|
|
|
string side = is_long ? "long" : "short";
|
|
|
|
|
string ref_name = is_long ? "bid" : "ask";
|
|
|
|
|
if(tp != 0.0 && (is_long ? (tp - reference) : (reference - tp)) < freeze)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("%s position on %s is frozen: take profit %s is within %s of %s %s",
|
|
|
|
|
side, symbol, TCPrice(symbol, tp), TCPrice(symbol, freeze),
|
|
|
|
|
ref_name, TCPrice(symbol, reference));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if(sl != 0.0 && (is_long ? (reference - sl) : (sl - reference)) < freeze)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("%s position on %s is frozen: stop loss %s is within %s of %s %s",
|
|
|
|
|
side, symbol, TCPrice(symbol, sl), TCPrice(symbol, freeze),
|
|
|
|
|
ref_name, TCPrice(symbol, reference));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #7 - attempt to modify/delete a PENDING ORDER inside |
|
|
|
|
|
//| SYMBOL_TRADE_FREEZE_LEVEL. |
|
|
|
|
|
//| |
|
|
|
|
|
//| BuyLimit: Ask - OpenPrice >= freeze |
|
|
|
|
|
//| BuyStop: OpenPrice - Ask >= freeze |
|
|
|
|
|
//| SellLimit: OpenPrice - Bid >= freeze |
|
|
|
|
|
//| SellStop: Bid - OpenPrice >= freeze |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCFreezeOkForOrder(const string symbol, const ENUM_ORDER_TYPE order_type,
|
|
|
|
|
const double open_price, string &description)
|
|
|
|
|
{
|
|
|
|
|
double freeze = TCFreezeLevel(symbol);
|
|
|
|
|
if(freeze <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
|
|
|
|
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
|
|
|
|
if(bid <= 0.0 || ask <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
description = "no live quote for " + symbol + ", cannot evaluate the freeze level";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
bool ok = true;
|
|
|
|
|
switch(order_type)
|
|
|
|
|
{
|
|
|
|
|
case ORDER_TYPE_BUY_LIMIT:
|
|
|
|
|
ok = (ask - open_price >= freeze);
|
|
|
|
|
break;
|
|
|
|
|
case ORDER_TYPE_BUY_STOP:
|
|
|
|
|
ok = (open_price - ask >= freeze);
|
|
|
|
|
break;
|
|
|
|
|
case ORDER_TYPE_SELL_LIMIT:
|
|
|
|
|
ok = (open_price - bid >= freeze);
|
|
|
|
|
break;
|
|
|
|
|
case ORDER_TYPE_SELL_STOP:
|
|
|
|
|
ok = (bid - open_price >= freeze);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
description = "";
|
|
|
|
|
return true; // a filled market order is not a pending order
|
|
|
|
|
}
|
|
|
|
|
if(!ok)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("%s on %s at %s is inside the %s freeze level (bid %s / ask %s)",
|
|
|
|
|
EnumToString(order_type), symbol, TCPrice(symbol, open_price),
|
|
|
|
|
TCPrice(symbol, freeze), TCPrice(symbol, bid), TCPrice(symbol, ask));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #11 - sending modification requests without actual changes.|
|
|
|
|
|
//| "A trade request which does not make any changes is considered an |
|
|
|
|
|
//| error" (TRADE_RETCODE_NO_CHANGES=10025). Both helpers return true |
|
|
|
|
|
//| only when at least one parameter really differs, using one point |
|
|
|
|
|
//| as the comparison tolerance exactly as the article does. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCPositionModifyIsMeaningful(const string symbol, const double current_sl, const double new_sl,
|
|
|
|
|
const double current_tp, const double new_tp)
|
|
|
|
|
{
|
|
|
|
|
double point = TCPoint(symbol);
|
|
|
|
|
if(MathAbs(current_sl - new_sl) > point)
|
|
|
|
|
return true;
|
|
|
|
|
if(MathAbs(current_tp - new_tp) > point)
|
|
|
|
|
return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TCOrderModifyIsMeaningful(const string symbol, const double current_price, const double new_price,
|
|
|
|
|
const double current_sl, const double new_sl,
|
|
|
|
|
const double current_tp, const double new_tp)
|
|
|
|
|
{
|
|
|
|
|
double point = TCPoint(symbol);
|
|
|
|
|
if(MathAbs(current_price - new_price) > point)
|
|
|
|
|
return true;
|
|
|
|
|
if(MathAbs(current_sl - new_sl) > point)
|
|
|
|
|
return true;
|
|
|
|
|
if(MathAbs(current_tp - new_tp) > point)
|
|
|
|
|
return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #8 - errors caused by insufficient quote history. |
|
|
|
|
|
//| The article's "correct" approach: check that the required depth is |
|
|
|
|
|
//| actually there, and REQUEST the missing data instead of assuming |
|
|
|
|
|
//| it is already loaded. In MQL5 the request is implicit - the first |
|
|
|
|
|
//| Bars()/CopyRates() call on an unsynchronised series starts the |
|
|
|
|
|
//| download and returns short - so this returns false for that tick |
|
|
|
|
|
//| and the caller simply skips it; the next tick finds the series |
|
|
|
|
|
//| built. Always true inside the Strategy Tester, where history is |
|
|
|
|
|
//| synchronous by construction. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool TCHasEnoughHistory(const string symbol, const ENUM_TIMEFRAMES timeframe,
|
|
|
|
|
const int required_bars, string &description)
|
|
|
|
|
{
|
|
|
|
|
if(required_bars <= 0)
|
|
|
|
|
{
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if(MQLInfoInteger(MQL_TESTER))
|
|
|
|
|
{
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
//--- SERIES_SYNCHRONIZED is the terminal's own "this series is fully built" flag; a series that
|
|
|
|
|
//--- reports enough bars while still syncing can still hand back gaps to iHighest/CopyBuffer
|
|
|
|
|
if(!SeriesInfoInteger(symbol, timeframe, SERIES_SYNCHRONIZED))
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("%s %s series is still synchronising", symbol, EnumToString(timeframe));
|
|
|
|
|
//--- touching the series is what asks the terminal to build it
|
|
|
|
|
datetime probe[];
|
|
|
|
|
CopyTime(symbol, timeframe, 0, 1, probe);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
int bars = Bars(symbol, timeframe);
|
|
|
|
|
if(bars < required_bars)
|
|
|
|
|
{
|
|
|
|
|
description = StringFormat("only %d bars of %s %s history available, %d required",
|
|
|
|
|
bars, symbol, EnumToString(timeframe), required_bars);
|
|
|
|
|
datetime probe[];
|
|
|
|
|
CopyTime(symbol, timeframe, 0, required_bars, probe);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
description = "";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Article #16 - consumption of CPU resources and memory. |
|
|
|
|
|
//| TCWarnIfSlow() is the article's GetMicrosecondCount() measurement |
|
|
|
|
|
//| turned into a guard rail: pass the timestamp taken before the |
|
|
|
|
|
//| section and a budget, and it reports (throttled) whenever the |
|
|
|
|
|
//| section overruns. The article's own yardstick is that a first |
|
|
|
|
|
//| calculation over 10+ years of M1 data should stay under 100 ms. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
ulong TCNow(void)
|
|
|
|
|
{
|
|
|
|
|
return GetMicrosecondCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TCWarnIfSlow(const string label, const ulong started_us, const ulong budget_us)
|
|
|
|
|
{
|
|
|
|
|
ulong elapsed = GetMicrosecondCount() - started_us;
|
|
|
|
|
if(elapsed <= budget_us)
|
|
|
|
|
return true;
|
|
|
|
|
TCLog("slow:" + label,
|
|
|
|
|
StringFormat("PERFORMANCE: %s took %.1f ms (budget %.1f ms) - see article 2555 #16; "
|
|
|
|
|
"profile it in MetaEditor if this persists",
|
|
|
|
|
label, elapsed / 1000.0, budget_us / 1000.0));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--- MQL_MEMORY_USED is reported in megabytes
|
|
|
|
|
int TCMemoryUsedMb(void)
|
|
|
|
|
{
|
|
|
|
|
return((int)MQLInfoInteger(MQL_MEMORY_USED));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TCWarnIfMemoryAbove(const int limit_mb)
|
|
|
|
|
{
|
|
|
|
|
int used = TCMemoryUsedMb();
|
|
|
|
|
if(used <= limit_mb)
|
|
|
|
|
return true;
|
|
|
|
|
TCLog("memory",
|
|
|
|
|
StringFormat("MEMORY: the EA is holding %d MB (soft limit %d MB) - see article 2555 #16", used, limit_mb));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif // WARRIOR_TRADECHECKS_MQH
|
|
|
|
|
//+------------------------------------------------------------------+
|