blackout: enforce bounded oldest-drop queue policy on overflow (#371)
All checks were successful
Gitea CI / test (push) Successful in 33s
Gitea CI / test (pull_request) Successful in 33s

This commit is contained in:
agentson
2026-03-02 02:57:08 +09:00
parent f7e242d147
commit 7959b749c7
6 changed files with 124 additions and 6 deletions

View File

@@ -79,3 +79,51 @@ def test_requeued_intent_is_processed_next_non_blackout_cycle() -> None:
manager.requeue(first_batch[0])
second_batch = manager.pop_recovery_batch(outside_blackout)
assert len(second_batch) == 1
def test_queue_overflow_drops_oldest_and_keeps_latest() -> None:
manager = BlackoutOrderManager(
enabled=True,
windows=parse_blackout_windows_kst("23:30-00:10"),
max_queue_size=2,
)
first = QueuedOrderIntent(
market_code="KR",
exchange_code="KRX",
stock_code="000001",
order_type="BUY",
quantity=1,
price=100.0,
source="first",
queued_at=datetime.now(UTC),
)
second = QueuedOrderIntent(
market_code="KR",
exchange_code="KRX",
stock_code="000002",
order_type="BUY",
quantity=1,
price=101.0,
source="second",
queued_at=datetime.now(UTC),
)
third = QueuedOrderIntent(
market_code="KR",
exchange_code="KRX",
stock_code="000003",
order_type="SELL",
quantity=2,
price=102.0,
source="third",
queued_at=datetime.now(UTC),
)
assert manager.enqueue(first)
assert manager.enqueue(second)
assert manager.enqueue(third)
assert manager.pending_count == 2
assert manager.overflow_drop_count == 1
outside_blackout = datetime(2026, 1, 1, 15, 20, tzinfo=UTC)
batch = manager.pop_recovery_batch(outside_blackout)
assert [intent.stock_code for intent in batch] == ["000002", "000003"]

View File

@@ -9,6 +9,7 @@ import src.main as main_module
from src.config import Settings
from src.context.layer import ContextLayer
from src.context.scheduler import ScheduleResult
from src.core.blackout_manager import BlackoutOrderManager
from src.core.order_policy import OrderPolicyRejected, get_session_info
from src.core.risk_manager import CircuitBreakerTripped, FatFingerRejected
from src.db import init_db, log_trade
@@ -33,6 +34,7 @@ from src.main import (
_extract_held_qty_from_balance,
_handle_market_close,
_inject_staged_exit_features,
_maybe_queue_order_intent,
_resolve_market_setting,
_resolve_sell_qty_for_pnl,
_retry_connection,
@@ -6529,6 +6531,40 @@ async def test_blackout_queues_order_and_skips_submission() -> None:
blackout_manager.enqueue.assert_called_once()
def test_blackout_queue_overflow_keeps_latest_intent() -> None:
manager = BlackoutOrderManager(enabled=True, windows=[], max_queue_size=1)
manager.in_blackout = lambda now=None: True # type: ignore[method-assign]
market = MagicMock()
market.code = "KR"
market.exchange_code = "KRX"
with patch("src.main.BLACKOUT_ORDER_MANAGER", manager):
assert _maybe_queue_order_intent(
market=market,
stock_code="005930",
order_type="BUY",
quantity=1,
price=100.0,
source="test-first",
)
assert _maybe_queue_order_intent(
market=market,
stock_code="000660",
order_type="BUY",
quantity=2,
price=200.0,
source="test-second",
)
assert manager.pending_count == 1
assert manager.overflow_drop_count == 1
manager.in_blackout = lambda now=None: False # type: ignore[method-assign]
batch = manager.pop_recovery_batch()
assert len(batch) == 1
assert batch[0].stock_code == "000660"
@pytest.mark.asyncio
async def test_process_blackout_recovery_executes_valid_intents() -> None:
"""Recovery must execute queued intents that pass revalidation."""