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

1144 lines
50 KiB
MQL5

//+------------------------------------------------------------------+
//| RequestTracker.mqh |
//| Copyright 2026, MetaQuotes Ltd. |
//| www.mql5.com |
//+------------------------------------------------------------------+
#ifndef REQUEST_LATENCY_LAB_REQUEST_TRACKER_MQH
#define REQUEST_LATENCY_LAB_REQUEST_TRACKER_MQH
#include "..\..\Include\RequestLatencyLab\Models.mqh"
#include "..\..\Include\RequestLatencyLab\CompletionPolicy.mqh"
#include "..\..\Include\RequestLatencyLab\Statistics.mqh"
//+------------------------------------------------------------------+
//| Полная запись запроса (обязательная LatencySample + служебные |
//| данные; ТЗ §3.2). |
//+------------------------------------------------------------------+
struct RequestMetadata
{
2026-09-12 22:14:24 +03:00
RequestKey key;
RequestPlan plan;
LatencySample sample;
//--- маски наличия/применимости точек; бит 1<<index, включая T5
2026-09-12 22:14:24 +03:00
uint present_mask;
uint applicable_mask;
uint applicability_known_mask;
bool has_request_id;
bool has_retcode;
//--- результат отправки
2026-09-12 22:14:24 +03:00
bool send_ok;
uint send_retcode;
int send_retcode_external;
int send_last_error;
int request_retcode_external;
//--- корреляция
ENUM_LAB_CORRELATION correlation_status;
string correlation_method;
ulong correlation_resolved_us;
ulong order_ticket;
ulong position_ticket;
ulong position_identifier;
//--- сделки
2026-09-12 22:14:24 +03:00
ulong last_deal_us; // T5
uint deal_count;
double executed_volume;
double callback_volume;
double history_volume;
bool deal_coverage_complete;
bool callback_coverage_complete; // R5-B4: полнота локальных callback-меток
//--- качество
2026-09-12 22:14:24 +03:00
bool recovered;
bool conflict;
bool measurement_interrupted;
bool scenario_deviation;
bool collection_closed;
bool deadline_exceeded;
bool trading_state_known;
bool confirmation_invalidated;
//--- завершение
ENUM_LAB_OUTCOME outcome;
ENUM_LAB_OUTCOME outcome_at_final;
ENUM_LAB_FINAL_SOURCE final_source;
ulong confirmation_record_id;
ulong current_confirmation_record_id;
ulong observation_deadline_us;
ulong collection_deadline_us;
ulong collection_close_us;
ulong collection_close_event_id;
//--- рынок
double bid_at_send;
double ask_at_send;
double spread_at_send;
long tick_time_msc;
ulong quote_received_us;
string market_window_id;
ulong market_window_seq; // R8-S2: номер окна E3 (market_windows.sequence)
ENUM_LAB_MARKET_REGIME market_regime;
string calibration_id;
//--- диагностика
2026-09-12 22:14:24 +03:00
bool is_warmup;
ulong last_handler_duration_us;
ulong last_timer_actual_us;
//--- метрики (индекс = ENUM_LAB_METRIC)
2026-09-12 22:14:24 +03:00
long metrics_value[8];
bool metrics_has[8];
ENUM_LAB_FITNESS metrics_category[8];
string metrics_reasons[8];
void Reset(const RequestPlan &p);
};
//+------------------------------------------------------------------+
//| CRequestTracker — реестр и корреляция. |
//+------------------------------------------------------------------+
class CRequestTracker
{
private:
2026-09-12 22:14:24 +03:00
string m_session_id;
int m_capacity;
int m_count;
RequestMetadata m_records[];
DealRecord m_deals[];
int m_deal_count;
int m_deal_lost_count; // R6-S4: потери реестра сделок
ulong m_unresolved_request_ids[]; // request_id без точного моста
int m_unresolved_count;
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
//--- B2 (audit-3): отложенные ORDER_ADD/DEAL_ADD до точного моста
struct DeferredTxn
{
2026-09-12 22:14:24 +03:00
ulong entry_us;
long trans_type;
ulong order_ticket;
ulong deal_ticket;
ulong position_ticket;
double volume;
bool bound;
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
};
2026-09-12 22:14:24 +03:00
DeferredTxn m_deferred[];
int m_deferred_count;
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
public:
CRequestTracker(void);
bool Init(const int capacity, const string session_id);
2026-09-12 22:14:24 +03:00
int Count() const { return(m_count); }
int DealCount() const { return(m_deal_count); }
int DealLostCount() const { return(m_deal_lost_count); } // R6-S4
bool GetByIndex(const int index, RequestMetadata &out) const;
//--- резервирование попытки до отправки (ТЗ §4)
bool Reserve(const RequestPlan &plan, ulong &sequence);
//--- R9-B1: откат НЕОТПРАВЛЕННОГО резерва (pre-T0 отказ: устаревший
//--- контекст E3, переполнение журнала intent и т.п.). Удаляется запись,
//--- в которую транспорт НЕ входил (нет T0) — слот планировщика мог быть
//--- возвращён в очередь (MarkWaitSlot), и повтор той же sequence обязан
//--- пройти без дубликата ключа. Фантомный MAIN в samples не экспортируется;
//--- реестр сделок/события журнала этот резерв не ссылают.
bool ReleaseReservation(const ulong sequence);
bool GetMetadata(const ulong sequence, RequestMetadata &out) const;
bool SetMetadata(const ulong sequence, const RequestMetadata &in);
bool GetDeal(const int index, DealRecord &out) const;
//--- T0: непосредственно перед вызовом отправки
bool MarkSendStart(const ulong sequence, const ulong t0);
//--- T1: сразу после возврата; сохраняются bool/result/error
bool MarkSendReturn(const ulong sequence, const SendObservation &send);
//--- T2 (REQUEST): точная связь по request_id с проверкой исходного
//--- запроса; retcode структуры — только из REQUEST.
bool MarkRequest(const ulong sequence, const ulong entry_us, const uint request_id,
const uint retcode, const int retcode_external,
const ulong result_order, const ulong result_deal);
//--- T3: вход в первый связанный ORDER_ADD
bool MarkOrderAdd(const ulong sequence, const ulong entry_us,
const ulong order_ticket, const string method);
//--- T4/T5: уникальные торговые DEAL_ADD (порядок наблюдения)
bool MarkDealAdd(const ulong sequence, const ulong entry_us, const ulong deal_ticket,
const double volume);
//--- T6 устанавливает только CompletionPolicy через эту точку
bool SetFinal(const ulong sequence, const ulong check_end_us,
const ENUM_LAB_OUTCOME outcome, const ENUM_LAB_FINAL_SOURCE source,
const ulong confirmation_record_id);
//--- опровержение первого доказательства (конфликт, поздняя коррекция)
bool InvalidateConfirmation(const ulong sequence, const string reason);
//--- сроки (основное и позднее окна от T0; проверка переполнения)
void ComputeDeadlines(const ulong sequence, const ulong outcome_timeout_us,
const ulong late_grace_us);
//--- закрытие сбора (исход подтверждён или истёк поздний срок)
bool CloseCollection(const ulong sequence, const ulong now_us,
const ulong event_sequence);
//--- расчёт метрики по ТЗ §3.4/§9 (только чтение записи; static —
//--- единая семантика также для offline rebuild/export контракта)
static bool EvaluateMetric(const RequestMetadata &r, const ENUM_LAB_METRIC metric,
MetricObservation &out, bool &evaluated);
//--- регистрация сделки из истории (recovered без callback-времени)
bool RegisterHistoryDeal(const ulong deal_ticket, const ulong order_ticket,
const double volume, const long deal_time_msc,
const ulong owner_sequence = 0);
//--- неподтверждённые request_id (для позднего разрешения)
bool PushUnresolved(const uint request_id);
int UnresolvedCount() const { return(m_unresolved_count); }
//--- точный поиск записи по request_id (мост из результата отправки)
int FindByRequestId(const uint request_id) const;
//--- поиск записи по order_ticket
int FindByOrderTicket(const ulong ticket) const;
//--- последовательность записи, которой принадлежит сделка
ulong FindDealOwnerSequence(const ulong deal_ticket) const;
//--- B2: буферизация раннего ORDER_ADD/DEAL_ADD без точной привязки
bool PushDeferred(const ulong entry_us, const long trans_type,
const ulong order_ticket, const ulong deal_ticket,
const ulong position_ticket, const double volume);
int DeferredCount() const { return(m_deferred_count); }
//--- B2: повторная точная корреляция буфера после появления моста.
//--- R4-B8: возвращает число впервые привязанных событий, чтобы вызывающий
//--- код мог зафиксировать CORRELATION_LINK в журнале (внешний путь связывания).
int ReconcileDeferred(const ulong sequence);
//--- разрешение последовательности события по идентификаторам:
//--- REQUEST связывается только по request_id (тикеты не используются);
//--- ORDER_ADD/DEAL_ADD - по тикетам сделки/ордера.
//--- Без точного совпадения возвращается 0 (аудит-2 B2): неоднозначное
//--- событие не получает фиктивную привязку к активной записи.
ulong ResolveEventSequence(const bool request_event, const uint request_id,
const ulong order_ticket, const ulong deal_ticket) const;
//--- позиция сделки (аудит-2 S7): тикет позиции из trans.position
bool SetPositionTicket(const ulong sequence, const ulong ticket);
//--- аудит-2 B3: истёк ли late-grace (по collection_deadline_us)
bool GraceExpired(const ulong sequence, const ulong now_us) const;
//--- фактический интервал таймера (резервная сверка)
bool SetTimerInterval(const ulong sequence, const ulong actual_us);
bool SetDeadlineExceeded(const ulong sequence);
bool SetTradingStateKnown(const ulong sequence, const bool known);
bool SetDealCoverageComplete(const ulong sequence);
//--- R6-B5: сохранение доказанного исхода ОТДЕЛЬНО от права установить T6
//--- (например, UNEXPECTED_ACTIVATION: confirmed=true, t6_assignable=false).
bool RecordOutcome(const ulong sequence, const ENUM_LAB_OUTCOME outcome);
//--- R4-B8: перенос результатов полной (не световой) сверки в запись,
//--- как это делает синтетический путь: executed_volume (units), объём
//--- истории (лоты, из объёма сделок), признак полного покрытия.
bool SyncReconcile(const ulong sequence, const ConfirmationSnapshot &snap,
const double volume_step, const double tolerance);
bool SetDeadlinesFromSample(const ulong sequence, const ulong timeout_us,
const ulong grace_us);
//--- S2 (audit-3): рыночный контекст при отправке
bool SetMarketContext(const ulong sequence, const double bid, const double ask,
const long tick_time_msc);
//--- длительность обработчика OnTradeTransaction для записи (E5)
bool SetHandlerDuration(const ulong sequence, const ulong duration_us);
//--- признак рыночного режима, наблюдаемый при отправке
bool SetMarketRegime(const ulong sequence, const ENUM_LAB_MARKET_REGIME regime,
const string window_id, const string calibration_id = "",
const ulong window_seq = 0);
private:
int FindBySequence(const ulong sequence) const;
int FindDeal(const ulong ticket) const;
};
//+------------------------------------------------------------------+
//| Внешние определения методов. |
//+------------------------------------------------------------------+
void RequestMetadata::Reset(const RequestPlan &p)
{
key.session_id = "";
key.sequence = p.sequence;
plan = p;
ZeroMemory(sample);
sample.sequence = p.sequence;
present_mask = 0;
applicable_mask = 0;
applicability_known_mask = 0;
has_request_id = false;
has_retcode = false;
send_ok = false;
send_retcode = 0;
send_retcode_external = 0;
send_last_error = 0;
request_retcode_external = 0;
correlation_status = LAB_CORR_UNRESOLVED;
correlation_method = "";
correlation_resolved_us = 0;
order_ticket = 0;
position_ticket = 0;
position_identifier = 0;
last_deal_us = 0;
deal_count = 0;
executed_volume = 0.0;
callback_volume = 0.0;
history_volume = 0.0;
deal_coverage_complete = false;
callback_coverage_complete = false; // R5-B4: полнота локальных callback-меток
recovered = false;
conflict = false;
measurement_interrupted = false;
scenario_deviation = false;
collection_closed = false;
deadline_exceeded = false;
trading_state_known = false;
confirmation_invalidated = false;
outcome = LAB_OUT_UNKNOWN;
outcome_at_final = LAB_OUT_UNKNOWN;
final_source = LAB_FINAL_NONE;
confirmation_record_id = 0;
current_confirmation_record_id = 0;
observation_deadline_us = 0;
collection_deadline_us = 0;
collection_close_us = 0;
collection_close_event_id = 0;
bid_at_send = 0.0;
ask_at_send = 0.0;
spread_at_send = 0.0;
tick_time_msc = 0;
quote_received_us = 0;
market_window_id = "";
market_window_seq = 0; // R8-S2
market_regime = LAB_REGIME_UNKNOWN;
calibration_id = "";
is_warmup = (p.role == LAB_ROLE_WARMUP || p.role == LAB_ROLE_SYNTHETIC);
last_handler_duration_us = 0;
last_timer_actual_us = 0;
ArrayInitialize(metrics_value, 0);
ArrayInitialize(metrics_has, false);
ArrayInitialize(metrics_category, LAB_FIT_MISSING);
for(int i = 0; i < 8; i++)
metrics_reasons[i] = "";
}
//+------------------------------------------------------------------+
//| CRequestTracker:: — реализации. |
//+------------------------------------------------------------------+
CRequestTracker::CRequestTracker(void)
{
m_session_id = "";
m_capacity = 0;
m_count = 0;
m_deal_count = 0;
m_deal_lost_count = 0;
m_unresolved_count = 0;
m_deferred_count = 0;
}
bool CRequestTracker::Init(const int capacity, const string session_id)
{
m_capacity = capacity;
m_session_id = session_id;
if(ArrayResize(m_records, m_capacity) != m_capacity)
return(false);
if(ArrayResize(m_deals, m_capacity) != m_capacity)
return(false);
if(ArrayResize(m_unresolved_request_ids, 64) != 64)
return(false);
if(ArrayResize(m_deferred, 256) != 256)
return(false);
m_deferred_count = 0;
m_count = 0;
m_deal_count = 0;
m_deal_lost_count = 0;
m_unresolved_count = 0;
return(true);
}
bool CRequestTracker::GetByIndex(const int index, RequestMetadata &out) const
{
if(index < 0 || index >= m_count)
return(false);
out = m_records[index];
return(true);
}
bool CRequestTracker::Reserve(const RequestPlan &plan, ulong &sequence)
{
if(m_count >= m_capacity)
return(false);
sequence = plan.sequence;
if(FindBySequence(sequence) >= 0)
return(false); // дубликат ключа session/sequence
m_count++;
m_records[m_count - 1].Reset(plan);
m_records[m_count - 1].key.session_id = m_session_id;
return(true);
}
bool CRequestTracker::ReleaseReservation(const ulong sequence)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
if((m_records[i].present_mask & (uint)LAB_MASK_T0) != 0)
return(false); // уже в транспорте — не откатывать
for(int k = i; k < m_count - 1; k++)
m_records[k] = m_records[k + 1];
m_count--;
return(true);
}
bool CRequestTracker::GetMetadata(const ulong sequence, RequestMetadata &out) const
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
out = m_records[i];
return(true);
}
bool CRequestTracker::SetMetadata(const ulong sequence, const RequestMetadata &in)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
m_records[i] = in;
return(true);
}
bool CRequestTracker::GetDeal(const int index, DealRecord &out) const
{
if(index < 0 || index >= m_deal_count)
return(false);
out = m_deals[index];
return(true);
}
bool CRequestTracker::MarkSendStart(const ulong sequence, const ulong t0)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
m_records[i].sample.send_start_us = t0;
m_records[i].present_mask |= (uint)LAB_MASK_T0;
return(true);
}
bool CRequestTracker::MarkSendReturn(const ulong sequence, const SendObservation &send)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
m_records[i].sample.send_return_us = send.t1_us;
m_records[i].present_mask |= (uint)LAB_MASK_T1;
m_records[i].send_ok = send.send_ok;
m_records[i].send_retcode = send.result.retcode;
m_records[i].send_retcode_external = send.result.retcode_external;
m_records[i].send_last_error = send.last_error;
m_records[i].sample.completed = false;
//--- мост из результата отправки: result.order/result.deal
if(send.result.order != 0 && m_records[i].order_ticket == 0)
{
m_records[i].order_ticket = send.result.order;
m_records[i].correlation_method = "RESULT_ORDER";
}
if(send.result.request_id != 0)
{
m_records[i].sample.request_id = send.result.request_id;
m_records[i].has_request_id = true;
}
return(true);
}
bool CRequestTracker::MarkRequest(const ulong sequence, const ulong entry_us,
const uint request_id, const uint retcode, const int retcode_external,
const ulong result_order, const ulong result_deal)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
//--- существующая метка T2 не переписывается
if((m_records[i].present_mask & (uint)LAB_MASK_T2) != 0)
return(false);
m_records[i].sample.request_event_us = entry_us;
m_records[i].present_mask |= (uint)LAB_MASK_T2;
m_records[i].sample.retcode = retcode;
m_records[i].has_retcode = true;
m_records[i].request_retcode_external = retcode_external;
if(request_id != 0)
{
m_records[i].sample.request_id = request_id;
m_records[i].has_request_id = true;
}
if(result_order != 0 && m_records[i].order_ticket == 0)
{
m_records[i].order_ticket = result_order;
m_records[i].correlation_method = "REQUEST_ID_RESULT_ORDER";
}
//--- result_deal - тикет сделки, а не идентификатор позиции; сюда не пишется (п.5 аудита)
if(m_records[i].correlation_status == LAB_CORR_UNRESOLVED)
{
m_records[i].correlation_status = LAB_CORR_EXACT;
m_records[i].correlation_resolved_us = entry_us;
}
return(true);
}
bool CRequestTracker::MarkOrderAdd(const ulong sequence, const ulong entry_us,
const ulong order_ticket, const string method)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
if((m_records[i].present_mask & (uint)LAB_MASK_T3) != 0)
return(false);
m_records[i].sample.order_event_us = entry_us;
m_records[i].present_mask |= (uint)LAB_MASK_T3;
if(order_ticket != 0)
m_records[i].order_ticket = order_ticket;
if(m_records[i].correlation_status == LAB_CORR_UNRESOLVED && order_ticket != 0)
{
m_records[i].correlation_status = LAB_CORR_EXACT;
m_records[i].correlation_method = method;
m_records[i].correlation_resolved_us = entry_us;
}
return(true);
}
bool CRequestTracker::MarkDealAdd(const ulong sequence, const ulong entry_us,
const ulong deal_ticket, const double volume)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
bool seen_before = false; // дубликат deal_ticket (callback уже был)
bool callback_new = false; // R6-B1: первый callback после истории
//--- уникальность сделки в реестре сделок
int di = FindDeal(deal_ticket);
if(di < 0)
{
//--- R6-S4: переполнение реестра сделок — явный отказ/качество,
//--- а не тихий успех с потерей дедупликации тикета.
if(m_deal_count >= m_capacity)
{
m_deal_lost_count++;
return(false);
}
m_deal_count++;
m_deals[m_deal_count - 1].Zero();
m_deals[m_deal_count - 1].deal_ticket = deal_ticket;
m_deals[m_deal_count - 1].order_ticket = m_records[i].order_ticket;
m_deals[m_deal_count - 1].owner_sequence = sequence;
m_deals[m_deal_count - 1].symbol = m_records[i].plan.symbol;
m_deals[m_deal_count - 1].has_callback = true;
m_deals[m_deal_count - 1].callback_first_us = entry_us;
m_deals[m_deal_count - 1].callback_volume = volume;
di = m_deal_count - 1;
}
else
{
seen_before = true;
//--- повтор DEAL_ADD: объём и T5 не сдвигаются дубликатом
if(!m_deals[di].has_callback)
{
//--- R6-B1: первый реальный callback ПОСЛЕ history-регистрации
//--- того же тикета учитывается ровно один раз (не теряет
//--- счётчик/объём из-за seen_before).
m_deals[di].has_callback = true;
m_deals[di].callback_first_us = entry_us;
m_deals[di].callback_volume = volume;
callback_new = true;
}
}
//--- первое получение уникальной сделки обновляет T4/T5
if(di >= 0 && m_deals[di].callback_first_us == entry_us)
{
if((m_records[i].present_mask & (uint)LAB_MASK_T4) == 0 ||
entry_us < m_records[i].sample.first_deal_us)
{
m_records[i].sample.first_deal_us = entry_us;
m_records[i].present_mask |= (uint)LAB_MASK_T4;
}
if((m_records[i].present_mask & (uint)LAB_MASK_T5) == 0 ||
entry_us > m_records[i].last_deal_us)
{
m_records[i].last_deal_us = entry_us;
m_records[i].present_mask |= (uint)LAB_MASK_T5;
}
}
//--- R6-B1: объём/счётчик растут на первой уникальной сделке И на первом
//--- callback истории-восстановленной сделки (но не на дубликате).
if(!seen_before || callback_new)
{
if(m_records[i].deal_count == 0)
m_records[i].callback_volume = volume;
else
m_records[i].callback_volume += volume;
m_records[i].deal_count++;
}
return(true);
}
bool CRequestTracker::SetFinal(const ulong sequence, const ulong check_end_us,
const ENUM_LAB_OUTCOME outcome, const ENUM_LAB_FINAL_SOURCE source,
const ulong confirmation_record_id)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
//--- первая успешная проверка не переписывается (ТЗ §4)
if((m_records[i].present_mask & (uint)LAB_MASK_T6) != 0)
return(false);
m_records[i].sample.final_event_us = check_end_us;
m_records[i].present_mask |= (uint)LAB_MASK_T6;
//--- R6-B3: превышение срока вычисляется из T0/срока/фактической T6
//--- независимо от порядка callback и таймера (sticky-семантика).
if(m_records[i].observation_deadline_us > 0 &&
check_end_us > m_records[i].observation_deadline_us)
m_records[i].deadline_exceeded = true;
m_records[i].outcome_at_final = outcome;
m_records[i].outcome = outcome;
m_records[i].final_source = source;
m_records[i].confirmation_record_id = confirmation_record_id;
m_records[i].current_confirmation_record_id = confirmation_record_id;
m_records[i].sample.completed = true;
return(true);
}
bool CRequestTracker::InvalidateConfirmation(const ulong sequence, const string reason)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
m_records[i].confirmation_invalidated = true;
m_records[i].conflict = true;
m_records[i].correlation_status = LAB_CORR_CONFLICT;
return(true);
}
void CRequestTracker::ComputeDeadlines(const ulong sequence, const ulong outcome_timeout_us,
const ulong late_grace_us)
{
const int i = FindBySequence(sequence);
if(i < 0)
return;
if((m_records[i].present_mask & (uint)LAB_MASK_T0) == 0)
return;
const ulong t0 = m_records[i].sample.send_start_us;
if(ULONG_MAX - t0 < outcome_timeout_us)
return; // переполнение: сроки не вычисляются
m_records[i].observation_deadline_us = t0 + outcome_timeout_us;
if(ULONG_MAX - t0 < outcome_timeout_us + late_grace_us)
{
m_records[i].collection_deadline_us = ULONG_MAX;
return;
}
m_records[i].collection_deadline_us = t0 + outcome_timeout_us + late_grace_us;
}
bool CRequestTracker::CloseCollection(const ulong sequence, const ulong now_us,
const ulong event_sequence)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
if(m_records[i].collection_closed)
return(false);
m_records[i].collection_closed = true;
m_records[i].collection_close_us = now_us;
m_records[i].collection_close_event_id = event_sequence;
return(true);
}
bool CRequestTracker::EvaluateMetric(const RequestMetadata &r, const ENUM_LAB_METRIC metric,
MetricObservation &out, bool &evaluated)
{
evaluated = false;
out.Zero();
const ENUM_LAB_OPERATION op = r.plan.operation;
const ENUM_LAB_OUTCOME oc = r.outcome;
const bool has_t0 = (r.present_mask & (uint)LAB_MASK_T0) != 0;
const bool has_t1 = (r.present_mask & (uint)LAB_MASK_T1) != 0;
const bool has_t2 = (r.present_mask & (uint)LAB_MASK_T2) != 0;
const bool has_t3 = (r.present_mask & (uint)LAB_MASK_T3) != 0;
const bool has_t4 = (r.present_mask & (uint)LAB_MASK_T4) != 0;
const bool has_t5 = (r.present_mask & (uint)LAB_MASK_T5) != 0;
const bool has_t6 = (r.present_mask & (uint)LAB_MASK_T6) != 0;
const bool deal_expected = (op == LAB_OP_MARKET_OPEN || op == LAB_OP_POSITION_CLOSE);
//--- шаг 1: применимость
ENUM_LAB_APPLICABILITY app = LAB_APP_APPLICABLE;
switch(metric)
{
case LAB_METRIC_CALL_DURATION:
case LAB_METRIC_REQUEST_DELAY:
case LAB_METRIC_FINAL_STATE_DELAY:
case LAB_METRIC_REMAINING_AFTER_RETURN:
app = LAB_APP_APPLICABLE;
break;
case LAB_METRIC_ORDER_DELAY:
if(op == LAB_OP_PENDING_DELETE)
app = LAB_APP_NOT_APPLICABLE; // для DELETE ORDER_ADD не ожидается
else
if(oc == LAB_OUT_REJECTED)
app = LAB_APP_NOT_APPLICABLE; // отказ без ордера
else
if(oc == LAB_OUT_UNKNOWN)
app = LAB_APP_UNKNOWN;
2026-09-12 22:14:24 +03:00
else
app = LAB_APP_APPLICABLE;
break;
case LAB_METRIC_FIRST_DEAL_DELAY:
case LAB_METRIC_LAST_DEAL_DELAY:
if(op == LAB_OP_PENDING_CREATE && oc != LAB_OUT_UNEXPECTED_ACTIVATION)
app = LAB_APP_NOT_APPLICABLE; // нормальное размещение без сделки
else
if(oc == LAB_OUT_REJECTED)
2026-09-12 22:14:24 +03:00
app = LAB_APP_NOT_APPLICABLE;
else
if(!deal_expected && oc != LAB_OUT_UNEXPECTED_ACTIVATION)
2026-09-12 22:14:24 +03:00
app = LAB_APP_NOT_APPLICABLE;
//--- R7-S1: доказанное отсутствие исполнения — сделок не было,
//--- метрика неприменима, а НЕ «потеря callback» (MISSING).
2026-09-12 22:14:24 +03:00
else
if(oc == LAB_OUT_CANCELED_NO_FILL || oc == LAB_OUT_EXPIRED_NO_FILL ||
oc == LAB_OUT_PENDING_DELETED)
app = LAB_APP_NOT_APPLICABLE;
//--- R7-S1: неустановленный исход при ОТСУТСТВУЮЩЕЙ сделке —
//--- применимость не определена (отдельная UNKNOWN-категория,
//--- а не APPLICABLE/MISSING); при наблюдённой сделке метрика
//--- измерима и остаётся APPLICABLE.
2026-09-12 22:14:24 +03:00
else
if(oc == LAB_OUT_UNKNOWN)
{
const bool deal_seen = (metric == LAB_METRIC_FIRST_DEAL_DELAY ? has_t4 :
(metric == LAB_METRIC_LAST_DEAL_DELAY ? has_t5 :
has_t4));
app = (deal_seen ? LAB_APP_APPLICABLE : LAB_APP_UNKNOWN);
}
2026-09-12 22:14:24 +03:00
else
app = LAB_APP_APPLICABLE;
break;
case LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET:
if(!deal_expected && oc != LAB_OUT_UNEXPECTED_ACTIVATION)
app = LAB_APP_NOT_APPLICABLE;
2026-09-12 22:14:24 +03:00
else
if(oc == LAB_OUT_REJECTED)
app = LAB_APP_NOT_APPLICABLE;
//--- R7-S1: без исполнения offset неприменим;
//--- UNKNOWN-исход с отсутствующей сделкой — применимость
//--- неопределена; при наблюдённой T4 offset измерим.
2026-09-12 22:14:24 +03:00
else
if(oc == LAB_OUT_CANCELED_NO_FILL || oc == LAB_OUT_EXPIRED_NO_FILL ||
oc == LAB_OUT_PENDING_DELETED)
app = LAB_APP_NOT_APPLICABLE;
2026-09-12 22:14:24 +03:00
else
if(oc == LAB_OUT_UNKNOWN)
app = (has_t4 ? LAB_APP_APPLICABLE : LAB_APP_UNKNOWN);
2026-09-12 22:14:24 +03:00
else
app = LAB_APP_APPLICABLE;
break;
}
out.applicability = app;
if(app != LAB_APP_APPLICABLE)
{
evaluated = true;
return(true);
}
//--- шаг 2: значение из точек
ulong start = 0, end = 0;
switch(metric)
{
case LAB_METRIC_CALL_DURATION:
start = r.sample.send_start_us;
end = r.sample.send_return_us;
break;
case LAB_METRIC_REQUEST_DELAY:
start = r.sample.send_start_us;
end = r.sample.request_event_us;
break;
case LAB_METRIC_ORDER_DELAY:
start = r.sample.send_start_us;
end = r.sample.order_event_us;
break;
case LAB_METRIC_FIRST_DEAL_DELAY:
start = r.sample.send_start_us;
end = r.sample.first_deal_us;
break;
case LAB_METRIC_LAST_DEAL_DELAY:
start = r.sample.send_start_us;
end = r.last_deal_us;
break;
case LAB_METRIC_FINAL_STATE_DELAY:
start = r.sample.send_start_us;
end = r.sample.final_event_us;
break;
case LAB_METRIC_REMAINING_AFTER_RETURN:
start = r.sample.send_return_us;
end = r.sample.final_event_us;
break;
case LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET:
start = r.sample.request_event_us;
end = r.sample.first_deal_us;
break;
}
const bool start_present =
(metric == LAB_METRIC_CALL_DURATION && has_t0) || (metric == LAB_METRIC_REQUEST_DELAY && has_t0) ||
(metric == LAB_METRIC_ORDER_DELAY && has_t0) || (metric == LAB_METRIC_FIRST_DEAL_DELAY && has_t0) ||
(metric == LAB_METRIC_LAST_DEAL_DELAY && has_t0) || (metric == LAB_METRIC_FINAL_STATE_DELAY && has_t0) ||
(metric == LAB_METRIC_REMAINING_AFTER_RETURN && has_t1) ||
(metric == LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET && has_t2);
const bool end_present =
(metric == LAB_METRIC_CALL_DURATION && has_t1) || (metric == LAB_METRIC_REQUEST_DELAY && has_t2) ||
(metric == LAB_METRIC_ORDER_DELAY && has_t3) || (metric == LAB_METRIC_FIRST_DEAL_DELAY && has_t4) ||
(metric == LAB_METRIC_LAST_DEAL_DELAY && has_t5) || (metric == LAB_METRIC_FINAL_STATE_DELAY && has_t6) ||
(metric == LAB_METRIC_REMAINING_AFTER_RETURN && has_t6) ||
(metric == LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET && has_t4);
out.start_us = start;
out.end_us = end;
//--- R5-B4: T5 полная только при подтверждённом покрытии всех сделок
//--- локальными callback-метками; восстановленные из истории сделки не
//--- дают времени предыдущего callback в качестве полного T5.
if(metric == LAB_METRIC_LAST_DEAL_DELAY && !r.callback_coverage_complete && has_t5)
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
{
out.has_value = false;
out.category = LAB_FIT_MISSING;
out.reason_codes = "PARTIAL_CALLBACK_COVERAGE";
evaluated = true;
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
return(true);
}
if(start_present && end_present)
{
long value = 0;
if(metric == LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET)
value = SignedDelta(start, end);
else
value = (long)(end - start);
//--- отрицательная длительность — невозможный исход, конфликт
if(metric != LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET && value < 0)
{
out.category = LAB_FIT_CONFLICT;
out.reason_codes = "TIMESTAMP_ORDER_VIOLATION";
evaluated = true;
return(true);
}
out.value_us = value;
out.has_value = true;
}
//--- шаг 3: категория пригодности (R5-B3). Своевременность оценивается
//--- по фактическому КОНЦУ метрики относительно зафиксированного
//--- observation_deadline_us, а не по порядку callbacks и ближайшего
//--- timer. Ранние точки (T4 etc.) остаются VALID даже при позднем T6
//--- или его отсутствии; поздние точки (T2/T6 после срока) — LATE.
//--- R6-B3: для знаковой T2-T4 своевременность требует ОБЕ точки:
//--- max(T2,T4)<=срок; перестановка знака не меняет критерий.
const ulong late_ref = (metric == LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET ?
(start > end ? start : end) : end);
const bool late_by_deadline = (out.has_value && r.observation_deadline_us > 0 &&
late_ref > r.observation_deadline_us);
if(r.conflict && out.has_value)
out.category = LAB_FIT_CONFLICT;
else
if(r.measurement_interrupted && out.has_value)
out.category = LAB_FIT_INTERRUPTED;
else
if(r.confirmation_invalidated && out.has_value)
out.category = LAB_FIT_CONFLICT;
else
if(!start_present || !end_present)
out.category = LAB_FIT_MISSING;
else
if(late_by_deadline)
out.category = LAB_FIT_LATE;
else
if(out.has_value)
out.category = LAB_FIT_VALID;
else
out.category = LAB_FIT_MISSING;
evaluated = true;
return(true);
}
bool CRequestTracker::RegisterHistoryDeal(const ulong deal_ticket, const ulong order_ticket,
const double volume, const long deal_time_msc, const ulong owner_sequence)
{
int di = FindDeal(deal_ticket);
if(di >= 0)
{
//--- R6-B1: известная callback-сделка ДОПОЛНЯЕТСЯ историческими
//--- атрибутами без потери callback-происхождения (два независимых
//--- источника по одной сделке объединяются).
if(m_deals[di].order_ticket == 0)
m_deals[di].order_ticket = order_ticket;
if(m_deals[di].owner_sequence == 0)
m_deals[di].owner_sequence = owner_sequence;
if(m_deals[di].history_volume == 0.0)
m_deals[di].history_volume = volume;
if(deal_time_msc != 0)
m_deals[di].deal_time_msc = deal_time_msc;
return(true);
}
if(m_deal_count < m_capacity)
{
m_deal_count++;
m_deals[m_deal_count - 1].Zero();
m_deals[m_deal_count - 1].deal_ticket = deal_ticket;
m_deals[m_deal_count - 1].order_ticket = order_ticket;
m_deals[m_deal_count - 1].owner_sequence = owner_sequence;
m_deals[m_deal_count - 1].history_volume = volume;
m_deals[m_deal_count - 1].deal_time_msc = deal_time_msc;
m_deals[m_deal_count - 1].recovered = true;
return(true);
}
//--- R7-S4: историческая регистрация при нехватке ёмкости — тоже
//--- потеря первичного факта (симметрия с MarkDealAdd).
m_deal_lost_count++;
return(false);
}
bool CRequestTracker::PushUnresolved(const uint request_id)
{
if(m_unresolved_count >= ArraySize(m_unresolved_request_ids))
{
const int n = ArraySize(m_unresolved_request_ids) + 64;
if(ArrayResize(m_unresolved_request_ids, n) != n)
return(false);
}
m_unresolved_request_ids[m_unresolved_count] = request_id;
m_unresolved_count++;
return(true);
}
int CRequestTracker::FindByRequestId(const uint request_id) const
{
if(request_id == 0)
return(-1);
for(int i = 0; i < m_count; i++)
if(m_records[i].has_request_id && m_records[i].sample.request_id == request_id)
return(i);
return(-1);
}
int CRequestTracker::FindByOrderTicket(const ulong ticket) const
{
if(ticket == 0)
return(-1);
for(int i = 0; i < m_count; i++)
if(m_records[i].order_ticket == ticket)
return(i);
return(-1);
}
ulong CRequestTracker::FindDealOwnerSequence(const ulong deal_ticket) const
{
for(int i = 0; i < m_deal_count; i++)
if(m_deals[i].deal_ticket == deal_ticket)
return(m_deals[i].owner_sequence);
return(0);
}
bool CRequestTracker::PushDeferred(const ulong entry_us, const long trans_type,
const ulong order_ticket, const ulong deal_ticket,
const ulong position_ticket, const double volume)
{
if(m_deferred_count >= ArraySize(m_deferred))
{
const int n = ArraySize(m_deferred) + 256;
if(ArrayResize(m_deferred, n) != n)
return(false);
}
DeferredTxn t;
t.entry_us = entry_us;
t.trans_type = trans_type;
t.order_ticket = order_ticket;
t.deal_ticket = deal_ticket;
t.position_ticket = position_ticket;
t.volume = volume;
t.bound = false;
m_deferred[m_deferred_count] = t;
m_deferred_count++;
return(true);
}
int CRequestTracker::ReconcileDeferred(const ulong sequence)
{
const int idx = FindBySequence(sequence);
if(idx < 0)
return(0);
const ulong rec_order = m_records[idx].order_ticket;
if(rec_order == 0)
return(0);
int bound = 0;
for(int i = 0; i < m_deferred_count; i++)
{
if(m_deferred[i].bound)
continue;
if(m_deferred[i].order_ticket != rec_order)
continue;
if(m_deferred[i].trans_type == TRADE_TRANSACTION_ORDER_ADD)
MarkOrderAdd(sequence, m_deferred[i].entry_us,
m_deferred[i].order_ticket, "DEFERRED_ORDER");
2026-09-12 22:14:24 +03:00
else
if(m_deferred[i].trans_type == TRADE_TRANSACTION_DEAL_ADD)
{
MarkDealAdd(sequence, m_deferred[i].entry_us, m_deferred[i].deal_ticket,
m_deferred[i].volume);
if(m_deferred[i].position_ticket != 0)
SetPositionTicket(sequence, m_deferred[i].position_ticket);
}
m_deferred[i].bound = true;
bound++;
}
return(bound);
}
ulong CRequestTracker::ResolveEventSequence(const bool request_event, const uint request_id,
const ulong order_ticket, const ulong deal_ticket) const
{
//--- аудит-2 B2: без точного совпадения событие не привязывается к
//--- записи (fallback-догадка отменена); неоднозначное сохраняется как
//--- unresolved, LatencySample не изменяется до точного разрешения
if(request_event)
{
const int by_req = FindByRequestId(request_id);
if(by_req >= 0)
return(m_records[by_req].key.sequence);
return(0);
}
const ulong by_deal = FindDealOwnerSequence(deal_ticket);
if(by_deal > 0)
return(by_deal);
const int by_order = FindByOrderTicket(order_ticket);
if(by_order >= 0)
return(m_records[by_order].key.sequence);
return(0);
}
bool CRequestTracker::SetPositionTicket(const ulong sequence, const ulong ticket)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
if(ticket != 0 && m_records[i].position_ticket == 0)
m_records[i].position_ticket = ticket;
return(true);
}
bool CRequestTracker::GraceExpired(const ulong sequence, const ulong now_us) const
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
return(m_records[i].collection_deadline_us > 0 &&
now_us >= m_records[i].collection_deadline_us);
}
bool CRequestTracker::SetTimerInterval(const ulong sequence, const ulong actual_us)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
m_records[i].last_timer_actual_us = actual_us;
return(true);
}
bool CRequestTracker::SetDeadlineExceeded(const ulong sequence)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
if(!m_records[i].deadline_exceeded)
m_records[i].deadline_exceeded = true;
return(true);
}
bool CRequestTracker::SetTradingStateKnown(const ulong sequence, const bool known)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
m_records[i].trading_state_known = known;
return(true);
}
bool CRequestTracker::SetDealCoverageComplete(const ulong sequence)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
m_records[i].deal_coverage_complete = true;
return(true);
}
bool CRequestTracker::RecordOutcome(const ulong sequence, const ENUM_LAB_OUTCOME outcome)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
if((m_records[i].present_mask & (uint)LAB_MASK_T6) != 0)
return(false); // T6 уже есть — исход не переписываем
m_records[i].outcome = outcome;
m_records[i].outcome_at_final = outcome;
return(true);
}
bool CRequestTracker::SyncReconcile(const ulong sequence, const ConfirmationSnapshot &snap,
const double volume_step, const double tolerance)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
m_records[i].executed_volume = snap.executed_volume_units;
//--- R6-B1: history_volume наполняется ТОЛЬКО полным сканом истории;
//--- световая сверка не смешивает callback-объём с историческим.
if(!snap.light_snapshot && volume_step > 0.0 && snap.executed_volume_units > 0.0)
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
{
const double ratio = snap.executed_volume_units * volume_step;
if(MathIsValidNumber(ratio))
m_records[i].history_volume = ratio;
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
}
if(snap.history_scan_failed)
{
//--- скан истории не удался: покрытие не подтверждается
m_records[i].deal_coverage_complete = false;
}
else
if(snap.coverage_complete)
m_records[i].deal_coverage_complete = true;
//--- R5-B4/R6-B1: полнота локальных callback-меток. Вид сверки
//--- определяется ЯВНЫМ признаком light_snapshot (не deal_count):
//--- световая — по совпадению callback-объёма с запрошенным; полная —
//--- по уникальным тикетам с has_callback.
if(snap.light_snapshot)
{
m_records[i].callback_coverage_complete =
(snap.coverage_complete && snap.executed_volume_units > 0.0);
}
else
if(!snap.history_scan_failed)
{
//--- R7-B2: полнота callback-меток подтверждается ТОЛЬКО полным
//--- сканом истории; снимок с ошибкой/обрезанием не доказывает
//--- полноту T5 по подмножеству тикетов (бит не повышается).
bool all_observed = (snap.deal_count > 0);
for(int k = 0; k < snap.deal_count && all_observed; k++)
{
const int di = FindDeal(snap.deal_tickets[k]);
if(di < 0 || !m_deals[di].has_callback)
all_observed = false;
}
m_records[i].callback_coverage_complete = all_observed;
}
//--- history_scan_failed: уже подтверждённая полным снимком полнота
//--- не снимается (ошибочный скан не доказывает отсутствие меток).
return(true);
}
bool CRequestTracker::SetDeadlinesFromSample(const ulong sequence, const ulong timeout_us,
const ulong grace_us)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
const ulong t0 = m_records[i].sample.send_start_us;
if(ULONG_MAX - t0 < timeout_us)
return(false);
m_records[i].observation_deadline_us = t0 + timeout_us;
if(ULONG_MAX - t0 >= timeout_us + grace_us)
m_records[i].collection_deadline_us = t0 + timeout_us + grace_us;
else
m_records[i].collection_deadline_us = ULONG_MAX;
return(true);
}
bool CRequestTracker::SetMarketContext(const ulong sequence, const double bid, const double ask,
const long tick_time_msc)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
m_records[i].bid_at_send = bid;
m_records[i].ask_at_send = ask;
m_records[i].spread_at_send = (bid > 0.0 && ask > 0.0 ? ask - bid : 0.0);
m_records[i].tick_time_msc = tick_time_msc;
return(true);
}
bool CRequestTracker::SetHandlerDuration(const ulong sequence, const ulong duration_us)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
m_records[i].last_handler_duration_us = duration_us;
return(true);
}
bool CRequestTracker::SetMarketRegime(const ulong sequence, const ENUM_LAB_MARKET_REGIME regime,
const string window_id, const string calibration_id, const ulong window_seq)
{
const int i = FindBySequence(sequence);
if(i < 0)
return(false);
m_records[i].market_regime = regime;
if(StringLen(window_id) > 0)
m_records[i].market_window_id = window_id;
if(StringLen(calibration_id) > 0)
m_records[i].calibration_id = calibration_id;
if(window_seq != 0) // R8-S2: связь sample->окно E3
m_records[i].market_window_seq = window_seq;
return(true);
}
int CRequestTracker::FindBySequence(const ulong sequence) const
{
for(int i = 0; i < m_count; i++)
if(m_records[i].key.sequence == sequence)
return(i);
return(-1);
}
int CRequestTracker::FindDeal(const ulong ticket) const
{
for(int i = 0; i < m_deal_count; i++)
if(m_deals[i].deal_ticket == ticket)
return(i);
return(-1);
}
#endif // REQUEST_LATENCY_LAB_REQUEST_TRACKER_MQH
//+------------------------------------------------------------------+