fix: use current_price for overseas limit orders (KIS VTS rejects market orders) (#149)
Some checks failed
CI / test (pull_request) Has been cancelled
Some checks failed
CI / test (pull_request) Has been cancelled
KIS VTS (paper trading) rejects overseas market orders with: "모의투자 주문처리가 안되었습니다(지정가만 가능한 상품입니다)" Root cause: send_overseas_order() was called with price=0.0 (market order) in both trading_cycle() and run_daily_session(), even though current_price was already computed correctly by Fix #147 (exchange code mapping). Fix: pass current_price as the limit order price in both call sites. Domestic broker send_order() keeps price=0 (market orders are fine on KRX). Adds regression test TestOverseasBalanceParsing::test_overseas_buy_order_uses_limit_price verifying price=182.5 is passed, not 0.0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -510,7 +510,7 @@ async def trading_cycle(
|
|||||||
stock_code=stock_code,
|
stock_code=stock_code,
|
||||||
order_type=decision.action,
|
order_type=decision.action,
|
||||||
quantity=quantity,
|
quantity=quantity,
|
||||||
price=0.0, # market order
|
price=current_price, # limit order — KIS VTS rejects market orders
|
||||||
)
|
)
|
||||||
logger.info("Order result: %s", result.get("msg1", "OK"))
|
logger.info("Order result: %s", result.get("msg1", "OK"))
|
||||||
|
|
||||||
@@ -919,7 +919,7 @@ async def run_daily_session(
|
|||||||
stock_code=stock_code,
|
stock_code=stock_code,
|
||||||
order_type=decision.action,
|
order_type=decision.action,
|
||||||
quantity=quantity,
|
quantity=quantity,
|
||||||
price=0.0, # market order
|
price=stock_data["current_price"], # limit order — KIS VTS rejects market orders
|
||||||
)
|
)
|
||||||
logger.info("Order result: %s", result.get("msg1", "OK"))
|
logger.info("Order result: %s", result.get("msg1", "OK"))
|
||||||
|
|
||||||
|
|||||||
@@ -738,6 +738,82 @@ class TestOverseasBalanceParsing:
|
|||||||
# Verify price API was called
|
# Verify price API was called
|
||||||
mock_overseas_broker_with_empty_price.get_overseas_price.assert_called_once()
|
mock_overseas_broker_with_empty_price.get_overseas_price.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_overseas_broker_with_buy_scenario(self) -> MagicMock:
|
||||||
|
"""Create mock overseas broker that returns a valid price for BUY orders."""
|
||||||
|
broker = MagicMock()
|
||||||
|
broker.get_overseas_price = AsyncMock(
|
||||||
|
return_value={"output": {"last": "182.50"}}
|
||||||
|
)
|
||||||
|
broker.get_overseas_balance = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"output2": [
|
||||||
|
{
|
||||||
|
"frcr_evlu_tota": "100000.00",
|
||||||
|
"frcr_dncl_amt_2": "50000.00",
|
||||||
|
"frcr_buy_amt_smtl": "50000.00",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
broker.send_overseas_order = AsyncMock(return_value={"msg1": "주문접수"})
|
||||||
|
return broker
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_scenario_engine_buy(self) -> MagicMock:
|
||||||
|
"""Create mock scenario engine that returns BUY."""
|
||||||
|
engine = MagicMock(spec=ScenarioEngine)
|
||||||
|
engine.evaluate = MagicMock(return_value=_make_buy_match("AAPL"))
|
||||||
|
return engine
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_overseas_buy_order_uses_limit_price(
|
||||||
|
self,
|
||||||
|
mock_domestic_broker: MagicMock,
|
||||||
|
mock_overseas_broker_with_buy_scenario: MagicMock,
|
||||||
|
mock_scenario_engine_buy: MagicMock,
|
||||||
|
mock_playbook: DayPlaybook,
|
||||||
|
mock_risk: MagicMock,
|
||||||
|
mock_db: MagicMock,
|
||||||
|
mock_decision_logger: MagicMock,
|
||||||
|
mock_context_store: MagicMock,
|
||||||
|
mock_criticality_assessor: MagicMock,
|
||||||
|
mock_telegram: MagicMock,
|
||||||
|
mock_overseas_market: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Overseas BUY order must use current_price (limit), not 0 (market).
|
||||||
|
|
||||||
|
KIS VTS rejects market orders for overseas paper trading.
|
||||||
|
Regression test for issue #149.
|
||||||
|
"""
|
||||||
|
mock_telegram.notify_trade_execution = AsyncMock()
|
||||||
|
|
||||||
|
with patch("src.main.log_trade"):
|
||||||
|
await trading_cycle(
|
||||||
|
broker=mock_domestic_broker,
|
||||||
|
overseas_broker=mock_overseas_broker_with_buy_scenario,
|
||||||
|
scenario_engine=mock_scenario_engine_buy,
|
||||||
|
playbook=mock_playbook,
|
||||||
|
risk=mock_risk,
|
||||||
|
db_conn=mock_db,
|
||||||
|
decision_logger=mock_decision_logger,
|
||||||
|
context_store=mock_context_store,
|
||||||
|
criticality_assessor=mock_criticality_assessor,
|
||||||
|
telegram=mock_telegram,
|
||||||
|
market=mock_overseas_market,
|
||||||
|
stock_code="AAPL",
|
||||||
|
scan_candidates={},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify limit order was sent with actual price, not 0.0
|
||||||
|
mock_overseas_broker_with_buy_scenario.send_overseas_order.assert_called_once()
|
||||||
|
call_kwargs = mock_overseas_broker_with_buy_scenario.send_overseas_order.call_args
|
||||||
|
sent_price = call_kwargs[1].get("price") or call_kwargs[0][4]
|
||||||
|
assert sent_price == 182.5, (
|
||||||
|
f"Expected limit price 182.5 but got {sent_price}. "
|
||||||
|
"KIS VTS only accepts limit orders for overseas paper trading."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestScenarioEngineIntegration:
|
class TestScenarioEngineIntegration:
|
||||||
"""Test scenario engine integration in trading_cycle."""
|
"""Test scenario engine integration in trading_cycle."""
|
||||||
|
|||||||
Reference in New Issue
Block a user