Skip to content

Commit 7915506

Browse files
keitajclaude
andcommitted
Address review: real ERROR back-compat, summary contract, dead code
Three review fixes on top of the post-only rejection tracker: 1. **Default ERROR level now byte-identical to the legacy line** (review item 1, should-fix). Previously the tracker emitted the richer ``[reject:post_only_match] coin — …`` line at every level including the default ERROR, so log scrapers / templated alerts keying off the historical ``Order rejected: …`` prefix would no longer match. ``OrderRejectionTracker.record`` now branches: at ``ERROR`` (default) it emits the legacy line verbatim, only downgraded levels get the categorised format. Counter aggregation is unchanged so ``[reject-summary]`` still works at any level. 2. **``log_summary_if_due`` return value matches its docstring** (review item 3, nit). The docstring promised "True iff a summary was emitted" but the function returned True whenever the interval elapsed, even on idle intervals with nothing to log. ``_emit_summary`` now returns a bool indicating whether anything was actually logged and ``log_summary_if_due`` propagates it. The strategy main loop discards the value, but tests / external observers can rely on it. 3. **Drop dead ``return None if any_emitted else None``** (review item 2, nit). The conditional collapsed to ``None`` either way; replaced with the meaningful ``return any_emitted`` from item 2. Tests updated: - ``test_default_log_level_emits_legacy_line_at_error`` (renamed) — now asserts the legacy line at ERROR and explicitly that the categorised format is absent at default level. - ``test_unknown_log_level_string_falls_back_to_error`` — same legacy-line shape after fallback. - ``test_summary_skipped_when_no_events`` — pins the new ``True iff emitted`` contract; also asserts the cursor still advances to prevent the next call re-firing on every loop. - New integration test ``TestSingleRejectionWithTrackerAtDefaultError`` pins back-compat at the order_manager → tracker boundary. README updated to drop the stale claim about ``[reject-summary]`` being the only new line and to describe the actual two-mode behaviour (legacy line at ERROR, categorised line at downgraded levels). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 69e27ad commit 7915506

4 files changed

Lines changed: 108 additions & 27 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ The `market_making` strategy uses **progressive close pricing**: as a position a
363363

364364
**Adverse selection logging** (`--enable-adverse-selection-log`): Measures mid-price movement 5s/30s/60s after each fill, logging per-coin summaries every 300s. Observation only — no trading impact.
365365

366-
**Order rejection log aggregation** (`--rejection-log-level`, `--rejection-summary-interval`): Routine post-only rejections (`Post only order would have immediately matched`) are an expected retry signal under maker-only quoting, but they were historically logged at ERROR — drowning out genuine errors as MM size grows. The strategy now classifies each rejection by API error text and routes routine matches through a small aggregator that logs a single `[reject-summary]` INFO line every `--rejection-summary-interval` seconds (default 300, set to 0 to disable). The default per-rejection log level stays `error` to preserve historical behaviour; flip `--rejection-log-level warning` (or `info`) once the summary line is trusted. Unknown rejection text always falls through to ERROR so format changes / new reject reasons stay visible.
366+
**Order rejection log aggregation** (`--rejection-log-level`, `--rejection-summary-interval`): Routine post-only rejections (`Post only order would have immediately matched`) are an expected retry signal under maker-only quoting, but they were historically logged at ERROR — drowning out genuine errors as MM size grows. The strategy now classifies each rejection by API error text and adds a single `[reject-summary]` INFO line every `--rejection-summary-interval` seconds (default 300, set to 0 to disable). The default per-rejection log level stays `error` and emits the same byte-identical `Order rejected: …` line as before, so existing log scrapers and ERROR-rate alerts keep working. Flipping `--rejection-log-level warning` (or `info`) opts into a richer `[reject:tag] coin — …` categorised format at the chosen level; unknown rejection text always falls through to ERROR with the legacy line so format changes / new reject reasons stay visible.
367367

368368
**Dynamic offset** (`--dynamic-offset`): Auto-adjusts per-coin BBO offset based on adverse selection severity from the tracker. Coins with higher adverse selection get wider offsets; favorable coins get tighter offsets. Requires `--enable-ws` and `--enable-adverse-selection-log`. Manual `--coin-offset-overrides` serve as the baseline; dynamic adjustment adds/subtracts from it.
369369

order_rejection_tracker.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ def record(self, coin: str, raw_msg: str) -> str:
130130
Emits the per-rejection log at the configured level for routine
131131
matches; unknown patterns are forwarded to ERROR so format
132132
changes / new reject reasons remain visible.
133+
134+
At the default ERROR level the line is byte-identical to the
135+
legacy ``logger.error("Order rejected: …")`` produced by
136+
``order_manager.py`` so existing log scrapers / ERROR-rate alert
137+
templates continue to match. The richer
138+
``[reject:tag] coin — …`` format is reserved for the opt-in
139+
downgraded levels (warning / info / debug).
133140
"""
134141
tag = classify_rejection(raw_msg)
135142
bbo = _extract_bbo(raw_msg)
@@ -138,7 +145,9 @@ def record(self, coin: str, raw_msg: str) -> str:
138145
if tag == UNKNOWN_TAG:
139146
with self._lock:
140147
self._unknown_count += 1
141-
logger.error(f"Order rejected ({coin}): {raw_msg}")
148+
# Legacy ``Order rejected: <msg>`` format preserved so
149+
# back-compat with log scrapers holds for unknowns too.
150+
logger.error(f"Order rejected: {raw_msg}")
142151
return tag
143152

144153
with self._lock:
@@ -151,7 +160,14 @@ def record(self, coin: str, raw_msg: str) -> str:
151160

152161
# Logger call deliberately outside the lock: the lock guards
153162
# only the in-memory counters.
154-
logger.log(self._level, f"[reject:{tag}] {coin}{raw_msg}")
163+
if self._level == logging.ERROR:
164+
# Default deployment: emit the legacy line verbatim so back-
165+
# compat with existing log scrapers / templated alerts holds.
166+
logger.error(f"Order rejected: {raw_msg}")
167+
else:
168+
# Downgraded — operator has opted in, give them the richer
169+
# categorised line that carries coin + tag for grep-ability.
170+
logger.log(self._level, f"[reject:{tag}] {coin}{raw_msg}")
155171
return tag
156172

157173
# ------------------------------------------------------------------ #
@@ -160,8 +176,14 @@ def record(self, coin: str, raw_msg: str) -> str:
160176
def log_summary_if_due(self, now_monotonic: Optional[float] = None) -> bool:
161177
"""Emit a summary line if the configured interval has elapsed.
162178
163-
Returns ``True`` iff a summary was emitted.
164-
``summary_interval <= 0`` disables the summary entirely.
179+
Returns ``True`` iff a summary line was actually emitted, ``False``
180+
when:
181+
182+
* ``summary_interval <= 0`` (feature disabled), or
183+
* the interval has not elapsed yet, or
184+
* the interval elapsed but no rejections occurred (counters were
185+
flushed silently — the strategy main loop discards the return
186+
value, but tests / external observers can rely on this signal).
165187
"""
166188
if self._summary_interval <= 0:
167189
return False
@@ -176,8 +198,7 @@ def log_summary_if_due(self, now_monotonic: Optional[float] = None) -> bool:
176198
self._unknown_count = 0
177199
self._last_summary_ts = now
178200

179-
self._emit_summary(snapshot, unknown)
180-
return True
201+
return self._emit_summary(snapshot, unknown)
181202

182203
# ------------------------------------------------------------------ #
183204
# Internal helpers
@@ -186,7 +207,13 @@ def _emit_summary(
186207
self,
187208
snapshot: Dict[str, Dict[str, _CoinStats]],
188209
unknown: int,
189-
) -> None:
210+
) -> bool:
211+
"""Emit summary lines and return ``True`` iff anything was logged.
212+
213+
Returns ``False`` when both *snapshot* and *unknown* are empty —
214+
the helper stays silent during idle periods rather than emitting
215+
a meaningless ``total=0`` line every interval.
216+
"""
190217
any_emitted = False
191218
for tag, by_coin in snapshot.items():
192219
if not by_coin:
@@ -209,10 +236,7 @@ def _emit_summary(
209236
f"count={unknown} (unknown patterns logged at ERROR individually)"
210237
)
211238
any_emitted = True
212-
# No-op when both snapshot and unknown are empty: keeps the log
213-
# quiet during idle periods rather than emitting a meaningless
214-
# "total=0" line every interval.
215-
return None if any_emitted else None
239+
return any_emitted
216240

217241
# ------------------------------------------------------------------ #
218242
# Operational helpers (read-only)

tests/test_order_manager_rejection_routing.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,48 @@ def test_single_rejection_routed_through_tracker(self, caplog):
124124
assert tracker_lines[0].levelno == logging.WARNING
125125

126126

127+
class TestSingleRejectionWithTrackerAtDefaultError:
128+
"""Back-compat: tracker registered at default ERROR level emits the
129+
legacy ``Order rejected: ...`` line (no categorised prefix), so log
130+
scrapers / templated alerts that key off that exact prefix continue
131+
to match. The categorised ``[reject:tag]`` format is reserved for
132+
opt-in downgraded levels.
133+
"""
134+
135+
def test_default_error_level_emits_legacy_format(self, caplog):
136+
mgr = _make_order_manager()
137+
tracker = OrderRejectionTracker(
138+
routine_log_level='error', summary_interval=0,
139+
)
140+
mgr.set_rejection_tracker(tracker)
141+
142+
mgr.exchange.order.return_value = {
143+
'status': 'ok',
144+
'response': {'data': {'statuses': [{'error': _POST_ONLY}]}},
145+
}
146+
147+
with caplog.at_level(logging.DEBUG):
148+
mgr._place_order(_make_order(coin='xyz:NVDA'))
149+
150+
# Counter still incremented (so summary aggregation works).
151+
assert tracker.get_stats_snapshot()['post_only_match']['xyz:NVDA'] == 1
152+
153+
# Legacy line emitted at ERROR — matches what order_manager would
154+
# produce without any tracker registered.
155+
legacy = [
156+
r for r in caplog.records
157+
if r.message == f"Order rejected: {_POST_ONLY}"
158+
and r.levelno == logging.ERROR
159+
]
160+
assert len(legacy) == 1
161+
162+
# Categorised format must NOT appear at the default level.
163+
categorised = [
164+
r for r in caplog.records if "[reject:post_only_match]" in r.message
165+
]
166+
assert categorised == []
167+
168+
127169
class TestUnknownPatternRouted:
128170
"""Unknown text is still surfaced at ERROR via the tracker path."""
129171

tests/test_order_rejection_tracker.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,25 @@ def test_unknown_returns_unknown_tag_and_increments_separately(self):
8282
assert "unknown" not in t.get_stats_snapshot()
8383
assert t.get_unknown_count() == 1
8484

85-
def test_default_log_level_is_error(self, caplog):
85+
def test_default_log_level_emits_legacy_line_at_error(self, caplog):
86+
"""At default ERROR level the line is byte-identical to the
87+
legacy ``order_manager`` log so back-compat with log scrapers
88+
holds. The richer ``[reject:tag]`` format is reserved for
89+
opt-in downgraded levels."""
8690
t = OrderRejectionTracker()
8791
with caplog.at_level(logging.DEBUG, logger="order_rejection_tracker"):
8892
t.record("xyz:NVDA", _POST_ONLY_MSG)
89-
# The routine log line is emitted at ERROR by default.
90-
records = [
93+
legacy = [
94+
r for r in caplog.records
95+
if r.message == f"Order rejected: {_POST_ONLY_MSG}"
96+
]
97+
assert len(legacy) == 1
98+
assert legacy[0].levelno == logging.ERROR
99+
# The categorised format must NOT appear at the default level.
100+
categorised = [
91101
r for r in caplog.records if "[reject:post_only_match]" in r.message
92102
]
93-
assert len(records) == 1
94-
assert records[0].levelno == logging.ERROR
103+
assert categorised == []
95104

96105
def test_log_level_downgrade_to_warning(self, caplog):
97106
t = OrderRejectionTracker(routine_log_level="warning", summary_interval=0)
@@ -126,15 +135,18 @@ def test_unknown_pattern_always_logs_at_error(self, caplog):
126135

127136
def test_unknown_log_level_string_falls_back_to_error(self, caplog):
128137
"""A typo'd config value must not crash and must not silently skip
129-
logging — fall back to ERROR so the operator sees output."""
138+
logging — fall back to ERROR (legacy line format) so the
139+
operator still sees output."""
130140
t = OrderRejectionTracker(routine_log_level="WARN_typo", summary_interval=0)
131141
with caplog.at_level(logging.DEBUG, logger="order_rejection_tracker"):
132142
t.record("xyz:NVDA", _POST_ONLY_MSG)
133-
records = [
134-
r for r in caplog.records if "[reject:post_only_match]" in r.message
143+
# Falls back to ERROR → emits the legacy "Order rejected: ..." line.
144+
legacy = [
145+
r for r in caplog.records
146+
if r.message == f"Order rejected: {_POST_ONLY_MSG}"
147+
and r.levelno == logging.ERROR
135148
]
136-
assert len(records) == 1
137-
assert records[0].levelno == logging.ERROR
149+
assert len(legacy) == 1
138150

139151
def test_uppercase_log_level_accepted(self):
140152
"""Config values like ``WARNING`` should still resolve."""
@@ -233,18 +245,21 @@ def test_summary_includes_unknown_when_present(self, caplog):
233245
assert "count=1" in unknown_lines[0].message
234246

235247
def test_summary_skipped_when_no_events(self, caplog):
236-
"""An empty interval should not emit any summary line at all."""
248+
"""An empty interval emits no summary line and the helper
249+
returns False — the contract is "True iff a line was logged"."""
237250
t = OrderRejectionTracker(summary_interval=300)
238251
future = t._last_summary_ts + 301.0
239252
with caplog.at_level(logging.INFO, logger="order_rejection_tracker"):
240253
emitted = t.log_summary_if_due(now_monotonic=future)
241-
# log_summary_if_due returns True (interval elapsed) but no summary
242-
# line is emitted because there's nothing to report.
243-
assert emitted is True
254+
# No counters, so nothing was logged → False per the contract.
255+
assert emitted is False
244256
summaries = [
245257
r for r in caplog.records if "[reject-summary]" in r.message
246258
]
247-
assert len(summaries) == 0
259+
assert summaries == []
260+
# The internal cursor *was* still advanced so the next due check
261+
# waits another full interval rather than re-firing every loop.
262+
assert t._last_summary_ts == future
248263

249264
def test_top_8_truncation(self, caplog):
250265
t = OrderRejectionTracker(summary_interval=300)

0 commit comments

Comments
 (0)