fix: apply KR session-aware exchange routing for rankings and orders (#409)
Some checks failed
Gitea CI / test (push) Successful in 35s
Gitea CI / test (pull_request) Failing after 5s

This commit is contained in:
agentson
2026-03-04 10:12:41 +09:00
parent 100586e237
commit c80f3daad7
6 changed files with 179 additions and 8 deletions

View File

@@ -400,6 +400,15 @@ class TestFetchMarketRankings:
assert result[0]["stock_code"] == "015260"
assert result[0]["change_rate"] == 29.74
@pytest.mark.asyncio
async def test_volume_uses_nx_market_code_in_nxt_session(self, broker: KISBroker) -> None:
mock_resp = _make_ranking_mock([])
with patch("aiohttp.ClientSession.get", return_value=mock_resp) as mock_get:
await broker.fetch_market_rankings(ranking_type="volume", session_id="NXT_PRE")
params = mock_get.call_args[1].get("params", {})
assert params.get("FID_COND_MRKT_DIV_CODE") == "NX"
# ---------------------------------------------------------------------------
# KRX tick unit / round-down helpers (issue #157)
@@ -591,6 +600,27 @@ class TestSendOrderTickRounding:
body = order_call[1].get("json", {})
assert body["ORD_DVSN"] == "01"
@pytest.mark.asyncio
async def test_send_order_sets_exchange_field_from_session(self, broker: KISBroker) -> None:
mock_hash = AsyncMock()
mock_hash.status = 200
mock_hash.json = AsyncMock(return_value={"HASH": "h"})
mock_hash.__aenter__ = AsyncMock(return_value=mock_hash)
mock_hash.__aexit__ = AsyncMock(return_value=False)
mock_order = AsyncMock()
mock_order.status = 200
mock_order.json = AsyncMock(return_value={"rt_cd": "0"})
mock_order.__aenter__ = AsyncMock(return_value=mock_order)
mock_order.__aexit__ = AsyncMock(return_value=False)
with patch("aiohttp.ClientSession.post", side_effect=[mock_hash, mock_order]) as mock_post:
await broker.send_order("005930", "BUY", 1, price=50000, session_id="NXT_PRE")
order_call = mock_post.call_args_list[1]
body = order_call[1].get("json", {})
assert body["EXCG_ID_DVSN_CD"] == "NXT"
# ---------------------------------------------------------------------------
# TR_ID live/paper branching (issues #201, #202, #203)

View File

@@ -0,0 +1,40 @@
from __future__ import annotations
from src.broker.kr_exchange_router import KRExchangeRouter
def test_ranking_market_code_by_session() -> None:
router = KRExchangeRouter()
assert router.resolve_for_ranking("KRX_REG") == "J"
assert router.resolve_for_ranking("NXT_PRE") == "NX"
assert router.resolve_for_ranking("NXT_AFTER") == "NX"
def test_order_exchange_falls_back_to_session_default_on_missing_data() -> None:
router = KRExchangeRouter()
resolved = router.resolve_for_order(
stock_code="0001A0",
session_id="NXT_PRE",
is_dual_listed=True,
spread_krx=None,
spread_nxt=None,
liquidity_krx=None,
liquidity_nxt=None,
)
assert resolved.exchange_code == "NXT"
assert resolved.reason == "fallback_data_unavailable"
def test_order_exchange_uses_spread_preference_for_dual_listing() -> None:
router = KRExchangeRouter()
resolved = router.resolve_for_order(
stock_code="0001A0",
session_id="KRX_REG",
is_dual_listed=True,
spread_krx=0.005,
spread_nxt=0.003,
liquidity_krx=100000.0,
liquidity_nxt=90000.0,
)
assert resolved.exchange_code == "NXT"
assert resolved.reason == "dual_listing_spread"

View File

@@ -103,6 +103,33 @@ class TestSmartVolatilityScanner:
assert candidates[0].stock_code == "005930"
assert candidates[0].signal == "oversold"
@pytest.mark.asyncio
async def test_scan_domestic_passes_session_id_to_rankings(
self, scanner: SmartVolatilityScanner, mock_broker: MagicMock
) -> None:
fluctuation_rows = [
{
"stock_code": "005930",
"name": "Samsung",
"price": 70000,
"volume": 5000000,
"change_rate": 1.0,
"volume_increase_rate": 120,
},
]
mock_broker.fetch_market_rankings.side_effect = [fluctuation_rows, fluctuation_rows]
mock_broker.get_daily_prices.return_value = [
{"open": 1, "high": 71000, "low": 69000, "close": 70000, "volume": 1000000},
{"open": 1, "high": 70000, "low": 68000, "close": 69000, "volume": 900000},
]
await scanner.scan(domestic_session_id="NXT_PRE")
first_call = mock_broker.fetch_market_rankings.call_args_list[0]
second_call = mock_broker.fetch_market_rankings.call_args_list[1]
assert first_call.kwargs["session_id"] == "NXT_PRE"
assert second_call.kwargs["session_id"] == "NXT_PRE"
@pytest.mark.asyncio
async def test_scan_domestic_finds_momentum_candidate(
self, scanner: SmartVolatilityScanner, mock_broker: MagicMock