From 0a4e69d40c926b78058cd8162f64dcf5ebd75629 Mon Sep 17 00:00:00 2001 From: agentson Date: Fri, 27 Feb 2026 00:41:13 +0900 Subject: [PATCH] fix: record kill switch cancel failures and add failure-path tests --- src/main.py | 30 ++++++++++++++++++++++----- tests/test_main.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/main.py b/src/main.py index 3230116..2fe9ef4 100644 --- a/src/main.py +++ b/src/main.py @@ -663,6 +663,7 @@ async def _cancel_pending_orders_for_kill_switch( overseas_broker: OverseasBroker, markets: list[MarketInfo], ) -> None: + failures: list[str] = [] domestic = [m for m in markets if m.is_domestic] overseas = [m for m in markets if not m.is_domestic] @@ -673,21 +674,28 @@ async def _cancel_pending_orders_for_kill_switch( logger.warning("KillSwitch: failed to fetch domestic pending orders: %s", exc) orders = [] for order in orders: + stock_code = str(order.get("pdno", "")) try: - stock_code = order.get("pdno", "") orgn_odno = order.get("orgn_odno", "") krx_fwdg_ord_orgno = order.get("ord_gno_brno", "") psbl_qty = int(order.get("psbl_qty", "0") or "0") if not stock_code or not orgn_odno or psbl_qty <= 0: continue - await broker.cancel_domestic_order( + cancel_result = await broker.cancel_domestic_order( stock_code=stock_code, orgn_odno=orgn_odno, krx_fwdg_ord_orgno=krx_fwdg_ord_orgno, qty=psbl_qty, ) + if cancel_result.get("rt_cd") != "0": + failures.append( + "domestic cancel failed for" + f" {stock_code}: rt_cd={cancel_result.get('rt_cd')}" + f" msg={cancel_result.get('msg1')}" + ) except Exception as exc: logger.warning("KillSwitch: domestic cancel failed: %s", exc) + failures.append(f"domestic cancel exception for {stock_code}: {exc}") us_exchanges = frozenset({"NASD", "NYSE", "AMEX"}) exchange_codes: list[str] = [] @@ -712,21 +720,33 @@ async def _cancel_pending_orders_for_kill_switch( ) continue for order in orders: + stock_code = str(order.get("pdno", "")) + order_exchange = str(order.get("ovrs_excg_cd") or exchange_code) try: - stock_code = order.get("pdno", "") odno = order.get("odno", "") nccs_qty = int(order.get("nccs_qty", "0") or "0") - order_exchange = order.get("ovrs_excg_cd") or exchange_code if not stock_code or not odno or nccs_qty <= 0: continue - await overseas_broker.cancel_overseas_order( + cancel_result = await overseas_broker.cancel_overseas_order( exchange_code=order_exchange, stock_code=stock_code, odno=odno, qty=nccs_qty, ) + if cancel_result.get("rt_cd") != "0": + failures.append( + "overseas cancel failed for" + f" {order_exchange}/{stock_code}: rt_cd={cancel_result.get('rt_cd')}" + f" msg={cancel_result.get('msg1')}" + ) except Exception as exc: logger.warning("KillSwitch: overseas cancel failed: %s", exc) + failures.append( + f"overseas cancel exception for {order_exchange}/{stock_code}: {exc}" + ) + + if failures: + raise RuntimeError("; ".join(failures[:3])) async def _refresh_order_state_for_kill_switch( diff --git a/tests/test_main.py b/tests/test_main.py index 719063d..3dee447 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5414,3 +5414,54 @@ async def test_trigger_emergency_kill_switch_executes_operational_steps() -> Non pnl_pct=-3.2, threshold=-3.0, ) + + +@pytest.mark.asyncio +async def test_trigger_emergency_kill_switch_records_cancel_failure() -> None: + """Cancel API rejection should be captured in kill switch errors.""" + broker = MagicMock() + broker.get_domestic_pending_orders = AsyncMock( + return_value=[ + { + "pdno": "005930", + "orgn_odno": "1", + "ord_gno_brno": "01", + "psbl_qty": "3", + } + ] + ) + broker.cancel_domestic_order = AsyncMock(return_value={"rt_cd": "1", "msg1": "fail"}) + broker.get_balance = AsyncMock(return_value={"output1": [], "output2": []}) + + overseas_broker = MagicMock() + overseas_broker.get_overseas_pending_orders = AsyncMock(return_value=[]) + overseas_broker.get_overseas_balance = AsyncMock(return_value={"output1": [], "output2": []}) + + telegram = MagicMock() + telegram.notify_circuit_breaker = AsyncMock() + + settings = MagicMock() + settings.enabled_market_list = ["KR"] + + market = MagicMock() + market.code = "KR" + market.exchange_code = "KRX" + market.is_domestic = True + + with ( + patch("src.main.MARKETS", {"KR": market}), + patch("src.main.BLACKOUT_ORDER_MANAGER.clear", return_value=0), + ): + report = await _trigger_emergency_kill_switch( + reason="test-fail", + broker=broker, + overseas_broker=overseas_broker, + telegram=telegram, + settings=settings, + current_market=market, + stock_code="005930", + pnl_pct=-3.2, + threshold=-3.0, + ) + + assert any(err.startswith("cancel_pending_orders:") for err in report.errors)