2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| TradeChecks.mqh |
|
| | | //| AnimateDread |
|
2026-08-22 00:30:14 -04:00 | | | //| "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. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | #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. |
|
 refactor(logs): two verbosity levels, and a throttle that works in the tester
The operator's complaint that "the logs get filled" is measurable, so it was measured. The
2026-09-06 tester log is 1,570,535 lines and FOUR print statements are 86.6% of it:
556,732 35.4% "Starting direction calculation with total filters: N"
556,731 35.5% "Final directional result: N"
247,146 15.7% the two "open rejected" traces
None of the four is a decision. Direction() runs on every tick and recurses into every filter -
about eight calls a tick on this fleet - and emits two lines carrying a filter count and a
number. The rejection traces fire on every tick a side is blocked, which on a one-sided chart
is forever.
They were all on the same switch as everything else, so turning VerboseMode on to diagnose one
thing produced a journal too large to search. Added a second level, TraceMode, off by default,
and moved exactly those four sites to it. PrintVerbose() keeps its meaning and none of its ~40
call sites changed.
TCLog's throttle was near-inert where it mattered most. It measured its window with
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 nearly every call.
That is why 247,146 lines got through a function whose whole purpose is collapsing them. Now
GetTickCount64(), which is real elapsed milliseconds and behaves identically in both worlds -
unchanged in live trading, genuinely one line per key per minute of run time in a tester pass.
Suppressed calls are counted and reported on the next line through, so nothing is hidden.
Compiled clean in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline taken first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:44:31 -04:00 | | | //| |
|
| | | //| 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. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | void TCLog(const string key, const string message)
|
| | | {
|
 refactor(logs): two verbosity levels, and a throttle that works in the tester
The operator's complaint that "the logs get filled" is measurable, so it was measured. The
2026-09-06 tester log is 1,570,535 lines and FOUR print statements are 86.6% of it:
556,732 35.4% "Starting direction calculation with total filters: N"
556,731 35.5% "Final directional result: N"
247,146 15.7% the two "open rejected" traces
None of the four is a decision. Direction() runs on every tick and recurses into every filter -
about eight calls a tick on this fleet - and emits two lines carrying a filter count and a
number. The rejection traces fire on every tick a side is blocked, which on a one-sided chart
is forever.
They were all on the same switch as everything else, so turning VerboseMode on to diagnose one
thing produced a journal too large to search. Added a second level, TraceMode, off by default,
and moved exactly those four sites to it. PrintVerbose() keeps its meaning and none of its ~40
call sites changed.
TCLog's throttle was near-inert where it mattered most. It measured its window with
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 nearly every call.
That is why 247,146 lines got through a function whose whole purpose is collapsing them. Now
GetTickCount64(), which is real elapsed milliseconds and behaves identically in both worlds -
unchanged in live trading, genuinely one line per key per minute of run time in a tester pass.
Suppressed calls are counted and reported on the next line through, so nothing is hidden.
Compiled clean in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline taken first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:44:31 -04:00 | | | 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;
|
2026-07-26 23:08:32 -04:00 | | | int total = ArraySize(s_keys);
|
| | | for(int i = 0; i < total; i++)
|
| | | {
|
| | | if(s_keys[i] != key)
|
| | | continue;
|
 refactor(logs): two verbosity levels, and a throttle that works in the tester
The operator's complaint that "the logs get filled" is measurable, so it was measured. The
2026-09-06 tester log is 1,570,535 lines and FOUR print statements are 86.6% of it:
556,732 35.4% "Starting direction calculation with total filters: N"
556,731 35.5% "Final directional result: N"
247,146 15.7% the two "open rejected" traces
None of the four is a decision. Direction() runs on every tick and recurses into every filter -
about eight calls a tick on this fleet - and emits two lines carrying a filter count and a
number. The rejection traces fire on every tick a side is blocked, which on a one-sided chart
is forever.
They were all on the same switch as everything else, so turning VerboseMode on to diagnose one
thing produced a journal too large to search. Added a second level, TraceMode, off by default,
and moved exactly those four sites to it. PrintVerbose() keeps its meaning and none of its ~40
call sites changed.
TCLog's throttle was near-inert where it mattered most. It measured its window with
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 nearly every call.
That is why 247,146 lines got through a function whose whole purpose is collapsing them. Now
GetTickCount64(), which is real elapsed milliseconds and behaves identically in both worlds -
unchanged in live trading, genuinely one line per key per minute of run time in a tester pass.
Suppressed calls are counted and reported on the next line through, so nothing is hidden.
Compiled clean in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline taken first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:44:31 -04:00 | | | //--- Unsigned subtraction, so this stays correct across the 64-bit tick counter's wrap.
|
| | | if(now - s_times[i] < window)
|
| | | {
|
| | | s_suppressed[i]++;
|
2026-07-26 23:08:32 -04:00 | | | return;
|
 refactor(logs): two verbosity levels, and a throttle that works in the tester
The operator's complaint that "the logs get filled" is measurable, so it was measured. The
2026-09-06 tester log is 1,570,535 lines and FOUR print statements are 86.6% of it:
556,732 35.4% "Starting direction calculation with total filters: N"
556,731 35.5% "Final directional result: N"
247,146 15.7% the two "open rejected" traces
None of the four is a decision. Direction() runs on every tick and recurses into every filter -
about eight calls a tick on this fleet - and emits two lines carrying a filter count and a
number. The rejection traces fire on every tick a side is blocked, which on a one-sided chart
is forever.
They were all on the same switch as everything else, so turning VerboseMode on to diagnose one
thing produced a journal too large to search. Added a second level, TraceMode, off by default,
and moved exactly those four sites to it. PrintVerbose() keeps its meaning and none of its ~40
call sites changed.
TCLog's throttle was near-inert where it mattered most. It measured its window with
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 nearly every call.
That is why 247,146 lines got through a function whose whole purpose is collapsing them. Now
GetTickCount64(), which is real elapsed milliseconds and behaves identically in both worlds -
unchanged in live trading, genuinely one line per key per minute of run time in a tester pass.
Suppressed calls are counted and reported on the next line through, so nothing is hidden.
Compiled clean in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline taken first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:44:31 -04:00 | | | }
|
2026-07-26 23:08:32 -04:00 | | | s_times[i] = now;
|
 refactor(logs): two verbosity levels, and a throttle that works in the tester
The operator's complaint that "the logs get filled" is measurable, so it was measured. The
2026-09-06 tester log is 1,570,535 lines and FOUR print statements are 86.6% of it:
556,732 35.4% "Starting direction calculation with total filters: N"
556,731 35.5% "Final directional result: N"
247,146 15.7% the two "open rejected" traces
None of the four is a decision. Direction() runs on every tick and recurses into every filter -
about eight calls a tick on this fleet - and emits two lines carrying a filter count and a
number. The rejection traces fire on every tick a side is blocked, which on a one-sided chart
is forever.
They were all on the same switch as everything else, so turning VerboseMode on to diagnose one
thing produced a journal too large to search. Added a second level, TraceMode, off by default,
and moved exactly those four sites to it. PrintVerbose() keeps its meaning and none of its ~40
call sites changed.
TCLog's throttle was near-inert where it mattered most. It measured its window with
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 nearly every call.
That is why 247,146 lines got through a function whose whole purpose is collapsing them. Now
GetTickCount64(), which is real elapsed milliseconds and behaves identically in both worlds -
unchanged in live trading, genuinely one line per key per minute of run time in a tester pass.
Suppressed calls are counted and reported on the next line through, so nothing is hidden.
Compiled clean in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline taken first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:44:31 -04:00 | | | if(s_suppressed[i] > 0)
|
| | | {
|
| | | Print(message + StringFormat(" [+%I64d more since the last line]", s_suppressed[i]));
|
| | | s_suppressed[i] = 0;
|
| | | }
|
| | | else
|
| | | Print(message);
|
2026-07-26 23:08:32 -04:00 | | | return;
|
| | | }
|
| | | ArrayResize(s_keys, total + 1);
|
| | | ArrayResize(s_times, total + 1);
|
 refactor(logs): two verbosity levels, and a throttle that works in the tester
The operator's complaint that "the logs get filled" is measurable, so it was measured. The
2026-09-06 tester log is 1,570,535 lines and FOUR print statements are 86.6% of it:
556,732 35.4% "Starting direction calculation with total filters: N"
556,731 35.5% "Final directional result: N"
247,146 15.7% the two "open rejected" traces
None of the four is a decision. Direction() runs on every tick and recurses into every filter -
about eight calls a tick on this fleet - and emits two lines carrying a filter count and a
number. The rejection traces fire on every tick a side is blocked, which on a one-sided chart
is forever.
They were all on the same switch as everything else, so turning VerboseMode on to diagnose one
thing produced a journal too large to search. Added a second level, TraceMode, off by default,
and moved exactly those four sites to it. PrintVerbose() keeps its meaning and none of its ~40
call sites changed.
TCLog's throttle was near-inert where it mattered most. It measured its window with
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 nearly every call.
That is why 247,146 lines got through a function whose whole purpose is collapsing them. Now
GetTickCount64(), which is real elapsed milliseconds and behaves identically in both worlds -
unchanged in live trading, genuinely one line per key per minute of run time in a tester pass.
Suppressed calls are counted and reported on the next line through, so nothing is hidden.
Compiled clean in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline taken first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:44:31 -04:00 | | | ArrayResize(s_suppressed, total + 1);
|
| | | s_keys[total] = key;
|
| | | s_times[total] = now;
|
| | | s_suppressed[total] = 0;
|
2026-07-26 23:08:32 -04:00 | | | 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));
|
| | | }
|
| | |
|
2026-08-19 19:33:19 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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;
|
| | | }
|
| | |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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);
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| Article #6/#7 - the two broker distance levels, in price units. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | 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));
|
| | | }
|
| | |
|
2026-08-24 01:39:19 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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);
|
| | | }
|
| | |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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;
|
| | | }
|
2026-08-19 19:33:19 -04:00 | | | double bid = 0.0, ask = 0.0;
|
| | | if(!TCLiveQuote(symbol, "trade it yet", bid, ask, description))
|
2026-07-26 23:08:32 -04:00 | | | return false;
|
| | | description = "";
|
| | | return true;
|
| | | }
|
| | |
|
2026-09-13 14:32:40 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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;
|
| | | }
|
| | |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| 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. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | 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;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| 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. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | 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;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| 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. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | 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;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| 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. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | 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);
|
2026-08-19 19:33:19 -04:00 | | | double bid = 0.0, ask = 0.0;
|
| | | if(!TCLiveQuote(symbol, "validate stops", bid, ask, description))
|
2026-07-26 23:08:32 -04:00 | | | 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;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| 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). |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | 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);
|
2026-08-19 19:33:19 -04:00 | | | double bid = 0.0, ask = 0.0;
|
| | | if(!TCLiveQuote(symbol, "adjust stops", bid, ask, description))
|
2026-07-26 23:08:32 -04:00 | | | return false;
|
2026-08-19 19:33:19 -04:00 | | | int digits = TCDigits(symbol);
|
2026-07-26 23:08:32 -04:00 | | | 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);
|
2026-08-19 19:33:19 -04:00 | | | double bid = 0.0, ask = 0.0;
|
| | | if(!TCLiveQuote(symbol, "check the stop distance", bid, ask, description))
|
2026-07-26 23:08:32 -04:00 | | | 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;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| 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. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | 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;
|
| | | }
|
2026-08-19 19:33:19 -04:00 | | | double bid = 0.0, ask = 0.0;
|
| | | if(!TCLiveQuote(symbol, "evaluate the freeze level", bid, ask, description))
|
2026-07-26 23:08:32 -04:00 | | | 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;
|
| | | }
|
2026-08-19 19:33:19 -04:00 | | | double bid = 0.0, ask = 0.0;
|
| | | if(!TCLiveQuote(symbol, "evaluate the freeze level", bid, ask, description))
|
2026-07-26 23:08:32 -04:00 | | | 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;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| 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. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
| | | 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;
|
| | | }
|
| | |
|
2026-09-13 14:32:40 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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;
|
| | | }
|
2026-07-26 23:08:32 -04:00 | | | #endif // WARRIOR_TRADECHECKS_MQH
|
| | | //+------------------------------------------------------------------+
|