1144 linhas
50 KiB
MQL5
1144 linhas
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
|
|
{
|
|
RequestKey key;
|
|
RequestPlan plan;
|
|
LatencySample sample;
|
|
//--- маски наличия/применимости точек; бит 1<<index, включая T5
|
|
uint present_mask;
|
|
uint applicable_mask;
|
|
uint applicability_known_mask;
|
|
bool has_request_id;
|
|
bool has_retcode;
|
|
//--- результат отправки
|
|
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;
|
|
//--- сделки
|
|
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-меток
|
|
//--- качество
|
|
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;
|
|
//--- диагностика
|
|
bool is_warmup;
|
|
ulong last_handler_duration_us;
|
|
ulong last_timer_actual_us;
|
|
//--- метрики (индекс = ENUM_LAB_METRIC)
|
|
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:
|
|
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;
|
|
//--- B2 (audit-3): отложенные ORDER_ADD/DEAL_ADD до точного моста
|
|
struct DeferredTxn
|
|
{
|
|
ulong entry_us;
|
|
long trans_type;
|
|
ulong order_ticket;
|
|
ulong deal_ticket;
|
|
ulong position_ticket;
|
|
double volume;
|
|
bool bound;
|
|
};
|
|
DeferredTxn m_deferred[];
|
|
int m_deferred_count;
|
|
|
|
public:
|
|
CRequestTracker(void);
|
|
bool Init(const int capacity, const string session_id);
|
|
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;
|
|
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
|
|
//+------------------------------------------------------------------+
|