From 854931bed2a1eaf26a07aa76c713b97e1f883a21 Mon Sep 17 00:00:00 2001 From: agentson Date: Thu, 5 Feb 2026 00:31:01 +0900 Subject: [PATCH] fix: handle empty strings in price data parsing (issue #49) Apply consistent empty-string handling across main.py and scanner.py to prevent ValueError when KIS API returns empty strings. Changes: - src/main.py:110 - Add 'or "0"' for current_price parsing - src/analysis/scanner.py:86-87 - Add 'or "0"' for price/volume parsing - tests/test_main.py - Add test_overseas_price_empty_string - tests/test_volatility.py - Add test_scan_stock_overseas_empty_price Before: ValueError crashes trading cycle After: Empty strings default to 0.0, trading continues Co-Authored-By: Claude Sonnet 4.5 --- src/analysis/scanner.py | 4 +-- src/main.py | 2 +- tests/test_main.py | 54 ++++++++++++++++++++++++++++++++++++++++ tests/test_volatility.py | 22 ++++++++++++++++ 4 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/analysis/scanner.py b/src/analysis/scanner.py index 73882de..6bb384d 100644 --- a/src/analysis/scanner.py +++ b/src/analysis/scanner.py @@ -83,8 +83,8 @@ class MarketScanner: # Convert to orderbook-like structure orderbook = { "output1": { - "stck_prpr": price_data.get("output", {}).get("last", "0"), - "acml_vol": price_data.get("output", {}).get("tvol", "0"), + "stck_prpr": price_data.get("output", {}).get("last", "0") or "0", + "acml_vol": price_data.get("output", {}).get("tvol", "0") or "0", } } diff --git a/src/main.py b/src/main.py index b607c24..50f070a 100644 --- a/src/main.py +++ b/src/main.py @@ -107,7 +107,7 @@ async def trading_cycle( total_cash = float(balance_info.get("frcr_dncl_amt_2", "0") or "0") purchase_total = float(balance_info.get("frcr_buy_amt_smtl", "0") or "0") - current_price = float(price_data.get("output", {}).get("last", "0")) + current_price = float(price_data.get("output", {}).get("last", "0") or "0") foreigner_net = 0.0 # Not available for overseas # Calculate daily P&L % diff --git a/tests/test_main.py b/tests/test_main.py index 54d4d7e..be96bd5 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -394,6 +394,26 @@ class TestOverseasBalanceParsing: broker.get_overseas_balance = AsyncMock(return_value={"output2": []}) return broker + @pytest.fixture + def mock_overseas_broker_with_empty_price(self) -> MagicMock: + """Create mock overseas broker returning empty string for price.""" + broker = MagicMock() + broker.get_overseas_price = AsyncMock( + return_value={"output": {"last": ""}} # Empty string + ) + broker.get_overseas_balance = AsyncMock( + return_value={ + "output2": [ + { + "frcr_evlu_tota": "10000.00", + "frcr_dncl_amt_2": "5000.00", + "frcr_buy_amt_smtl": "4500.00", + } + ] + } + ) + return broker + @pytest.fixture def mock_domestic_broker(self) -> MagicMock: """Create minimal mock domestic broker.""" @@ -559,3 +579,37 @@ class TestOverseasBalanceParsing: # Verify balance API was called mock_overseas_broker_with_empty.get_overseas_balance.assert_called_once() + + @pytest.mark.asyncio + async def test_overseas_price_empty_string( + self, + mock_domestic_broker: MagicMock, + mock_overseas_broker_with_empty_price: MagicMock, + mock_brain_hold: MagicMock, + 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: + """Test overseas price parsing with empty string (issue #49).""" + with patch("src.main.log_trade"): + # Should not raise ValueError, should default to 0.0 + await trading_cycle( + broker=mock_domestic_broker, + overseas_broker=mock_overseas_broker_with_empty_price, + brain=mock_brain_hold, + 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", + ) + + # Verify price API was called + mock_overseas_broker_with_empty_price.get_overseas_price.assert_called_once() diff --git a/tests/test_volatility.py b/tests/test_volatility.py index b8f3a99..59a68fc 100644 --- a/tests/test_volatility.py +++ b/tests/test_volatility.py @@ -338,6 +338,28 @@ class TestMarketScanner: assert metrics.stock_code == "AAPL" assert metrics.current_price == 150.50 + @pytest.mark.asyncio + async def test_scan_stock_overseas_empty_price( + self, + scanner: MarketScanner, + mock_overseas_broker: OverseasBroker, + context_store: ContextStore, + ) -> None: + """Test scanning overseas stock with empty price string (issue #49).""" + mock_overseas_broker.get_overseas_price.return_value = { + "output": { + "last": "", # Empty string + "tvol": "", # Empty string + } + } + + market = MARKETS["US_NASDAQ"] + metrics = await scanner.scan_stock("AAPL", market) + + assert metrics is not None + assert metrics.stock_code == "AAPL" + assert metrics.current_price == 0.0 # Should default to 0.0 + @pytest.mark.asyncio async def test_scan_stock_error_handling( self,