fixtureqa 0.4.25__tar.gz → 0.4.26__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.
- {fixtureqa-0.4.25/fixtureqa.egg-info → fixtureqa-0.4.26}/PKG-INFO +1 -1
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/perf.py +25 -21
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_engine.py +25 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_models.py +23 -0
- fixtureqa-0.4.25/fixture/static/assets/index-76KAxdQ5.js → fixtureqa-0.4.26/fixture/static/assets/index-C7Iff2Mt.js +23 -23
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/index.html +1 -1
- {fixtureqa-0.4.25 → fixtureqa-0.4.26/fixtureqa.egg-info}/PKG-INFO +1 -1
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixtureqa.egg-info/SOURCES.txt +1 -1
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/pyproject.toml +1 -1
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_perf_api.py +26 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_perf_engine.py +21 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_perf_models.py +26 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/LICENSE +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/README.md +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/__init__.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/__main__.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/app.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/connection_manager.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/deps.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/fix_spec.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/messages.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/sessions.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/routers/ws.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/api/schemas.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/auth.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/events.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_spec_parser.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/latency_csv_writer.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/models.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_charts.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_stats.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_store.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/scenario_runner.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/session.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/session_manager.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/fix_specs/FIX42.xml +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/fix_specs/FIX44.xml +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/server.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/assets/index-C9bcm_lN.css +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixtureqa.egg-info/requires.txt +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/setup.cfg +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_auth.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_config_store.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_health.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_inbound.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_message_store.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_session_auto_response.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_sessions.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_templates.py +0 -0
- {fixtureqa-0.4.25 → fixtureqa-0.4.26}/tests/test_ws.py +0 -0
|
@@ -17,7 +17,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSock
|
|
|
17
17
|
from fastapi.responses import FileResponse, Response, StreamingResponse
|
|
18
18
|
|
|
19
19
|
from ...core.models import SessionStatus
|
|
20
|
-
from ...core.perf_models import RunConfig, RunStatus
|
|
20
|
+
from ...core.perf_models import RunConfig, RunStatus, est_msgs_per_order
|
|
21
21
|
from ...core.perf_charts import render_latency_chart
|
|
22
22
|
from ...core.perf_engine import RunRegistry
|
|
23
23
|
from ...core.perf_payload import PerfConfigError
|
|
@@ -54,18 +54,6 @@ _TERMINAL = ("completed", "stopped", "error")
|
|
|
54
54
|
DURABLE_SAFE_MSG_RATE = 30 # messages/second a logged/durable session can sustain
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def _est_msgs_per_order(body: RunConfig) -> int:
|
|
58
|
-
"""Rough peak FIX message count generated per order, for the durability rate
|
|
59
|
-
guard: 35=D out + 35=8 ack + fills, plus a request+reply pair for each of the
|
|
60
|
-
amend / cancel flavours when enabled. Deliberately an upper-ish bound."""
|
|
61
|
-
n = 2 + body.payload.fill.fills_per_order # order + ack + fills
|
|
62
|
-
if body.amend.enabled:
|
|
63
|
-
n += 2 # 35=G + 35=8 replace
|
|
64
|
-
if body.cancel.enabled:
|
|
65
|
-
n += 2 # 35=F + 35=8/9 reply
|
|
66
|
-
return max(1, n)
|
|
67
|
-
|
|
68
|
-
|
|
69
57
|
def _order_rate_per_s(body: RunConfig) -> float:
|
|
70
58
|
r = body.rate
|
|
71
59
|
return r.orders_per_window * 1000.0 / r.order_window_ms
|
|
@@ -100,17 +88,32 @@ async def create_run(body: RunConfig, sm: SM, reg: REG, ts: TS, admin: Admin):
|
|
|
100
88
|
if need_venue:
|
|
101
89
|
participating.append(("venue", _require_started(sm, body.venue_session_id, "venue").config))
|
|
102
90
|
|
|
91
|
+
# Optional per-session msgs/s ceiling: validate the named session actually
|
|
92
|
+
# participates, then fold it into the effective order rate (the injector is
|
|
93
|
+
# throttled to min(configured, ceiling) — see PerfRun). mpo every message
|
|
94
|
+
# crosses the link, so each leg touches ~mpo messages per order.
|
|
95
|
+
participant_sids = {sid for sid in (body.client_session_id, body.venue_session_id) if sid}
|
|
96
|
+
csid = body.rate.ceiling_session_id
|
|
97
|
+
if body.rate.max_msgs_per_sec is not None and csid and csid not in participant_sids:
|
|
98
|
+
raise HTTPException(status_code=422, detail=(
|
|
99
|
+
f"rate.ceiling_session_id {csid!r} is not a session this run drives "
|
|
100
|
+
f"(participants: {', '.join(sorted(participant_sids)) or 'none'})"))
|
|
101
|
+
mpo = est_msgs_per_order(body)
|
|
102
|
+
effective_ops = _order_rate_per_s(body)
|
|
103
|
+
if body.rate.max_msgs_per_sec is not None:
|
|
104
|
+
effective_ops = min(effective_ops, body.rate.max_msgs_per_sec / mpo)
|
|
105
|
+
|
|
103
106
|
# Durability rate guard — BEFORE reg.create() (which starts the run). A
|
|
104
107
|
# session with durable_seqnums or log_messages persists every message inline
|
|
105
108
|
# on the event loop; above a low rate this stalls the loop and freezes the
|
|
106
|
-
# whole server.
|
|
107
|
-
#
|
|
109
|
+
# whole server. Evaluated on the EFFECTIVE rate, so a msgs/s ceiling that
|
|
110
|
+
# holds a logged session under the limit is a legitimate escape hatch (the run
|
|
111
|
+
# is allowed, just throttled) instead of being refused outright.
|
|
108
112
|
logged = [(label, cfg) for label, cfg in participating
|
|
109
113
|
if cfg.durable_seqnums or cfg.log_messages]
|
|
110
114
|
durable_warnings: list[str] = []
|
|
111
115
|
if logged:
|
|
112
|
-
|
|
113
|
-
est_msg_rate = _order_rate_per_s(body) * mpo
|
|
116
|
+
est_msg_rate = effective_ops * mpo
|
|
114
117
|
safe_order_rate = DURABLE_SAFE_MSG_RATE / mpo
|
|
115
118
|
|
|
116
119
|
def _flags(cfg) -> str:
|
|
@@ -122,13 +125,14 @@ async def create_run(body: RunConfig, sm: SM, reg: REG, ts: TS, admin: Admin):
|
|
|
122
125
|
if est_msg_rate > DURABLE_SAFE_MSG_RATE:
|
|
123
126
|
raise HTTPException(status_code=409, detail=(
|
|
124
127
|
f"This run would generate ~{est_msg_rate:.0f} msg/s "
|
|
125
|
-
f"(~{mpo} msgs/order × {
|
|
128
|
+
f"(~{mpo} msgs/order × {effective_ops:.0f} orders/s), but "
|
|
126
129
|
f"{who} persist every message inline on the event loop "
|
|
127
130
|
f"(fsync / messages.db write). Above ~{DURABLE_SAFE_MSG_RATE} msg/s "
|
|
128
131
|
f"this stalls the server. Either lower the order rate to "
|
|
129
|
-
f"≤ {safe_order_rate:.0f}/s
|
|
130
|
-
f"
|
|
131
|
-
f"
|
|
132
|
+
f"≤ {safe_order_rate:.0f}/s, set a per-session messages/sec ceiling "
|
|
133
|
+
f"that holds the logged session under ~{DURABLE_SAFE_MSG_RATE} msg/s, "
|
|
134
|
+
f"or convert the session(s) to a Performance session (durable_seqnums "
|
|
135
|
+
f"+ log_messages off) for high-rate runs."))
|
|
132
136
|
durable_warnings.append(
|
|
133
137
|
f"message persistence is on for {who}; safe only up to "
|
|
134
138
|
f"~{safe_order_rate:.0f} orders/s (~{DURABLE_SAFE_MSG_RATE} msg/s) — at "
|
|
@@ -35,6 +35,7 @@ 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,
|
|
38
|
+
est_msgs_per_order,
|
|
38
39
|
)
|
|
39
40
|
from .perf_payload import PayloadFactory, ScenarioSelector
|
|
40
41
|
from .perf_stats import BurstGate, PerfStats, TokenBucket
|
|
@@ -189,6 +190,17 @@ class PerfRun:
|
|
|
189
190
|
self._order_bucket = _pacer(r.orders_per_window, r.order_window_ms, r.dispatch)
|
|
190
191
|
self._fill_bucket = _pacer(r.fills_per_window, r.fill_window_ms, r.fill_dispatch)
|
|
191
192
|
|
|
193
|
+
# Optional per-session messages/sec ceiling: a second smooth bucket the
|
|
194
|
+
# injector acquires in series with _order_bucket, so the effective order
|
|
195
|
+
# rate is the min of the two. ceiling_ops = max_msgs_per_sec / est-msgs/order;
|
|
196
|
+
# expressed as TokenBucket(1, window) the rate is 1000/window_ms orders/s
|
|
197
|
+
# with capacity 1 (smooth, no over-capacity acquire(n) deadlock).
|
|
198
|
+
self._ceiling_bucket: Optional[TokenBucket] = None
|
|
199
|
+
if r.max_msgs_per_sec is not None:
|
|
200
|
+
mpo = est_msgs_per_order(config)
|
|
201
|
+
self._ceiling_ops = r.max_msgs_per_sec / mpo
|
|
202
|
+
self._ceiling_bucket = TokenBucket(1, max(1, round(1000 * mpo / r.max_msgs_per_sec)))
|
|
203
|
+
|
|
192
204
|
self._client_sub = None
|
|
193
205
|
self._venue_sub = None
|
|
194
206
|
self._tasks: list[asyncio.Task] = []
|
|
@@ -230,6 +242,15 @@ class PerfRun:
|
|
|
230
242
|
self.warnings.append(
|
|
231
243
|
"cancel injection has no effect in venue mode (no client leg); "
|
|
232
244
|
"inbound 35=F from the external client is still answered")
|
|
245
|
+
if self._ceiling_bucket is not None and self._has_client:
|
|
246
|
+
r = self.config.rate
|
|
247
|
+
order_ops = r.orders_per_window * 1000.0 / r.order_window_ms
|
|
248
|
+
if self._ceiling_ops < order_ops:
|
|
249
|
+
who = r.ceiling_session_id or "the chosen session"
|
|
250
|
+
self.warnings.append(
|
|
251
|
+
f"injector capped to ~{self._ceiling_ops:.0f} orders/s to hold "
|
|
252
|
+
f"{who} under {r.max_msgs_per_sec} msg/s (order rate "
|
|
253
|
+
f"{order_ops:.0f}/s would exceed it)")
|
|
233
254
|
if self._has_client:
|
|
234
255
|
self._client_sub = self._sm.subscribe_session(self.config.client_session_id)
|
|
235
256
|
self._sm.mark_perf_active(self.config.client_session_id)
|
|
@@ -370,6 +391,8 @@ class PerfRun:
|
|
|
370
391
|
await self._dispatch_scenario(scn)
|
|
371
392
|
else:
|
|
372
393
|
await self._order_bucket.acquire()
|
|
394
|
+
if self._ceiling_bucket is not None:
|
|
395
|
+
await self._ceiling_bucket.acquire()
|
|
373
396
|
if not self._injecting:
|
|
374
397
|
break
|
|
375
398
|
msg, meta = self._factory.build_single_order()
|
|
@@ -453,6 +476,8 @@ class PerfRun:
|
|
|
453
476
|
|
|
454
477
|
for tpl in scn.messages:
|
|
455
478
|
await self._order_bucket.acquire()
|
|
479
|
+
if self._ceiling_bucket is not None:
|
|
480
|
+
await self._ceiling_bucket.acquire() # approx in scenario mode: per message
|
|
456
481
|
if not self._injecting:
|
|
457
482
|
return
|
|
458
483
|
msg, meta = self._factory.build_template_message(tpl, ctx, refs)
|
|
@@ -40,6 +40,15 @@ class RateConfig(BaseModel):
|
|
|
40
40
|
fill_ratio: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
41
41
|
allow_burst: bool = False
|
|
42
42
|
max_burst_multiplier: float = Field(default=2.0, ge=1.0)
|
|
43
|
+
# Optional per-session messages/sec ceiling. The injector is throttled to the
|
|
44
|
+
# order rate that keeps the chosen session under `max_msgs_per_sec` (using
|
|
45
|
+
# est_msgs_per_order — every per-order message crosses the link, so each leg
|
|
46
|
+
# touches ~the same count). ceiling_session_id is the session it protects:
|
|
47
|
+
# informational (UI label, warning text, participant validation), not the math.
|
|
48
|
+
# Throttling only the injector keeps latency clean and can't deadlock the
|
|
49
|
+
# lifecycle (replies/acks stay immediate). None = no ceiling.
|
|
50
|
+
max_msgs_per_sec: Optional[int] = Field(default=None, gt=0)
|
|
51
|
+
ceiling_session_id: Optional[str] = None
|
|
43
52
|
|
|
44
53
|
|
|
45
54
|
class TestConfig(BaseModel):
|
|
@@ -222,6 +231,20 @@ class RunConfig(BaseModel):
|
|
|
222
231
|
return self
|
|
223
232
|
|
|
224
233
|
|
|
234
|
+
def est_msgs_per_order(cfg: "RunConfig") -> int:
|
|
235
|
+
"""Rough peak FIX message count generated per order — the one formula shared
|
|
236
|
+
by the durability rate guard (api/routers/perf.py) and the per-session msgs/s
|
|
237
|
+
ceiling (perf_engine.py): 35=D out + 35=8 ack + fills, plus a request+reply
|
|
238
|
+
pair for each of the amend / cancel flavours when enabled. Deliberately an
|
|
239
|
+
upper-ish bound (a tighter divisor → a more conservative cap)."""
|
|
240
|
+
n = 2 + cfg.payload.fill.fills_per_order # order + ack + fills
|
|
241
|
+
if cfg.amend.enabled:
|
|
242
|
+
n += 2 # 35=G + 35=8 replace
|
|
243
|
+
if cfg.cancel.enabled:
|
|
244
|
+
n += 2 # 35=F + 35=8/9 reply
|
|
245
|
+
return max(1, n)
|
|
246
|
+
|
|
247
|
+
|
|
225
248
|
# ---------------------------------------------------------------------------
|
|
226
249
|
# Responses (read by key downstream — keep stable)
|
|
227
250
|
# ---------------------------------------------------------------------------
|