From ccc97ebaa9c3a6e5d63259ac389adc03e3be7291 Mon Sep 17 00:00:00 2001 From: agentson Date: Wed, 18 Feb 2026 23:53:15 +0900 Subject: [PATCH 1/2] fix: use current_price for overseas limit orders (KIS VTS rejects market orders) (#149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/main.py | 4 +-- tests/test_main.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/main.py b/src/main.py index 1542a0c..4145ca7 100644 --- a/src/main.py +++ b/src/main.py @@ -510,7 +510,7 @@ async def trading_cycle( stock_code=stock_code, order_type=decision.action, 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")) @@ -919,7 +919,7 @@ async def run_daily_session( stock_code=stock_code, order_type=decision.action, 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")) diff --git a/tests/test_main.py b/tests/test_main.py index 8cc234a..a7fbb58 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -738,6 +738,82 @@ class TestOverseasBalanceParsing: # Verify price API was called 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: """Test scenario engine integration in trading_cycle.""" -- 2.49.1 From 3952a5337b4ae3fc5d4d0ed4070f86f30a26727f Mon Sep 17 00:00:00 2001 From: agentson Date: Wed, 18 Feb 2026 23:54:18 +0900 Subject: [PATCH 2/2] docs: add requirements log entry for overseas limit order fix (#149) --- docs/requirements-log.md | 65 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/requirements-log.md b/docs/requirements-log.md index 0d684ce..c2c2b72 100644 --- a/docs/requirements-log.md +++ b/docs/requirements-log.md @@ -201,3 +201,68 @@ - `tests/test_brain.py`: 3개 테스트 추가 (override 전달, optimization 우회, 미지정 시 기존 동작 유지) **이슈/PR:** #143 + +### 미국장 거래 미실행 근본 원인 분석 및 수정 (자율 실행 세션) + +**배경:** +- 사용자 요청: "미국장 열면 프로그램 돌려서 거래 한 번도 못 한 거 꼭 원인 찾아서 해결해줘" +- 프로그램을 미국장 개장(9:30 AM EST) 전부터 실행하여 실시간 로그를 분석 + +**발견된 근본 원인 #1: Defensive Playbook — BUY 조건 없음** + +- Gemini free tier (20 RPD) 소진 → `generate_playbook()` 실패 → `_defensive_playbook()` 폴백 +- Defensive playbook은 `price_change_pct_below: -3.0 → SELL` 조건만 존재, BUY 조건 없음 +- ScenarioEngine이 항상 HOLD 반환 → 거래 0건 + +**수정 #1 (PR #146, Issue #145):** +- `src/strategy/pre_market_planner.py`: `_smart_fallback_playbook()` 메서드 추가 + - 스캐너 signal 기반 BUY 조건 생성: `momentum → volume_ratio_above`, `oversold → rsi_below` + - 기존 defensive stop-loss SELL 조건 유지 +- Gemini 실패 시 defensive → smart fallback으로 전환 +- 테스트 10개 추가 + +**발견된 근본 원인 #2: 가격 API 거래소 코드 불일치 + VTS 잔고 API 오류** + +실제 로그: +``` +Scenario matched for MRNX: BUY (confidence=80) ✓ +Decision for EWUS (NYSE American): BUY (confidence=80) ✓ +Skip BUY APLZ (NYSE American): no affordable quantity (cash=0.00, price=0.00) ✗ +``` + +- `get_overseas_price()`: `NASD`/`NYSE`/`AMEX` 전송 → API가 `NAS`/`NYS`/`AMS` 기대 → 빈 응답 → `price=0` +- `VTTS3012R` 잔고 API: "ERROR : INPUT INVALID_CHECK_ACNO" → `total_cash=0` +- 결과: `_determine_order_quantity()` 가 0 반환 → 주문 건너뜀 + +**수정 #2 (PR #148, Issue #147):** +- `src/broker/overseas.py`: `_PRICE_EXCHANGE_MAP = _RANKING_EXCHANGE_MAP` 추가, 가격 API에 매핑 적용 +- `src/config.py`: `PAPER_OVERSEAS_CASH: float = Field(default=50000.0)` — paper 모드 시뮬레이션 잔고 +- `src/main.py`: 잔고 0일 때 PAPER_OVERSEAS_CASH 폴백, 가격 0일 때 candidate.price 폴백 +- 테스트 8개 추가 + +**효과:** +- BUY 결정 → 실제 주문 전송까지의 파이프라인이 완전히 동작 +- Paper 모드에서 KIS VTS 해외 잔고 API 오류에 관계없이 시뮬레이션 거래 가능 + +**이슈/PR:** #145, #146, #147, #148 + +### 해외주식 시장가 주문 거부 수정 (Fix #3, 연속 발견) + +**배경:** +- Fix #147 적용 후 주문 전송 시작 → KIS VTS가 거부: "지정가만 가능한 상품입니다" + +**근본 원인:** +- `trading_cycle()`, `run_daily_session()` 양쪽에서 `send_overseas_order(price=0.0)` 하드코딩 +- `price=0` → `ORD_DVSN="01"` (시장가) 전송 → KIS VTS 거부 +- Fix #147에서 이미 `current_price`를 올바르게 계산했으나 주문 시 미사용 + +**구현 결과:** +- `src/main.py`: 두 곳에서 `price=0.0` → `price=current_price`/`price=stock_data["current_price"]` +- `tests/test_main.py`: 회귀 테스트 `test_overseas_buy_order_uses_limit_price` 추가 + +**최종 확인 로그:** +``` +Order result: 모의투자 매수주문이 완료 되었습니다. ✓ +``` + +**이슈/PR:** #149, #150 -- 2.49.1