//+------------------------------------------------------------------+ //| TradeChecks.mqh | //| AnimateDread | //| "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. | //+------------------------------------------------------------------+ #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. | //| | //| THE WINDOW IS WALL CLOCK, NOT BROKER TIME (fixed 2026-09-07), and | //| the difference is the whole value of this function. It used | //| TimeCurrent(), which in the Strategy Tester is SIMULATED time: a | //| pass over years of history crosses sixty simulated seconds many | //| times a second, so the throttle admitted almost every call and | //| the function did nothing where it was needed most. Measured on the| //| 2026-09-06 tester log: 247,146 lines - 15.7% of 1,570,535 - were | //| two "rejected" messages that this throttle was supposed to be | //| collapsing. GetTickCount64() is real elapsed milliseconds and | //| behaves the same in both worlds: unchanged in live trading, where | //| wall clock and broker time advance together, and genuinely one | //| line per key per minute of RUN TIME in a tester pass. | //| | //| Suppressed calls are COUNTED and reported on the next line that | //| gets through, so throttling never silently hides how often a | //| condition fired - the number is usually the interesting part. | //+------------------------------------------------------------------+ void TCLog(const string key, const string message) { static string s_keys[]; static ulong s_times[]; static long s_suppressed[]; const ulong now = GetTickCount64(); const ulong window = (ulong)TC_LOG_THROTTLE_SECONDS * 1000; int total = ArraySize(s_keys); for(int i = 0; i < total; i++) { if(s_keys[i] != key) continue; //--- Unsigned subtraction, so this stays correct across the 64-bit tick counter's wrap. if(now - s_times[i] < window) { s_suppressed[i]++; return; } s_times[i] = now; if(s_suppressed[i] > 0) { Print(message + StringFormat(" [+%I64d more since the last line]", s_suppressed[i])); s_suppressed[i] = 0; } else Print(message); return; } ArrayResize(s_keys, total + 1); ArrayResize(s_times, total + 1); ArrayResize(s_suppressed, total + 1); s_keys[total] = key; s_times[total] = now; s_suppressed[total] = 0; 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)); } //+------------------------------------------------------------------+ //| Two-sided quote, or false with a description saying what could | //| not be done without one. The single definition of "usable quote" | //| for every check below - a symbol that has not synced yet reads | //| back 0 on both sides, and 0 would pass a distance test that a | //| real price fails. | //+------------------------------------------------------------------+ bool TCLiveQuote(const string symbol, const string purpose, double &bid, double &ask, string &description) { bid = SymbolInfoDouble(symbol, SYMBOL_BID); ask = SymbolInfoDouble(symbol, SYMBOL_ASK); if(bid > 0.0 && ask > 0.0) return true; description = "no live quote for " + symbol + ", cannot " + purpose; return false; } //+------------------------------------------------------------------+ //| 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. | //+------------------------------------------------------------------+ 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)); } //+------------------------------------------------------------------+ //| Which order type an entry price will produce - see | //| CExpertTrade::Buy()/Sell(), whose routing this reproduces. `ask`/ | //| `bid` are taken as parameters rather than re-queried here so a | //| caller's own (possibly RefreshRates()-cached) CSymbolInfo values | //| are what decide the routing, matching what CExpertTrade itself | //| would see. | //+------------------------------------------------------------------+ ENUM_ORDER_TYPE TCResolveOrderType(const string symbol, const bool isLong, const double price, const double ask, const double bid) { if(price <= 0.0 || price == EMPTY_VALUE) return(isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL); double stops = TCStopsLevel(symbol); if(isLong) { if(price > ask + stops) return(ORDER_TYPE_BUY_STOP); if(price < ask - stops) return(ORDER_TYPE_BUY_LIMIT); return(ORDER_TYPE_BUY); } if(price > bid + stops) return(ORDER_TYPE_SELL_LIMIT); if(price < bid - stops) return(ORDER_TYPE_SELL_STOP); return(ORDER_TYPE_SELL); } //+------------------------------------------------------------------+ //| 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 = 0.0, ask = 0.0; if(!TCLiveQuote(symbol, "trade it yet", bid, ask, description)) return false; description = ""; return true; } //+------------------------------------------------------------------+ //| IS TRADING PERMITTED AT ALL - terminal, program, account, server. | //| | //| Five separate switches, any one of which stops an EA dead, and | //| they fail in different places: | //| TERMINAL_CONNECTED no server - every request times out | //| TERMINAL_TRADE_ALLOWED the AlgoTrading button | //| MQL_TRADE_ALLOWED this program's own checkbox | //| ACCOUNT_TRADE_ALLOWED the BROKER disabled trading (an | //| investor password lands here) | //| ACCOUNT_TRADE_EXPERT the broker forbids EAs specifically | //| | //| This repo has already lost a session to the second one: the EA | //| ran for days placing nothing because AlgoTrading was off, and | //| nothing said so. Checked and REPORTED, never assumed. | //+------------------------------------------------------------------+ bool TCTradingPermitted(string &description) { if(!TerminalInfoInteger(TERMINAL_CONNECTED)) { description = "terminal is not connected to a trade server"; return false; } if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { description = "AlgoTrading is disabled in the terminal"; return false; } if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) { description = "trading is disabled for this program"; return false; } if(!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)) { description = "the broker has disabled trading on this account"; return false; } if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) { description = "the broker forbids expert advisors on this account"; return false; } description = ""; return true; } //+------------------------------------------------------------------+ //| THE FILLING MODE THE SYMBOL WILL ACCEPT. | //| | //| SYMBOL_FILLING_MODE is a BIT MASK of what the symbol permits, and | //| a request carrying an unsupported one is rejected outright with | //| "Unsupported filling mode" - a run of zero trades that looks | //| exactly like a strategy which never fired. Neither bit set means | //| only ORDER_FILLING_RETURN is legal, which is the usual case on | //| exchange-execution symbols and the one most often missed. | //+------------------------------------------------------------------+ bool TCPickFilling(const string symbol, ENUM_ORDER_TYPE_FILLING &mode, string &description) { const long mask = SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE); //--- FOK first: it is the strictest and the least surprising. A partial fill the EA does not //--- expect is a position of the wrong size, which every later R calculation then misstates. if((mask & SYMBOL_FILLING_FOK) != 0) mode = ORDER_FILLING_FOK; else if((mask & SYMBOL_FILLING_IOC) != 0) mode = ORDER_FILLING_IOC; else mode = ORDER_FILLING_RETURN; description = ""; return true; } //+------------------------------------------------------------------+ //| DOES THE SYMBOL ALLOW THIS ORDER TYPE, and stops attached to it? | //| | //| SYMBOL_ORDER_MODE is another bit mask. A symbol may permit market | //| orders but not stops, or permit the order but not a stop loss on | //| it - in which case the SL is rejected and the position opens | //| NAKED. That is the dangerous one: the order succeeds and the | //| protection silently does not. | //+------------------------------------------------------------------+ bool TCOrderTypeAllowed(const string symbol, const ENUM_ORDER_TYPE type, const bool withSL, const bool withTP, string &description) { const long mask = SymbolInfoInteger(symbol, SYMBOL_ORDER_MODE); long need = 0; switch(type) { case ORDER_TYPE_BUY: case ORDER_TYPE_SELL: need = SYMBOL_ORDER_MARKET; break; case ORDER_TYPE_BUY_LIMIT: case ORDER_TYPE_SELL_LIMIT: need = SYMBOL_ORDER_LIMIT; break; case ORDER_TYPE_BUY_STOP: case ORDER_TYPE_SELL_STOP: need = SYMBOL_ORDER_STOP; break; case ORDER_TYPE_BUY_STOP_LIMIT: case ORDER_TYPE_SELL_STOP_LIMIT: need = SYMBOL_ORDER_STOP_LIMIT; break; default: description = "unsupported order type for " + symbol; return false; } if((mask & need) == 0) { description = StringFormat("%s does not accept this order type (SYMBOL_ORDER_MODE %d)", symbol, (int)mask); return false; } if(withSL && (mask & SYMBOL_ORDER_SL) == 0) { description = symbol + " rejects a stop loss on the order - it would open unprotected"; return false; } if(withTP && (mask & SYMBOL_ORDER_TP) == 0) { description = symbol + " rejects a take profit on the order"; return false; } description = ""; return true; } //+------------------------------------------------------------------+ //| Expiration modes the symbol accepts. Pending orders only. | //+------------------------------------------------------------------+ bool TCExpirationAllowed(const string symbol, const ENUM_ORDER_TYPE_TIME mode, string &description) { const long mask = SymbolInfoInteger(symbol, SYMBOL_EXPIRATION_MODE); long need = 0; switch(mode) { case ORDER_TIME_GTC: need = SYMBOL_EXPIRATION_GTC; break; case ORDER_TIME_DAY: need = SYMBOL_EXPIRATION_DAY; break; case ORDER_TIME_SPECIFIED: need = SYMBOL_EXPIRATION_SPECIFIED; break; case ORDER_TIME_SPECIFIED_DAY: need = SYMBOL_EXPIRATION_SPECIFIED_DAY; break; default: description = "unknown expiration mode"; return false; } if((mask & need) == 0) { description = StringFormat("%s rejects this expiration mode (SYMBOL_EXPIRATION_MODE %d)", symbol, (int)mask); return false; } description = ""; return true; } //+------------------------------------------------------------------+ //| Hedging account? Netting changes what a second order DOES - it | //| nets against the open position instead of adding one, so "sell | //| while long" is a CLOSE on netting and a hedge on hedging. Logic | //| that assumes one is wrong on the other. | //+------------------------------------------------------------------+ bool TCIsHedging(void) { return ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); } //+------------------------------------------------------------------+ //| WHAT TO DO WITH A RETCODE. Three outcomes, and conflating them is | //| how an EA either hammers a dead server or gives up on a requote. | //| >0 retry is sensible - transient (requote, price changed, busy) | //| 0 accept - it worked, or there was nothing to do | //| <0 do NOT retry - the request itself is wrong | //+------------------------------------------------------------------+ int TCRetcodeAdvice(const uint retcode, string &description) { switch(retcode) { case TRADE_RETCODE_DONE: case TRADE_RETCODE_DONE_PARTIAL: case TRADE_RETCODE_PLACED: case TRADE_RETCODE_NO_CHANGES: description = "accepted"; return 0; case TRADE_RETCODE_REQUOTE: case TRADE_RETCODE_PRICE_CHANGED: case TRADE_RETCODE_PRICE_OFF: case TRADE_RETCODE_TIMEOUT: case TRADE_RETCODE_CONNECTION: case TRADE_RETCODE_TOO_MANY_REQUESTS: case TRADE_RETCODE_SERVER_DISABLES_AT: description = "transient - a retry is sensible"; return 1; default: description = StringFormat("permanent rejection (retcode %u) - the request is wrong", retcode); return -1; } } //+------------------------------------------------------------------+ //| IS THE MARKET ACTUALLY OPEN FOR THIS SYMBOL, right now? | //| | //| SYMBOL_TRADE_MODE above catches a symbol that is disabled or | //| close-only, which is a PROPERTY of the symbol. It does not catch | //| the far more common case: the symbol is perfectly tradeable and | //| the session is simply shut. The tester will still hand the EA a | //| tick at 00:01 on a Monday before the session opens, the order | //| goes out, and the server answers "Market closed" - measured on | //| AUDUSD, four rejected entries in one run. | //| | //| A rejected order is not a harmless no-op. The signal that would | //| have been traded is silently discarded, so the backtest is | //| quietly measuring a DIFFERENT strategy from the one being | //| described - one that skips whichever trades happen to land in a | //| session gap, which is not a rule anybody chose. | //| | //| Sessions are per day-of-week and there may be several in a day | //| (an index with a lunch break has two). A session whose end is at | //| or before its start has wrapped past midnight. | //+------------------------------------------------------------------+ bool TCMarketIsOpen(const string symbol, const datetime when, string &description) { MqlDateTime t; TimeToStruct(when, t); const ENUM_DAY_OF_WEEK dow = (ENUM_DAY_OF_WEEK)t.day_of_week; const int now = t.hour * 3600 + t.min * 60 + t.sec; datetime from = 0, to = 0; for(int i = 0; i < 8; i++) // MT5 allows a handful of sessions per day { if(!SymbolInfoSessionTrade(symbol, dow, i, from, to)) break; // no more sessions defined for this day const int s = (int)((long)from % 86400); const int e = (int)((long)to % 86400); const bool inside = (e > s) ? (now >= s && now < e) // ordinary session : (now >= s || now < e); // wraps past midnight if(inside) { description = ""; return true; } } description = StringFormat("%s has no open trading session at %s", symbol, TimeToString(when, TIME_DATE | TIME_MINUTES)); return false; } //+------------------------------------------------------------------+ //| 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. | //+------------------------------------------------------------------+ 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. | //+------------------------------------------------------------------+ 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. | //+------------------------------------------------------------------+ 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. | //+------------------------------------------------------------------+ 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 = 0.0, ask = 0.0; if(!TCLiveQuote(symbol, "validate stops", bid, ask, description)) 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). | //+------------------------------------------------------------------+ 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 = 0.0, ask = 0.0; if(!TCLiveQuote(symbol, "adjust stops", bid, ask, description)) return false; int digits = TCDigits(symbol); 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 = 0.0, ask = 0.0; if(!TCLiveQuote(symbol, "check the stop distance", bid, ask, description)) 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. | //+------------------------------------------------------------------+ 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 = 0.0, ask = 0.0; if(!TCLiveQuote(symbol, "evaluate the freeze level", bid, ask, description)) 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 = 0.0, ask = 0.0; if(!TCLiveQuote(symbol, "evaluate the freeze level", bid, ask, description)) 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. | //+------------------------------------------------------------------+ 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; } //+------------------------------------------------------------------+ //| THE ONE GATE. Every entry runs the whole checklist, in order. | //| | //| WHY THIS EXISTS AS A SINGLE FUNCTION. Every rule below was | //| already implemented in this file and NONE of them were called - //| the entry path reached the broker with nothing between it and the //| server but hope. Coverage that lives in uncalled functions is not //| coverage, it is documentation, and it reads identically in a code //| review. One gate on the one path means a rule cannot be added and //| then quietly not used. //| | //| ORDER MATTERS. Cheap and global first (permissions, connection), | //| then per-symbol, then per-request, then the arithmetic that needs | //| a live quote. The first refusal wins and names itself, so the log | //| says WHICH rule declined rather than that something did. | //| | //| `lots` and the stops are IN/OUT: this normalises rather than | //| merely judging, because a volume off its step and a stop inside | //| the freeze level are both fixable and both fatal if sent as-is. | //+------------------------------------------------------------------+ bool TCCanOpen(const string symbol, const ENUM_ORDER_TYPE type, double &lots, const double price, double &sl, double &tp, string &description) { //--- 1. Is trading possible at all - terminal, program, account, server. if(!TCTradingPermitted(description)) return false; //--- 2. Is the symbol tradeable, selected, and quoting. if(!TCSymbolIsTradeable(symbol, description)) return false; //--- 3. Is the session actually open. A closed market rejects at the server and the firing is //--- lost, so the run silently measures a strategy that skips session gaps. if(!TCMarketIsOpen(symbol, TimeCurrent(), description)) return false; //--- 4. Does the symbol accept this order type, and a stop/target attached to it. Rejecting the //--- SL while accepting the order is the one failure that opens an UNPROTECTED position. if(!TCOrderTypeAllowed(symbol, type, sl != 0.0, tp != 0.0, description)) return false; //--- 5. Volume: legal, on-step, inside the symbol's and the account's limits. if(!TCValidateVolumeForTrade(symbol, lots, type, description)) return false; //--- 6. Is another order allowed - account order/position limits. if(!TCIsNewOrderAllowed(description)) return false; //--- 7. Money. Last of the request checks because it is the most expensive. if(!TCCheckMoneyForTrade(symbol, lots, type, description)) return false; //--- 8. Stops outside the broker's minimum distance and freeze band. Adjusted, not just judged - //--- a stop a few points inside the level is a legal trade with an illegal request. if(!TCAdjustStops(symbol, type, price, sl, tp, description)) return false; description = ""; return true; } #endif // WARRIOR_TRADECHECKS_MQH //+------------------------------------------------------------------+