311 lines
No EOL
14 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+ |