feat: 블랙아웃 복구 시 가격/세션 재검증 강화 (#328) #345

Merged
jihoson merged 2 commits from feature/issue-328-blackout-revalidation into feature/v3-session-policy-stream 2026-03-01 09:43:09 +09:00
Showing only changes of commit 5fae9765e7 - Show all commits

View File

@@ -6488,6 +6488,100 @@ async def test_process_blackout_recovery_drops_intent_on_excessive_price_drift()
validate_policy.assert_not_called()
@pytest.mark.asyncio
async def test_process_blackout_recovery_drops_overseas_intent_on_excessive_price_drift() -> None:
"""Overseas queued intent is dropped when price drift exceeds threshold."""
db_conn = init_db(":memory:")
broker = MagicMock()
broker.send_order = AsyncMock(return_value={"rt_cd": "0", "msg1": "OK"})
overseas_broker = MagicMock()
overseas_broker.get_overseas_price = AsyncMock(return_value={"output": {"last": "106.0"}})
overseas_broker.send_overseas_order = AsyncMock(return_value={"rt_cd": "0", "msg1": "OK"})
market = MagicMock()
market.code = "US_NASDAQ"
market.exchange_code = "NASD"
market.is_domestic = False
intent = MagicMock()
intent.market_code = "US_NASDAQ"
intent.stock_code = "AAPL"
intent.order_type = "BUY"
intent.quantity = 1
intent.price = 100.0
intent.source = "test"
intent.attempts = 0
blackout_manager = MagicMock()
blackout_manager.pop_recovery_batch.return_value = [intent]
with (
patch("src.main.BLACKOUT_ORDER_MANAGER", blackout_manager),
patch("src.main.MARKETS", {"US_NASDAQ": market}),
patch("src.main.get_open_position", return_value=None),
patch("src.main.validate_order_policy") as validate_policy,
):
await process_blackout_recovery_orders(
broker=broker,
overseas_broker=overseas_broker,
db_conn=db_conn,
settings=Settings(
KIS_APP_KEY="k",
KIS_APP_SECRET="s",
KIS_ACCOUNT_NO="12345678-01",
GEMINI_API_KEY="g",
BLACKOUT_RECOVERY_MAX_PRICE_DRIFT_PCT=5.0,
),
)
overseas_broker.send_overseas_order.assert_not_called()
validate_policy.assert_not_called()
@pytest.mark.asyncio
async def test_process_blackout_recovery_requeues_intent_when_price_lookup_fails() -> None:
"""Price lookup failure must requeue intent for a later retry."""
db_conn = init_db(":memory:")
broker = MagicMock()
broker.get_current_price = AsyncMock(side_effect=ConnectionError("price API down"))
broker.send_order = AsyncMock(return_value={"rt_cd": "0", "msg1": "OK"})
overseas_broker = MagicMock()
market = MagicMock()
market.code = "KR"
market.exchange_code = "KRX"
market.is_domestic = True
intent = MagicMock()
intent.market_code = "KR"
intent.stock_code = "005930"
intent.order_type = "BUY"
intent.quantity = 1
intent.price = 100.0
intent.source = "test"
intent.attempts = 0
blackout_manager = MagicMock()
blackout_manager.pop_recovery_batch.return_value = [intent]
with (
patch("src.main.BLACKOUT_ORDER_MANAGER", blackout_manager),
patch("src.main.MARKETS", {"KR": market}),
patch("src.main.get_open_position", return_value=None),
patch("src.main.validate_order_policy") as validate_policy,
):
await process_blackout_recovery_orders(
broker=broker,
overseas_broker=overseas_broker,
db_conn=db_conn,
)
broker.send_order.assert_not_called()
validate_policy.assert_not_called()
blackout_manager.requeue.assert_called_once_with(intent)
assert intent.attempts == 1
@pytest.mark.asyncio
async def test_trigger_emergency_kill_switch_executes_operational_steps() -> None:
"""Emergency kill switch should execute cancel/refresh/reduce/notify callbacks."""