feat: Add drain mode for graceful pre-shutdown (--drain-flag-file)#127
Conversation
Introduce a flag-file based drain mode for the market-making strategy. When the configured file exists, the strategy stops placing new entry orders and only manages existing positions via the normal maker-first PositionCloser flow. Designed to be triggered by an external script before SIGTERM so positions can unwind via maker close instead of taker IOC close at the boundary. - New CLI flag --drain-flag-file (default: empty = disabled) - Behavior mirrors quiet_hours full-stop mode; drain takes priority when both apply - _is_drain_mode() polls the flag file each cycle (cheap stat call) - New tests/test_drain_mode.py covering enable/disable, no-repeat cancel, position management, exit reset, and precedence over quiet_hours - Update test fixtures that bypass __init__ to set the new attributes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
keitaj
left a comment
There was a problem hiding this comment.
Review: Drain mode for graceful pre-shutdown
Clean implementation that reuses the established quiet_hours full-stop pattern. Backward compatible (empty default disables), risk guards untouched, well covered by the new test module. One nit and one optional test gap below.
1. Redundant _is_drain_mode() evaluation in run() (nit)
strategies/market_making_strategy.py L279–L313:
The drain branch calls self._is_drain_mode() twice per cycle in the not-active path: once to gate the early-return block, then again in if self._was_drain and not self._is_drain_mode(): after it. Each call performs an os.path.exists syscall. Cheap, but easy to dedupe and matches the pattern already used for is_quiet.
Suggestion:
is_drain = self._is_drain_mode()
if is_drain:
# ... existing drain branch ...
return
if self._was_drain and not is_drain:
logger.info("[mm] Exiting drain mode, resuming quotes")
self._was_drain = False2. Test coverage for close_immediately=True in drain (nit)
tests/test_drain_mode.py:
TestDrainModeBehavior only exercises the close_immediately=False branch (the default in tests). The if self.close_immediately: self.close_position(coin) path inside the drain branch is structurally identical to the quiet-hours equivalent, but is currently uncovered for drain. Optional — adding a single parametrized assertion would close the gap.
Suggestion: Add a test that sets close_immediately=True and asserts strategy.close_position is called when a position is open under drain.
3. test_exit_drain_resets_flag could verify quotes resume (nit)
tests/test_drain_mode.py L173–L196:
After removing the flag file, the test asserts _was_drain is False but does not check that normal quoting resumes (the more useful behavioural guarantee). Asserting bulk_place_orders.assert_called_once() after the second run() would catch a regression where the early-return is left in place by mistake.
Verdict: LGTM
No blockers. Two nits + one optional test gap, all non-blocking. Behaviour mirrors quiet_hours precisely so the risk surface is well understood. Ready to merge after CI confirmation.
Summary
PositionCloserflow.SIGTERM, so positions can unwind via maker close instead of taker IOC close at the boundary.quiet_hoursfull-stop mode; drain takes priority when both apply.Changes
strategies/market_making_strategy.py_drain_flag_fileand_was_drainstate initialised fromconfig.get('drain_flag_file', '')._is_drain_mode()helper that returnsTruewhen the flag file exists (cheapos.path.existsper cycle).run(), a drain branch executes before the quiet-hours branch: cancels open orders on entry, manages existing positions throughPositionCloser, skips new entry quotes, logs[DRAIN]cycle suffix, and resets state when the flag disappears.bot.py--drain-flag-file(default empty = disabled)._STRATEGY_PARAMS['market_making']asdrain_flag_file.tests/test_drain_mode.py(new)_is_drain_mode()truthiness in all configurations (empty / missing / present /None).bulk_place_orderswhen no positions are open.PositionCloser.manage()for open positions._was_drain.quiet_hourswhen both apply.tests/test_mm_*.py(3 files): test fixtures that bypass__init__extended with_drain_flag_file = ''and_was_drain = Falseto keep the existing assertions intact.README.md: human description in## Strategiesplus YAML reference in## Parameter Reference for AI Agents.Test plan
tests/test_drain_mode.py, 8 tests)flake8 . --max-line-length=120passes (no new findings)pytest tests/passes (810 / 810)test_quiet_hours.py(drain takes priority but quiet-hours behaviour preserved when drain inactive)🤖 Generated with Claude Code