2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-10 16:43:28 +03:00 | | | //| CompletionPolicy.mqh |
|
2026-09-16 15:37:55 +03:00 | | | //| Copyright 2026, MetaQuotes Ltd. |
|
2026-09-10 16:43:28 +03:00 | | | //| www.mql5.com |
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
| | | #ifndef REQUEST_LATENCY_LAB_COMPLETION_POLICY_MQH
|
| | | #define REQUEST_LATENCY_LAB_COMPLETION_POLICY_MQH
|
| | |
|
| | | #include "..\..\Include\RequestLatencyLab\Models.mqh"
|
| | |
|
| | | struct CompletionDecision
|
| | | {
|
2026-09-12 22:14:24 +03:00 | | | ENUM_LAB_OUTCOME outcome;
|
| | | bool confirmed;
|
| | | string reason_code;
|
| | | bool t6_assignable; // первая успешная проверка
|
2026-09-09 01:19:31 +03:00 | | |
|
2026-09-16 15:37:55 +03:00 | | | void Zero(void);
|
2026-09-09 01:19:31 +03:00 | | | };
|
| | |
|
| | | class CCompletionPolicy
|
| | | {
|
| | | public:
|
| | | //--- окончательные отказы, разрешённые для нашего действия (ТЗ §6.4)
|
2026-09-16 15:37:55 +03:00 | | | static bool IsFinalRejection(const uint retcode);
|
2026-09-09 01:19:31 +03:00 | | | //--- TIMEOUT/CONNECTION/LOCKED/ERROR не превращаются в REJECTED
|
2026-09-12 22:14:24 +03:00 | | | static bool IsAmbiguousCode(const uint retcode)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | return (retcode == TRADE_RETCODE_TIMEOUT || retcode == TRADE_RETCODE_CONNECTION ||
|
| | | retcode == TRADE_RETCODE_LOCKED || retcode == TRADE_RETCODE_ERROR);
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | //--- оценка снимка для операции
|
2026-09-12 22:14:24 +03:00 | | | static void Evaluate(const ENUM_LAB_OPERATION expected_op,
|
2026-09-09 01:19:31 +03:00 | | | const ConfirmationSnapshot &snap,
|
| | | const double volume_tolerance,
|
2026-09-16 15:37:55 +03:00 | | | CompletionDecision &decision);
|
2026-09-09 01:19:31 +03:00 | | |
|
| | | private:
|
2026-09-12 22:14:24 +03:00 | | | static void EvaluateMarketLike(const ConfirmationSnapshot &snap,
|
| | | const ENUM_LAB_OPERATION op, const double volume_tolerance,
|
2026-09-16 15:37:55 +03:00 | | | CompletionDecision &decision);
|
| | | static void EvaluatePendingCreate(const ConfirmationSnapshot &snap,
|
| | | CompletionDecision &decision);
|
| | | static void EvaluatePendingDelete(const ConfirmationSnapshot &snap,
|
| | | CompletionDecision &decision);
|
| | | };
|
| | | //+------------------------------------------------------------------+
|
| | | //| Внешние определения методов. |
|
| | | //+------------------------------------------------------------------+
|
| | | void CompletionDecision::Zero(void)
|
| | | {
|
| | | outcome = LAB_OUT_UNKNOWN;
|
| | | confirmed = false;
|
| | | reason_code = "";
|
| | | t6_assignable = false;
|
| | | }
|
| | | bool CCompletionPolicy::IsFinalRejection(const uint retcode)
|
| | | {
|
| | | switch(retcode)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | case TRADE_RETCODE_REJECT:
|
| | | case TRADE_RETCODE_INVALID:
|
| | | case TRADE_RETCODE_INVALID_VOLUME:
|
| | | case TRADE_RETCODE_INVALID_PRICE:
|
| | | case TRADE_RETCODE_INVALID_STOPS:
|
| | | case TRADE_RETCODE_INVALID_EXPIRATION:
|
| | | case TRADE_RETCODE_INVALID_FILL:
|
| | | case TRADE_RETCODE_INVALID_ORDER:
|
| | | case TRADE_RETCODE_NO_MONEY:
|
| | | case TRADE_RETCODE_TRADE_DISABLED:
|
| | | case TRADE_RETCODE_MARKET_CLOSED:
|
| | | case TRADE_RETCODE_CLIENT_DISABLES_AT:
|
| | | case TRADE_RETCODE_SERVER_DISABLES_AT:
|
| | | return(true);
|
| | | default:
|
| | | return(false);
|
| | | }
|
| | | }
|
| | | void CCompletionPolicy::Evaluate(const ENUM_LAB_OPERATION expected_op,
|
| | | const ConfirmationSnapshot &snap, const double volume_tolerance,
|
| | | CompletionDecision &decision)
|
| | | {
|
| | | decision.Zero();
|
| | | //--- B7 (audit-3): однозначный финальный reject (из sync-return или REQUEST)
|
| | | //--- без созданного ордера — допустимое терминальное подтверждение (ТЗ §6.4).
|
| | | if(snap.order_ticket == 0 && snap.reject_retcode != 0)
|
| | | {
|
| | | if(IsFinalRejection(snap.reject_retcode))
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | decision.outcome = LAB_OUT_REJECTED;
|
2026-09-12 22:14:24 +03:00 | | | decision.confirmed = true;
|
| | | decision.t6_assignable = true;
|
2026-09-16 15:37:55 +03:00 | | | decision.reason_code = "FINAL_REJECTION";
|
2026-09-09 01:19:31 +03:00 | | | return;
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- неоднозначные TIMEOUT/CONNECTION/LOCKED/ERROR не reject
|
| | | if(IsAmbiguousCode(snap.reject_retcode))
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | decision.reason_code = "AMBIGUOUS_RETURN";
|
2026-09-09 01:19:31 +03:00 | | | return;
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | if(!snap.trading_state_known && snap.order_ticket == 0)
|
| | | {
|
| | | decision.reason_code = "NO_EVIDENCE";
|
| | | return;
|
| | | }
|
| | | switch(expected_op)
|
| | | {
|
| | | case LAB_OP_MARKET_OPEN:
|
| | | case LAB_OP_POSITION_CLOSE:
|
| | | EvaluateMarketLike(snap, expected_op, volume_tolerance, decision);
|
| | | break;
|
| | | case LAB_OP_PENDING_CREATE:
|
| | | EvaluatePendingCreate(snap, decision);
|
| | | break;
|
| | | case LAB_OP_PENDING_DELETE:
|
| | | EvaluatePendingDelete(snap, decision);
|
| | | break;
|
| | | default:
|
| | | decision.reason_code = "UNSUPPORTED_OPERATION";
|
| | | break;
|
| | | }
|
| | | }
|
| | | void CCompletionPolicy::EvaluateMarketLike(const ConfirmationSnapshot &snap,
|
| | | const ENUM_LAB_OPERATION op, const double volume_tolerance,
|
| | | CompletionDecision &decision)
|
| | | {
|
| | | const long state = snap.order_state_after > 0 ? snap.order_state_after : snap.order_state_before;
|
| | | const bool terminal_filled = (state == ORDER_STATE_FILLED);
|
| | | const bool terminal_canceled = (state == ORDER_STATE_CANCELED);
|
| | | const bool terminal_expired = (state == ORDER_STATE_EXPIRED);
|
| | | //--- B4 (audit-3): отсутствие остатка доказывается ТОЛЬКО подтверждённым
|
| | | //--- отсутствием (4754); произвольная ошибка чтения (FAILED) НЕ считается
|
| | | //--- absence. Полный сбой скана истории также не даёт "нет остатка".
|
| | | const bool no_active = (!snap.history_scan_failed &&
|
| | | snap.active_select_status == LAB_READ_ABSENT_CONFIRMED);
|
| | | const double executed = snap.executed_volume_units;
|
| | | const double remaining = snap.remaining_volume_units;
|
| | | if(terminal_filled && no_active && executed > 0.0 &&
|
| | | MathAbs(executed - snap.requested_volume_units) <= volume_tolerance)
|
| | | {
|
| | | //--- полное исполнение: сделки согласованы с объёмом, остатка нет
|
| | | decision.outcome = LAB_OUT_FILLED;
|
| | | decision.confirmed = true;
|
| | | decision.t6_assignable = true;
|
| | | decision.reason_code = "FILLED_VOLUME_MATCH";
|
| | | return;
|
| | | }
|
| | | if(terminal_canceled && executed > 0.0 && no_active &&
|
| | | executed < snap.requested_volume_units)
|
| | | {
|
| | | //--- R5-B8: частичное завершение подтверждается по отсутствию
|
| | | //--- активного остатка; исторический неисполненный объём остатка
|
| | | //--- не смешивается с активным остатком.
|
| | | decision.outcome = LAB_OUT_PARTIAL_CANCELED;
|
| | | decision.confirmed = true;
|
| | | decision.t6_assignable = true;
|
| | | decision.reason_code = "PARTIAL_CANCELED";
|
| | | return;
|
| | | }
|
| | | if(terminal_expired && executed > 0.0 && no_active &&
|
| | | executed < snap.requested_volume_units)
|
| | | {
|
| | | decision.outcome = LAB_OUT_PARTIAL_EXPIRED;
|
| | | decision.confirmed = true;
|
| | | decision.t6_assignable = true;
|
| | | decision.reason_code = "PARTIAL_EXPIRED";
|
| | | return;
|
| | | }
|
| | | if(terminal_canceled && executed == 0.0 && no_active)
|
| | | {
|
| | | decision.outcome = LAB_OUT_CANCELED_NO_FILL;
|
| | | decision.confirmed = true;
|
| | | decision.t6_assignable = true;
|
| | | decision.reason_code = "CANCELED_NO_FILL";
|
| | | return;
|
| | | }
|
| | | if(terminal_expired && executed == 0.0 && no_active)
|
| | | {
|
| | | decision.outcome = LAB_OUT_EXPIRED_NO_FILL;
|
| | | decision.confirmed = true;
|
| | | decision.t6_assignable = true;
|
| | | decision.reason_code = "EXPIRED_NO_FILL";
|
| | | return;
|
| | | }
|
| | | //--- недостаточно данных — T6 не ставится
|
| | | decision.outcome = LAB_OUT_UNKNOWN;
|
| | | decision.confirmed = false;
|
| | | decision.reason_code = (no_active ? "INSUFFICIENT_HISTORY" : "ORDER_STILL_ACTIVE");
|
| | | }
|
| | | void CCompletionPolicy::EvaluatePendingCreate(const ConfirmationSnapshot &snap,
|
| | | CompletionDecision &decision)
|
| | | {
|
| | | const long state = snap.order_state_after > 0 ? snap.order_state_after : snap.order_state_before;
|
| | | const bool active_placed = (state == ORDER_STATE_PLACED && snap.active_select_status == LAB_READ_FOUND);
|
| | | if(active_placed)
|
| | | {
|
| | | //--- R4-S2: размещение подтверждается только при совпадении ожидаемого
|
| | | //--- контракта (symbol/type/magic/volume/price). Допуск цены задаётся
|
| | | //--- контроллером (доли тика); отсутствие ожидаемых данных не даёт T6.
|
| | | //--- R5-B8: ожидаемый контракт включает ТОЧНЫЙ тип/сторону ордера.
|
| | | //--- Совпадение symbol/magic/volume/price при другом типе не
|
| | | //--- подтверждает размещение.
|
| | | const bool contract_known = (StringLen(snap.expected_symbol) > 0 &&
|
| | | snap.expected_magic != 0 &&
|
| | | snap.expected_volume_lots > 0.0 &&
|
| | | snap.expected_price > 0.0 &&
|
| | | snap.expected_order_type != 0);
|
| | | const bool params_ok = (contract_known &&
|
| | | snap.order_symbol == snap.expected_symbol &&
|
| | | snap.order_magic == snap.expected_magic &&
|
| | | MathAbs(snap.order_volume_lots - snap.expected_volume_lots) < 1e-9 &&
|
| | | MathAbs(snap.order_price - snap.expected_price) <=
|
| | | (snap.price_tolerance > 0.0 ? snap.price_tolerance : 1e-9) &&
|
| | | snap.order_type == snap.expected_order_type);
|
| | | if(params_ok)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | decision.outcome = LAB_OUT_PENDING_PLACED;
|
2026-09-12 22:14:24 +03:00 | | | decision.confirmed = true;
|
| | | decision.t6_assignable = true;
|
2026-09-16 15:37:55 +03:00 | | | decision.reason_code = "PENDING_PLACED_CONTRACT_MATCH";
|
2026-09-09 01:19:31 +03:00 | | | return;
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- ордер существует, но не совпадает с контрактом — T6 не ставится
|
2026-09-12 22:14:24 +03:00 | | | decision.outcome = LAB_OUT_UNKNOWN;
|
| | | decision.confirmed = false;
|
2026-09-16 15:37:55 +03:00 | | | decision.reason_code = "PENDING_CONTRACT_MISMATCH";
|
| | | return;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- pending успел исполниться до подтверждения размещения?
|
| | | if(state == ORDER_STATE_FILLED || snap.executed_volume_units > 0.0)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | decision.outcome = LAB_OUT_UNEXPECTED_ACTIVATION;
|
| | | decision.confirmed = true;
|
| | | decision.t6_assignable = false; // T6 размещения не восстанавливается
|
| | | decision.reason_code = "UNEXPECTED_ACTIVATION";
|
| | | return;
|
| | | }
|
| | | if(state == ORDER_STATE_CANCELED)
|
| | | {
|
| | | decision.outcome = LAB_OUT_CANCELED_NO_FILL;
|
| | | decision.confirmed = true;
|
| | | decision.t6_assignable = true;
|
| | | decision.reason_code = "PENDING_CANCELED_BEFORE_CONFIRM";
|
| | | return;
|
| | | }
|
| | | decision.outcome = LAB_OUT_UNKNOWN;
|
| | | decision.confirmed = false;
|
| | | decision.reason_code = "PENDING_NOT_CONFIRMED";
|
| | | }
|
| | | void CCompletionPolicy::EvaluatePendingDelete(const ConfirmationSnapshot &snap,
|
| | | CompletionDecision &decision)
|
| | | {
|
| | | const long state = snap.order_state_after > 0 ? snap.order_state_after : snap.order_state_before;
|
| | | //--- R4-S1: отсутствие активного ордера доказывается ТОЛЬКО
|
| | | //--- LAB_READ_ABSENT_CONFIRMED; произвольная ошибка чтения не absence.
|
| | | const bool not_active = (snap.active_select_status == LAB_READ_ABSENT_CONFIRMED);
|
| | | //--- R5-B8: исходы CANCEL/EXPIRE/FILL не смешиваются.
|
| | | //--- R6-B5: ошибка/обрыв скана истории не доказывает нулевое исполнение.
|
| | | if(not_active && !snap.history_scan_failed &&
|
| | | state == ORDER_STATE_CANCELED && snap.executed_volume_units == 0.0)
|
| | | {
|
| | | //--- отмена согласована, объём не исполнен — удаление подтверждено
|
| | | decision.outcome = LAB_OUT_PENDING_DELETED;
|
| | | decision.confirmed = true;
|
| | | decision.t6_assignable = true;
|
| | | decision.reason_code = "PENDING_DELETED";
|
| | | return;
|
| | | }
|
| | | if(state == ORDER_STATE_FILLED)
|
| | | {
|
| | | if(snap.executed_volume_units > 0.0)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | //--- заполнение после попытки удаления — активация исходного ордера
|
2026-09-12 22:14:24 +03:00 | | | decision.outcome = LAB_OUT_UNEXPECTED_ACTIVATION;
|
| | | decision.confirmed = true;
|
| | | decision.t6_assignable = true;
|
2026-09-16 15:37:55 +03:00 | | | decision.reason_code = "DELETED_ORDER_FILLED";
|
2026-09-09 01:19:31 +03:00 | | | return;
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- FILLED с нулевым известным объёмом — противоречивый снимок
|
2026-09-12 22:14:24 +03:00 | | | decision.outcome = LAB_OUT_UNKNOWN;
|
| | | decision.confirmed = false;
|
2026-09-16 15:37:55 +03:00 | | | decision.reason_code = "FILLED_WITH_ZERO_EXECUTED";
|
| | | return;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(state == ORDER_STATE_EXPIRED)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | //--- истечение не является подтверждением выполнения команды отмены
|
2026-09-12 22:14:24 +03:00 | | | decision.outcome = LAB_OUT_UNKNOWN;
|
| | | decision.confirmed = false;
|
2026-09-16 15:37:55 +03:00 | | | decision.reason_code = "EXPIRED_NOT_DELETE_CONFIRM";
|
| | | return;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(snap.executed_volume_units > 0.0)
|
| | | {
|
| | | //--- fill после попытки удаления относится к исходному ордеру
|
| | | decision.outcome = LAB_OUT_UNEXPECTED_ACTIVATION;
|
| | | decision.confirmed = true;
|
| | | decision.t6_assignable = true;
|
| | | decision.reason_code = "DELETED_ORDER_FILLED_LATER";
|
| | | return;
|
| | | }
|
| | | decision.outcome = LAB_OUT_UNKNOWN;
|
| | | decision.confirmed = false;
|
| | | decision.reason_code = "DELETE_NOT_CONFIRMED";
|
| | | }
|
2026-09-09 01:19:31 +03:00 | | |
|
2026-09-12 22:14:24 +03:00 | | | #endif // REQUEST_LATENCY_LAB_COMPLETION_POLICY_MQH
|
| | | //+------------------------------------------------------------------+
|