//+------------------------------------------------------------------+ //| 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) { 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; } 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) { 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; } 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 //+------------------------------------------------------------------+