mql5-execution-microstructu.../Include/RequestLatencyLab/CompletionPolicy.mqh

311 lines
No EOL
14 KiB
MQL5

//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+