2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-10 16:43:28 +03:00 | | | //| RequestTracker.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_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;
|
2026-09-09 01:19:31 +03:00 | | | //--- маски наличия/применимости точек; бит 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-09 01:19:31 +03:00 | | | //--- результат отправки
|
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;
|
2026-09-09 01:19:31 +03:00 | | | //--- корреляция
|
| | | 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-09 01:19:31 +03:00 | | | //--- качество
|
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;
|
2026-09-09 01:19:31 +03:00 | | | //--- завершение
|
2026-09-10 16:43:28 +03:00 | | | ENUM_LAB_OUTCOME outcome;
|
| | | ENUM_LAB_OUTCOME outcome_at_final;
|
2026-09-09 01:19:31 +03:00 | | | ENUM_LAB_FINAL_SOURCE final_source;
|
2026-09-10 16:43:28 +03:00 | | | 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;
|
2026-09-09 01:19:31 +03:00 | | | //--- рынок
|
2026-09-10 16:43:28 +03:00 | | | double bid_at_send;
|
| | | double ask_at_send;
|
| | | double spread_at_send;
|
| | | long tick_time_msc;
|
| | | ulong quote_received_us;
|
| | | string market_window_id;
|
2026-09-11 18:16:32 +03:00 | | | ulong market_window_seq; // R8-S2: номер окна E3 (market_windows.sequence)
|
2026-09-09 01:19:31 +03:00 | | | ENUM_LAB_MARKET_REGIME market_regime;
|
2026-09-10 16:43:28 +03:00 | | | string calibration_id;
|
2026-09-09 01:19:31 +03:00 | | | //--- диагностика
|
2026-09-12 22:14:24 +03:00 | | | bool is_warmup;
|
| | | ulong last_handler_duration_us;
|
| | | ulong last_timer_actual_us;
|
2026-09-09 01:19:31 +03:00 | | | //--- метрики (индекс = 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];
|
2026-09-09 01:19:31 +03:00 | | |
|
2026-09-16 15:37:55 +03:00 | | | void Reset(const RequestPlan &p);
|
2026-09-09 01:19:31 +03:00 | | | };
|
| | | //+------------------------------------------------------------------+
|
| | | //| 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 | | |
|
2026-09-09 01:19:31 +03:00 | | | public:
|
2026-09-16 15:37:55 +03:00 | | | 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
|
2026-09-16 15:37:55 +03:00 | | | bool GetByIndex(const int index, RequestMetadata &out) const;
|
2026-09-09 01:19:31 +03:00 | | | //--- резервирование попытки до отправки (ТЗ §4)
|
2026-09-16 15:37:55 +03:00 | | | bool Reserve(const RequestPlan &plan, ulong &sequence);
|
2026-09-11 21:20:36 +03:00 | | | //--- R9-B1: откат НЕОТПРАВЛЕННОГО резерва (pre-T0 отказ: устаревший
|
| | | //--- контекст E3, переполнение журнала intent и т.п.). Удаляется запись,
|
| | | //--- в которую транспорт НЕ входил (нет T0) — слот планировщика мог быть
|
| | | //--- возвращён в очередь (MarkWaitSlot), и повтор той же sequence обязан
|
| | | //--- пройти без дубликата ключа. Фантомный MAIN в samples не экспортируется;
|
| | | //--- реестр сделок/события журнала этот резерв не ссылают.
|
2026-09-16 15:37:55 +03:00 | | | 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)
|
2026-09-11 21:20:36 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_records[i].order_ticket = send.result.order;
|
| | | m_records[i].correlation_method = "RESULT_ORDER";
|
2026-09-11 21:20:36 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(send.result.request_id != 0)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_records[i].sample.request_id = send.result.request_id;
|
| | | m_records[i].has_request_id = true;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_records[i].sample.request_id = request_id;
|
| | | m_records[i].has_request_id = true;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(result_order != 0 && m_records[i].order_ticket == 0)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_records[i].order_ticket = result_order;
|
| | | m_records[i].correlation_method = "REQUEST_ID_RESULT_ORDER";
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- result_deal - тикет сделки, а не идентификатор позиции; сюда не пишется (п.5 аудита)
|
| | | if(m_records[i].correlation_status == LAB_CORR_UNRESOLVED)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_records[i].correlation_status = LAB_CORR_EXACT;
|
| | | m_records[i].correlation_resolved_us = entry_us;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_records[i].correlation_status = LAB_CORR_EXACT;
|
| | | m_records[i].correlation_method = method;
|
| | | m_records[i].correlation_resolved_us = entry_us;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | //--- R6-S4: переполнение реестра сделок — явный отказ/качество,
|
| | | //--- а не тихий успех с потерей дедупликации тикета.
|
| | | if(m_deal_count >= m_capacity)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_deal_lost_count++;
|
| | | return(false);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | else
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | seen_before = true;
|
| | | //--- повтор DEAL_ADD: объём и T5 не сдвигаются дубликатом
|
| | | if(!m_deals[di].has_callback)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | //--- 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;
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- первое получение уникальной сделки обновляет T4/T5
|
| | | if(di >= 0 && m_deals[di].callback_first_us == entry_us)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if((m_records[i].present_mask & (uint)LAB_MASK_T4) == 0 ||
|
| | | entry_us < m_records[i].sample.first_deal_us)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_records[i].sample.first_deal_us = entry_us;
|
| | | m_records[i].present_mask |= (uint)LAB_MASK_T4;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if((m_records[i].present_mask & (uint)LAB_MASK_T5) == 0 ||
|
| | | entry_us > m_records[i].last_deal_us)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_records[i].last_deal_us = entry_us;
|
| | | m_records[i].present_mask |= (uint)LAB_MASK_T5;
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R6-B1: объём/счётчик растут на первой уникальной сделке И на первом
|
| | | //--- callback истории-восстановленной сделки (но не на дубликате).
|
| | | if(!seen_before || callback_new)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | 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++;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_records[i].collection_deadline_us = ULONG_MAX;
|
| | | return;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | 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; // отказ без ордера
|
2026-09-09 01:19:31 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(oc == LAB_OUT_UNKNOWN)
|
| | | app = LAB_APP_UNKNOWN;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | 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;
|
2026-09-09 01:19:31 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(!deal_expected && oc != LAB_OUT_UNEXPECTED_ACTIVATION)
|
2026-09-12 22:14:24 +03:00 | | | app = LAB_APP_NOT_APPLICABLE;
|
2026-09-16 15:37:55 +03:00 | | | //--- 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;
|
2026-09-16 15:37:55 +03:00 | | | //--- R7-S1: неустановленный исход при ОТСУТСТВУЮЩЕЙ сделке —
|
| | | //--- применимость не определена (отдельная UNKNOWN-категория,
|
| | | //--- а не APPLICABLE/MISSING); при наблюдённой сделке метрика
|
| | | //--- измерима и остаётся APPLICABLE.
|
2026-09-12 22:14:24 +03:00 | | | else
|
| | | if(oc == LAB_OUT_UNKNOWN)
|
2026-09-16 15:37:55 +03:00 | | | {
|
| | | 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;
|
2026-09-16 15:37:55 +03:00 | | | 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
|
2026-09-16 15:37:55 +03:00 | | | 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
|
2026-09-16 15:37:55 +03:00 | | | 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
|
2026-09-16 15:37:55 +03:00 | | | if(oc == LAB_OUT_UNKNOWN)
|
| | | app = (has_t4 ? LAB_APP_APPLICABLE : LAB_APP_UNKNOWN);
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | app = LAB_APP_APPLICABLE;
|
| | | break;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | out.applicability = app;
|
| | | if(app != LAB_APP_APPLICABLE)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | evaluated = true;
|
2026-09-10 16:43:28 +03:00 | | | return(true);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- шаг 2: значение из точек
|
| | | ulong start = 0, end = 0;
|
| | | switch(metric)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | 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;
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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 | | | {
|
2026-09-16 15:37:55 +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);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | if(start_present && end_present)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | 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)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | out.category = LAB_FIT_CONFLICT;
|
| | | out.reason_codes = "TIMESTAMP_ORDER_VIOLATION";
|
| | | evaluated = true;
|
| | | return(true);
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | out.value_us = value;
|
| | | out.has_value = true;
|
2026-09-10 19:04:10 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- шаг 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)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | //--- 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;
|
2026-09-10 16:43:28 +03:00 | | | return(true);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(m_deal_count < m_capacity)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | 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;
|
2026-09-10 16:43:28 +03:00 | | | return(true);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- 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))
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const int n = ArraySize(m_unresolved_request_ids) + 64;
|
| | | if(ArrayResize(m_unresolved_request_ids, n) != n)
|
2026-09-10 16:43:28 +03:00 | | | return(false);
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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))
|
2026-09-11 09:06:39 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const int n = ArraySize(m_deferred) + 256;
|
| | | if(ArrayResize(m_deferred, n) != n)
|
2026-09-11 09:06:39 +03:00 | | | return(false);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | 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++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | 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
|
2026-09-16 15:37:55 +03:00 | | | if(m_deferred[i].trans_type == TRADE_TRANSACTION_DEAL_ADD)
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | 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);
|
2026-09-11 04:09:32 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | m_deferred[i].bound = true;
|
| | | bound++;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const int by_req = FindByRequestId(request_id);
|
| | | if(by_req >= 0)
|
| | | return(m_records[by_req].key.sequence);
|
| | | return(0);
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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 | | | {
|
2026-09-16 15:37:55 +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 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(snap.history_scan_failed)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | //--- скан истории не удался: покрытие не подтверждается
|
| | | m_records[i].deal_coverage_complete = false;
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_records[i].callback_coverage_complete =
|
| | | (snap.coverage_complete && snap.executed_volume_units > 0.0);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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);
|
| | | }
|
2026-09-09 01:19:31 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | #endif // REQUEST_LATENCY_LAB_REQUEST_TRACKER_MQH
|
2026-09-16 15:37:55 +03:00 | | | //+------------------------------------------------------------------+
|