//+------------------------------------------------------------------+ //| CompletionPolicy.mqh | //| Copyright 2026, MetaQuotes Ltd. | //| www.mql5.com | //+------------------------------------------------------------------+ #ifndef REQUEST_LATENCY_LAB_COMPLETION_POLICY_MQH #define REQUEST_LATENCY_LAB_COMPLETION_POLICY_MQH #include "..\..\Include\RequestLatencyLab\Models.mqh" struct CompletionDecision { ENUM_LAB_OUTCOME outcome; bool confirmed; string reason_code; bool t6_assignable; // первая успешная проверка void Zero(void); }; class CCompletionPolicy { public: //--- окончательные отказы, разрешённые для нашего действия (ТЗ §6.4) static bool IsFinalRejection(const uint retcode); //--- TIMEOUT/CONNECTION/LOCKED/ERROR не превращаются в REJECTED static bool IsAmbiguousCode(const uint retcode) { return (retcode == TRADE_RETCODE_TIMEOUT || retcode == TRADE_RETCODE_CONNECTION || retcode == TRADE_RETCODE_LOCKED || retcode == TRADE_RETCODE_ERROR); } //--- оценка снимка для операции static void Evaluate(const ENUM_LAB_OPERATION expected_op, const ConfirmationSnapshot &snap, const double volume_tolerance, CompletionDecision &decision); private: static void EvaluateMarketLike(const ConfirmationSnapshot &snap, const ENUM_LAB_OPERATION op, const double volume_tolerance, 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) { 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)) { decision.outcome = LAB_OUT_REJECTED; decision.confirmed = true; decision.t6_assignable = true; decision.reason_code = "FINAL_REJECTION"; return; } //--- неоднозначные TIMEOUT/CONNECTION/LOCKED/ERROR не reject if(IsAmbiguousCode(snap.reject_retcode)) { decision.reason_code = "AMBIGUOUS_RETURN"; return; } } 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) { decision.outcome = LAB_OUT_PENDING_PLACED; decision.confirmed = true; decision.t6_assignable = true; decision.reason_code = "PENDING_PLACED_CONTRACT_MATCH"; return; } //--- ордер существует, но не совпадает с контрактом — T6 не ставится decision.outcome = LAB_OUT_UNKNOWN; decision.confirmed = false; decision.reason_code = "PENDING_CONTRACT_MISMATCH"; return; } //--- pending успел исполниться до подтверждения размещения? if(state == ORDER_STATE_FILLED || snap.executed_volume_units > 0.0) { 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) { //--- заполнение после попытки удаления — активация исходного ордера decision.outcome = LAB_OUT_UNEXPECTED_ACTIVATION; decision.confirmed = true; decision.t6_assignable = true; decision.reason_code = "DELETED_ORDER_FILLED"; return; } //--- FILLED с нулевым известным объёмом — противоречивый снимок decision.outcome = LAB_OUT_UNKNOWN; decision.confirmed = false; decision.reason_code = "FILLED_WITH_ZERO_EXECUTED"; return; } if(state == ORDER_STATE_EXPIRED) { //--- истечение не является подтверждением выполнения команды отмены decision.outcome = LAB_OUT_UNKNOWN; decision.confirmed = false; decision.reason_code = "EXPIRED_NOT_DELETE_CONFIRM"; return; } 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"; } #endif // REQUEST_LATENCY_LAB_COMPLETION_POLICY_MQH //+------------------------------------------------------------------+