fixtureqa 0.4.19__tar.gz → 0.4.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. {fixtureqa-0.4.19/fixtureqa.egg-info → fixtureqa-0.4.20}/PKG-INFO +1 -1
  2. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/app.py +1 -1
  3. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/perf.py +15 -15
  4. fixtureqa-0.4.19/fixture/core/exec_csv_writer.py → fixtureqa-0.4.20/fixture/core/latency_csv_writer.py +5 -5
  5. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/perf_charts.py +1 -1
  6. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/perf_engine.py +25 -25
  7. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/perf_models.py +9 -5
  8. fixtureqa-0.4.19/fixture/static/assets/index-BSuHxk6b.js → fixtureqa-0.4.20/fixture/static/assets/index-BhxUj8V-.js +2 -2
  9. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/static/index.html +1 -1
  10. {fixtureqa-0.4.19 → fixtureqa-0.4.20/fixtureqa.egg-info}/PKG-INFO +1 -1
  11. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixtureqa.egg-info/SOURCES.txt +2 -2
  12. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/pyproject.toml +1 -1
  13. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_perf_api.py +22 -14
  14. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_perf_engine.py +27 -27
  15. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_perf_models.py +10 -0
  16. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/LICENSE +0 -0
  17. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/README.md +0 -0
  18. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/__init__.py +0 -0
  19. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/__main__.py +0 -0
  20. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/__init__.py +0 -0
  21. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/connection_manager.py +0 -0
  22. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/deps.py +0 -0
  23. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/__init__.py +0 -0
  24. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/admin.py +0 -0
  25. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/auth.py +0 -0
  26. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/branding.py +0 -0
  27. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/custom_tags.py +0 -0
  28. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/fix_spec.py +0 -0
  29. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/messages.py +0 -0
  30. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/scenarios.py +0 -0
  31. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/sessions.py +0 -0
  32. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/setup.py +0 -0
  33. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/spec_overlay.py +0 -0
  34. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/templates.py +0 -0
  35. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/routers/ws.py +0 -0
  36. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/api/schemas.py +0 -0
  37. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/config/__init__.py +0 -0
  38. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/__init__.py +0 -0
  39. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/atomic_io.py +0 -0
  40. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/auth.py +0 -0
  41. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/config_store.py +0 -0
  42. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/custom_tag_store.py +0 -0
  43. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/db_migrations.py +0 -0
  44. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/events.py +0 -0
  45. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/fix_application.py +0 -0
  46. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/fix_builder.py +0 -0
  47. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/fix_parser.py +0 -0
  48. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/fix_spec_parser.py +0 -0
  49. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/fix_tags.py +0 -0
  50. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/fix_time.py +0 -0
  51. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/housekeeping.py +0 -0
  52. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/inbound.py +0 -0
  53. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/json_store.py +0 -0
  54. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/message_log.py +0 -0
  55. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/message_store.py +0 -0
  56. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/models.py +0 -0
  57. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/perf_payload.py +0 -0
  58. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/perf_stats.py +0 -0
  59. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/perf_store.py +0 -0
  60. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/perf_writer.py +0 -0
  61. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/scenario_runner.py +0 -0
  62. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/scenario_store.py +0 -0
  63. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/session.py +0 -0
  64. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/session_manager.py +0 -0
  65. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/spec_overlay_store.py +0 -0
  66. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/template_store.py +0 -0
  67. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/user_store.py +0 -0
  68. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/core/venue_responses.py +0 -0
  69. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/fix_specs/FIX42.xml +0 -0
  70. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/fix_specs/FIX44.xml +0 -0
  71. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/server.py +0 -0
  72. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  73. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  74. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/static/assets/index-o_l1KBYd.css +0 -0
  75. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  76. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/static/favicon.svg +0 -0
  77. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixture/ui/__init__.py +0 -0
  78. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixtureqa.egg-info/dependency_links.txt +0 -0
  79. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixtureqa.egg-info/entry_points.txt +0 -0
  80. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixtureqa.egg-info/requires.txt +0 -0
  81. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/fixtureqa.egg-info/top_level.txt +0 -0
  82. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/setup.cfg +0 -0
  83. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_atomic_io.py +0 -0
  84. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_auth.py +0 -0
  85. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_config_store.py +0 -0
  86. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_connection_manager.py +0 -0
  87. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_db_migrations.py +0 -0
  88. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_fix_builder.py +0 -0
  89. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_health.py +0 -0
  90. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_inbound.py +0 -0
  91. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_inbound_validation.py +0 -0
  92. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_message_store.py +0 -0
  93. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_perf_payload.py +0 -0
  94. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_perf_rehydrate.py +0 -0
  95. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_scenarios.py +0 -0
  96. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_session_lifecycle.py +0 -0
  97. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_session_manager_concurrency.py +0 -0
  98. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_sessions.py +0 -0
  99. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_templates.py +0 -0
  100. {fixtureqa-0.4.19 → fixtureqa-0.4.20}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.4.19
3
+ Version: 0.4.20
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -91,7 +91,7 @@ def create_app(
91
91
  perf_writer = PerfWriter(os.path.join(data_dir, "perf.db"))
92
92
  app.state.perf_writer = perf_writer
93
93
  app.state.perf_registry = RunRegistry(
94
- perf_writer, execs_dir=os.path.join(data_dir, "perf_execs"))
94
+ perf_writer, latency_dir=os.path.join(data_dir, "perf_latency"))
95
95
  # Runs left non-terminal in perf.db were interrupted by a prior restart —
96
96
  # mark them errored so History doesn't show zombie 'running' rows.
97
97
  app.state.perf_registry.reconcile_interrupted()
@@ -180,19 +180,19 @@ def run_export(run_id: str, reg: REG, pw: PW, _admin: Admin,
180
180
  _csv_stream(pw, "perf_scenarios", pw.scenario_columns(), run_id),
181
181
  media_type="text/csv",
182
182
  headers={"Content-Disposition": f'attachment; filename="{run_id}_scenarios.csv"'})
183
- if kind == "execs":
184
- # CSV-direct: the run streams per-exec rows straight into this csv.gz; the
185
- # gzip footer is only written when the run finishes, so a live file would
186
- # download truncated.
183
+ if kind in ("latency", "execs"): # "execs" kept for back-compat
184
+ # CSV-direct: the run streams per-message rows straight into this csv.gz;
185
+ # the gzip footer is only written when the run finishes, so a live file
186
+ # would download truncated.
187
187
  if _run_is_live(reg, run_id):
188
188
  raise HTTPException(status_code=409,
189
- detail="Run still active — the exec CSV is finalized when the run finishes")
190
- path = reg.exec_csv_path(run_id)
189
+ detail="Run still active — the latency CSV is finalized when the run finishes")
190
+ path = reg.latency_csv_path(run_id)
191
191
  if path is None:
192
192
  raise HTTPException(status_code=404,
193
- detail="No exec recording for this run (enable record_execs before starting)")
193
+ detail="No per-message recording for this run (enable record_latency before starting)")
194
194
  return FileResponse(path, media_type="application/gzip",
195
- filename=f"{run_id}_execs.csv.gz")
195
+ filename=f"{run_id}_latency.csv.gz")
196
196
  if kind == "both":
197
197
  buf = io.BytesIO()
198
198
  with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
@@ -200,14 +200,14 @@ def run_export(run_id: str, reg: REG, pw: PW, _admin: Admin,
200
200
  _csv_text(pw, "perf_messages", pw.message_columns(), run_id))
201
201
  z.writestr(f"{run_id}_scenarios.csv",
202
202
  _csv_text(pw, "perf_scenarios", pw.scenario_columns(), run_id))
203
- execs_path = reg.exec_csv_path(run_id)
204
- if execs_path is not None and not _run_is_live(reg, run_id):
203
+ latency_path = reg.latency_csv_path(run_id)
204
+ if latency_path is not None and not _run_is_live(reg, run_id):
205
205
  # Already gzipped — store as-is rather than re-deflating.
206
- z.write(execs_path, arcname=f"{run_id}_execs.csv.gz",
206
+ z.write(latency_path, arcname=f"{run_id}_latency.csv.gz",
207
207
  compress_type=zipfile.ZIP_STORED)
208
208
  return Response(buf.getvalue(), media_type="application/zip",
209
209
  headers={"Content-Disposition": f'attachment; filename="{run_id}.zip"'})
210
- raise HTTPException(status_code=422, detail="kind must be messages|scenarios|execs|both")
210
+ raise HTTPException(status_code=422, detail="kind must be messages|scenarios|latency|both")
211
211
 
212
212
 
213
213
  @router.get("/runs/{run_id}/chart")
@@ -215,7 +215,7 @@ async def run_chart(run_id: str, reg: REG, _admin: Admin,
215
215
  metric: str = Query("one_way"), agg: str = Query("both")):
216
216
  """Per-message latency chart (PNG) from the run's recorded csv.gz. Rendered
217
217
  off-loop on first request and cached per (metric, agg) — the CSV is immutable
218
- once the run finishes. Needs record_execs; 404 otherwise, 409 while live."""
218
+ once the run finishes. Needs record_latency; 404 otherwise, 409 while live."""
219
219
  if metric not in ("one_way", "round_trip", "both") or agg not in ("scatter", "percentile", "both"):
220
220
  raise HTTPException(status_code=422,
221
221
  detail="metric must be one_way|round_trip|both; agg must be scatter|percentile|both")
@@ -224,10 +224,10 @@ async def run_chart(run_id: str, reg: REG, _admin: Admin,
224
224
  if _run_is_live(reg, run_id):
225
225
  raise HTTPException(status_code=409,
226
226
  detail="Run still active — the latency chart is rendered when the run finishes")
227
- csv_path = reg.exec_csv_path(run_id)
227
+ csv_path = reg.latency_csv_path(run_id)
228
228
  if csv_path is None or not os.path.exists(csv_path):
229
229
  raise HTTPException(status_code=404,
230
- detail="No per-message recording for this run (enable record_execs before starting)")
230
+ detail="No per-message recording for this run (enable record_latency before starting)")
231
231
  # Single-leg runs (client/venue) derive one-way from the wire send time;
232
232
  # label it so the chart can't be misread as the single-clock measurement.
233
233
  st = reg.get_status(run_id)
@@ -1,10 +1,10 @@
1
1
  """
2
2
  Per-run gzipped CSV recorder for individual inbound correlated messages —
3
3
  every ExecutionReport (ack, fill, amend/cancel reply) plus OrderCancelReject
4
- (35=9), opt-in via test.record_execs.
4
+ (35=9), opt-in via test.record_latency.
5
5
 
6
6
  CSV-direct: there is no database staging — rows stream straight to
7
- data/perf_execs/<run_id>.csv.gz and export serves the finished file as-is.
7
+ data/perf_latency/<run_id>.csv.gz and export serves the finished file as-is.
8
8
  Same non-blocking producer contract as PerfWriter: the event loop only does a
9
9
  put_nowait (drop+count when the bounded queue is full); all formatting and
10
10
  gzip/disk work happens on one daemon writer thread, so recording cannot add
@@ -26,7 +26,7 @@ logger = logging.getLogger(__name__)
26
26
 
27
27
  DEFAULT_MAX_QUEUE = 200_000
28
28
 
29
- EXEC_COLS = (
29
+ LATENCY_COLS = (
30
30
  "run_id", "corr_id", "clordid", "exec_id",
31
31
  "kind", # message kind / chart series key (engine-assigned):
32
32
  # client→venue: order / amend / cancel
@@ -45,7 +45,7 @@ EXEC_COLS = (
45
45
  )
46
46
 
47
47
 
48
- class ExecCsvWriter:
48
+ class LatencyCsvWriter:
49
49
  def __init__(self, path: str, max_queue: int = DEFAULT_MAX_QUEUE):
50
50
  self.path = path
51
51
  os.makedirs(os.path.dirname(path), exist_ok=True)
@@ -79,7 +79,7 @@ class ExecCsvWriter:
79
79
  try:
80
80
  with gzip.open(self.path, "wt", newline="") as fh:
81
81
  w = csv.writer(fh)
82
- w.writerow(EXEC_COLS)
82
+ w.writerow(LATENCY_COLS)
83
83
  while True:
84
84
  item = self._queue.get()
85
85
  if item is None:
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Post-run per-message latency charts from a recorded run's csv.gz
3
- (test.record_execs). One plot line/series per message `kind` (ack / fill /
3
+ (test.record_latency). One plot line/series per message `kind` (ack / fill /
4
4
  replace / canceled / cancel-reject / …): x = order send time (run progression),
5
5
  y = per-message latency.
6
6
 
@@ -31,7 +31,7 @@ from typing import Optional
31
31
 
32
32
  from fixcore.message.message import Message
33
33
 
34
- from .exec_csv_writer import ExecCsvWriter
34
+ from .latency_csv_writer import LatencyCsvWriter
35
35
  from .fix_time import utc_timestamp
36
36
  from .perf_models import (
37
37
  RunConfig, RunStatus, LiveSnapshot, ClientLeg, VenueLeg, SnapshotErrors, LatencyStats,
@@ -49,7 +49,7 @@ T_ORDERQTY, T_ORDTYPE, T_PRICE, T_TIF, T_TRANSACTTIME = 38, 40, 44, 59, 60
49
49
  T_ORDERID, T_EXECID, T_EXECTRANSTYPE, T_EXECTYPE, T_ORDSTATUS = 37, 17, 20, 150, 39
50
50
  T_LASTQTY, T_LASTPX, T_CUMQTY, T_LEAVESQTY, T_AVGPX = 32, 31, 14, 151, 6
51
51
 
52
- # ExecType(150) → per-message latency-chart series label (see ExecCsvWriter).
52
+ # ExecType(150) → per-message latency-chart series label (see LatencyCsvWriter).
53
53
  _EXEC_KIND = {
54
54
  "0": "ack", "1": "fill", "2": "fill", "F": "fill",
55
55
  "E": "pending-replace", "5": "replace",
@@ -124,13 +124,13 @@ class PerfRun:
124
124
  def __init__(self, run_id: str, config: RunConfig, sm: SessionManager, writer: PerfWriter,
125
125
  template_store=None, owner_uid: str = "",
126
126
  sender_comp_id: str = "", target_comp_id: str = "",
127
- execs_dir: Optional[str] = None):
127
+ latency_dir: Optional[str] = None):
128
128
  self.run_id = run_id
129
129
  self.config = config
130
130
  self._sm = sm
131
131
  self._writer = writer
132
- self._execs_dir = execs_dir
133
- self._exec_csv: Optional[ExecCsvWriter] = None
132
+ self._latency_dir = latency_dir
133
+ self._latency_csv: Optional[LatencyCsvWriter] = None
134
134
 
135
135
  self.status: str = "pending"
136
136
  self.started_at: Optional[float] = None
@@ -209,14 +209,14 @@ class PerfRun:
209
209
  # message's monotonic recv_perf_ns can be expressed as a UTC wall time
210
210
  # (for the tag-60 latency path) without a second clock read per message.
211
211
  self._start_wall = time.time()
212
- if self.config.test.record_execs:
212
+ if self.config.test.record_latency:
213
213
  if not (self._has_client or self._has_venue):
214
- self.warnings.append("record_execs has no effect with no client or venue leg")
215
- elif not self._execs_dir:
216
- self.warnings.append("record_execs unavailable (no exec CSV directory configured)")
214
+ self.warnings.append("record_latency has no effect with no client or venue leg")
215
+ elif not self._latency_dir:
216
+ self.warnings.append("record_latency unavailable (no latency CSV directory configured)")
217
217
  else:
218
- self._exec_csv = ExecCsvWriter(
219
- os.path.join(self._execs_dir, f"{self.run_id}.csv.gz"))
218
+ self._latency_csv = LatencyCsvWriter(
219
+ os.path.join(self._latency_dir, f"{self.run_id}.csv.gz"))
220
220
  if not self._one_way:
221
221
  self.warnings.append(
222
222
  "single-leg run: per-message one-way latency uses the wire "
@@ -298,10 +298,10 @@ class PerfRun:
298
298
  self._client_sub.close()
299
299
  if self._venue_sub:
300
300
  self._venue_sub.close()
301
- if self._exec_csv is not None:
301
+ if self._latency_csv is not None:
302
302
  # Correlator is cancelled (no more enqueues); drain + gzip-finalize
303
303
  # off-loop — close() joins the writer thread.
304
- await asyncio.to_thread(self._exec_csv.close)
304
+ await asyncio.to_thread(self._latency_csv.close)
305
305
  self.status = "stopped" if self._stopped_by_user else "completed"
306
306
  self._build_snapshot()
307
307
  self._publish()
@@ -822,10 +822,10 @@ class PerfRun:
822
822
  send_at, recv_at, req_send_at, one_way_us, round_trip_us,
823
823
  exec_type="", ord_status="", cum_qty=None, leaves_qty=None) -> None:
824
824
  """Append one message-crossing row to the per-message latency CSV
825
- (opt-in via record_execs). Column order must match EXEC_COLS."""
826
- if self._exec_csv is None:
825
+ (opt-in via record_latency). Column order must match LATENCY_COLS."""
826
+ if self._latency_csv is None:
827
827
  return
828
- self._exec_csv.enqueue((
828
+ self._latency_csv.enqueue((
829
829
  self.run_id, corr, clordid, exec_id, kind, direction,
830
830
  send_at, recv_at, req_send_at, one_way_us, round_trip_us,
831
831
  exec_type, ord_status, cum_qty, leaves_qty,
@@ -836,7 +836,7 @@ class PerfRun:
836
836
  """Stamp a venue→client message's send time keyed by its ExecID, so the
837
837
  client correlator can compute one-way hub latency on receipt. No-op when
838
838
  not recording. Bounded: drop-oldest past the cap (age-swept too)."""
839
- if self._exec_csv is None:
839
+ if self._latency_csv is None:
840
840
  return
841
841
  eid = msg.get_field_or(self.config.exec_id_tag, "") or msg.get_field_or(T_EXECID, "")
842
842
  if not eid:
@@ -858,7 +858,7 @@ class PerfRun:
858
858
  inbound side. Manager/loopback: one-way = venue recv − the client's send
859
859
  (from _pending, one clock). Venue-only: one-way = our recv − the
860
860
  counterparty's wire send time (tag-60), needing a synced clock."""
861
- if self._exec_csv is None:
861
+ if self._latency_csv is None:
862
862
  return
863
863
  if self._one_way:
864
864
  entry = self._pending.get(corr)
@@ -888,7 +888,7 @@ class PerfRun:
888
888
  from the venue send registry (manager/loopback) or the counterparty's
889
889
  wire send time (tag-60, client-only — synced clock needed). round-trip:
890
890
  from the originating request's send in _pending (always our own clock)."""
891
- if self._exec_csv is None:
891
+ if self._latency_csv is None:
892
892
  return
893
893
  vs = self._venue_sends.pop(exec_id, None) if exec_id else None
894
894
  if vs:
@@ -1307,9 +1307,9 @@ class RunRegistry:
1307
1307
  TICKET_TTL_S = 30.0
1308
1308
  _TERMINAL_STATES = ("completed", "stopped", "error")
1309
1309
 
1310
- def __init__(self, writer: PerfWriter, execs_dir: Optional[str] = None):
1310
+ def __init__(self, writer: PerfWriter, latency_dir: Optional[str] = None):
1311
1311
  self._writer = writer
1312
- self._execs_dir = execs_dir
1312
+ self._latency_dir = latency_dir
1313
1313
  self._runs: dict[str, PerfRun] = {}
1314
1314
  self._tickets: dict[str, tuple[str, float]] = {}
1315
1315
 
@@ -1317,17 +1317,17 @@ class RunRegistry:
1317
1317
  owner_uid: str = "", sender_comp_id: str = "", target_comp_id: str = "") -> PerfRun:
1318
1318
  run_id = str(uuid.uuid4())
1319
1319
  run = PerfRun(run_id, config, sm, self._writer, template_store, owner_uid,
1320
- sender_comp_id, target_comp_id, execs_dir=self._execs_dir)
1320
+ sender_comp_id, target_comp_id, latency_dir=self._latency_dir)
1321
1321
  self._runs[run_id] = run
1322
1322
  run.start()
1323
1323
  return run
1324
1324
 
1325
- def exec_csv_path(self, run_id: str) -> Optional[str]:
1325
+ def latency_csv_path(self, run_id: str) -> Optional[str]:
1326
1326
  """Path to the run's per-exec csv.gz, or None if it never recorded.
1327
1327
  The file is complete (gzip footer written) only once the run is terminal."""
1328
- if not self._execs_dir:
1328
+ if not self._latency_dir:
1329
1329
  return None
1330
- path = os.path.join(self._execs_dir, f"{run_id}.csv.gz")
1330
+ path = os.path.join(self._latency_dir, f"{run_id}.csv.gz")
1331
1331
  return path if os.path.exists(path) else None
1332
1332
 
1333
1333
  def get(self, run_id: str) -> Optional[PerfRun]:
@@ -16,7 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  from typing import Literal, Optional
18
18
 
19
- from pydantic import BaseModel, Field, model_validator
19
+ from pydantic import AliasChoices, BaseModel, Field, model_validator
20
20
 
21
21
  RunMode = Literal["loopback", "manager", "client", "venue"]
22
22
  SaturationPolicy = Literal["pause", "drop_oldest"]
@@ -51,10 +51,14 @@ class TestConfig(BaseModel):
51
51
  max_pending: int = Field(default=100_000, gt=0)
52
52
  on_saturation: SaturationPolicy = "pause"
53
53
  output: str = "results"
54
- # Record every inbound ExecutionReport (ack + each fill) to a per-run
55
- # csv.gz for detailed latency analysis. Off by default: ~(1+fills_per_order)×
56
- # the per-order row volume on disk.
57
- record_execs: bool = False
54
+ # Record every message crossing (orders, amends, cancels + acks, fills,
55
+ # replies) to a per-run csv.gz the source for the per-message latency
56
+ # charts. On by default; ~(2+fills_per_order)× the per-order row volume on
57
+ # disk. Turn off for pure throughput runs that don't need the latency CSV.
58
+ # Accepts the legacy key `record_execs` so saved configs / run history from
59
+ # before the rename still load.
60
+ record_latency: bool = Field(
61
+ default=True, validation_alias=AliasChoices("record_latency", "record_execs"))
58
62
 
59
63
 
60
64
  class AmendConfig(BaseModel):