fix: record kill switch cancel failures and add failure-path tests
This commit is contained in:
30
src/main.py
30
src/main.py
@@ -663,6 +663,7 @@ async def _cancel_pending_orders_for_kill_switch(
|
|||||||
overseas_broker: OverseasBroker,
|
overseas_broker: OverseasBroker,
|
||||||
markets: list[MarketInfo],
|
markets: list[MarketInfo],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
failures: list[str] = []
|
||||||
domestic = [m for m in markets if m.is_domestic]
|
domestic = [m for m in markets if m.is_domestic]
|
||||||
overseas = [m for m in markets if not 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)
|
logger.warning("KillSwitch: failed to fetch domestic pending orders: %s", exc)
|
||||||
orders = []
|
orders = []
|
||||||
for order in orders:
|
for order in orders:
|
||||||
|
stock_code = str(order.get("pdno", ""))
|
||||||
try:
|
try:
|
||||||
stock_code = order.get("pdno", "")
|
|
||||||
orgn_odno = order.get("orgn_odno", "")
|
orgn_odno = order.get("orgn_odno", "")
|
||||||
krx_fwdg_ord_orgno = order.get("ord_gno_brno", "")
|
krx_fwdg_ord_orgno = order.get("ord_gno_brno", "")
|
||||||
psbl_qty = int(order.get("psbl_qty", "0") or "0")
|
psbl_qty = int(order.get("psbl_qty", "0") or "0")
|
||||||
if not stock_code or not orgn_odno or psbl_qty <= 0:
|
if not stock_code or not orgn_odno or psbl_qty <= 0:
|
||||||
continue
|
continue
|
||||||
await broker.cancel_domestic_order(
|
cancel_result = await broker.cancel_domestic_order(
|
||||||
stock_code=stock_code,
|
stock_code=stock_code,
|
||||||
orgn_odno=orgn_odno,
|
orgn_odno=orgn_odno,
|
||||||
krx_fwdg_ord_orgno=krx_fwdg_ord_orgno,
|
krx_fwdg_ord_orgno=krx_fwdg_ord_orgno,
|
||||||
qty=psbl_qty,
|
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:
|
except Exception as exc:
|
||||||
logger.warning("KillSwitch: domestic cancel failed: %s", 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"})
|
us_exchanges = frozenset({"NASD", "NYSE", "AMEX"})
|
||||||
exchange_codes: list[str] = []
|
exchange_codes: list[str] = []
|
||||||
@@ -712,21 +720,33 @@ async def _cancel_pending_orders_for_kill_switch(
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
for order in orders:
|
for order in orders:
|
||||||
|
stock_code = str(order.get("pdno", ""))
|
||||||
|
order_exchange = str(order.get("ovrs_excg_cd") or exchange_code)
|
||||||
try:
|
try:
|
||||||
stock_code = order.get("pdno", "")
|
|
||||||
odno = order.get("odno", "")
|
odno = order.get("odno", "")
|
||||||
nccs_qty = int(order.get("nccs_qty", "0") or "0")
|
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:
|
if not stock_code or not odno or nccs_qty <= 0:
|
||||||
continue
|
continue
|
||||||
await overseas_broker.cancel_overseas_order(
|
cancel_result = await overseas_broker.cancel_overseas_order(
|
||||||
exchange_code=order_exchange,
|
exchange_code=order_exchange,
|
||||||
stock_code=stock_code,
|
stock_code=stock_code,
|
||||||
odno=odno,
|
odno=odno,
|
||||||
qty=nccs_qty,
|
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:
|
except Exception as exc:
|
||||||
logger.warning("KillSwitch: overseas cancel failed: %s", 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(
|
async def _refresh_order_state_for_kill_switch(
|
||||||
|
|||||||
@@ -5414,3 +5414,54 @@ async def test_trigger_emergency_kill_switch_executes_operational_steps() -> Non
|
|||||||
pnl_pct=-3.2,
|
pnl_pct=-3.2,
|
||||||
threshold=-3.0,
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user