//+------------------------------------------------------------------+ //| 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 { RequestKey key; RequestPlan plan; LatencySample sample; //--- маски наличия/применимости точек; бит 1<= 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; 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) app = LAB_APP_NOT_APPLICABLE; else if(!deal_expected && oc != LAB_OUT_UNEXPECTED_ACTIVATION) app = LAB_APP_NOT_APPLICABLE; //--- R7-S1: доказанное отсутствие исполнения — сделок не было, //--- метрика неприменима, а НЕ «потеря callback» (MISSING). 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. 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); } 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; else if(oc == LAB_OUT_REJECTED) app = LAB_APP_NOT_APPLICABLE; //--- R7-S1: без исполнения offset неприменим; //--- UNKNOWN-исход с отсутствующей сделкой — применимость //--- неопределена; при наблюдённой T4 offset измерим. else if(oc == LAB_OUT_CANCELED_NO_FILL || oc == LAB_OUT_EXPIRED_NO_FILL || oc == LAB_OUT_PENDING_DELETED) app = LAB_APP_NOT_APPLICABLE; else if(oc == LAB_OUT_UNKNOWN) app = (has_t4 ? LAB_APP_APPLICABLE : LAB_APP_UNKNOWN); 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) { out.has_value = false; out.category = LAB_FIT_MISSING; out.reason_codes = "PARTIAL_CALLBACK_COVERAGE"; evaluated = true; 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"); 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) { const double ratio = snap.executed_volume_units * volume_step; if(MathIsValidNumber(ratio)) m_records[i].history_volume = ratio; } 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 //+------------------------------------------------------------------+