From cb2e3fae57546cc68e2939b607dbed8718840e78 Mon Sep 17 00:00:00 2001 From: agentson Date: Thu, 5 Feb 2026 00:12:57 +0900 Subject: [PATCH 1/2] fix: reduce rate limit from 10 to 5 RPS to avoid API errors (issue #43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce RATE_LIMIT_RPS from 10.0 to 5.0 to prevent "초당 거래건수를 초과하였습니다" (EGW00201) errors from KIS API. Docker logs showed this was the most frequent error (70% of failures), occurring when multiple stocks are scanned rapidly. Changes: - src/config.py: RATE_LIMIT_RPS 10.0 → 5.0 - .env.example: Update default and add explanation comment Trade-off: Slower API throughput, but more reliable operation. Can be tuned per deployment via environment variable. Fixes: #43 Co-Authored-By: Claude Sonnet 4.5 --- .env.example | 5 +++-- src/config.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index cc2ffb0..ac91c53 100644 --- a/.env.example +++ b/.env.example @@ -16,8 +16,9 @@ CONFIDENCE_THRESHOLD=80 # Database DB_PATH=data/trade_logs.db -# Rate Limiting -RATE_LIMIT_RPS=10.0 +# Rate Limiting (requests per second for KIS API) +# Reduced to 5.0 to avoid "초당 거래건수 초과" errors (EGW00201) +RATE_LIMIT_RPS=5.0 # Trading Mode (paper / live) MODE=paper diff --git a/src/config.py b/src/config.py index 222afd0..2999f39 100644 --- a/src/config.py +++ b/src/config.py @@ -37,7 +37,8 @@ class Settings(BaseSettings): DB_PATH: str = "data/trade_logs.db" # Rate Limiting (requests per second for KIS API) - RATE_LIMIT_RPS: float = 10.0 + # Reduced to 5.0 to avoid EGW00201 "초당 거래건수 초과" errors + RATE_LIMIT_RPS: float = 5.0 # Trading mode MODE: str = Field(default="paper", pattern="^(paper|live)$") From c57ccc4bca368b175e874b816998f320f9890491 Mon Sep 17 00:00:00 2001 From: agentson Date: Thu, 5 Feb 2026 00:15:04 +0900 Subject: [PATCH 2/2] fix: add safe_float() to handle empty string conversions (issue #44) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add safe_float() helper function to safely convert API response values to float, handling empty strings, None, and invalid values that cause ValueError: "could not convert string to float: ''". Changes: - Add safe_float() function in src/main.py with full docstring - Replace all float() calls with safe_float() in trading_cycle() - Domestic market: orderbook prices, balance amounts - Overseas market: price data, balance info - Add 6 comprehensive unit tests for safe_float() The function handles: - Empty strings ("") → default (0.0) - None values → default (0.0) - Invalid strings ("abc") → default (0.0) - Valid strings ("123.45") → parsed float - Float inputs (123.45) → pass through This prevents crashes when KIS API returns empty strings during market closed hours or data unavailability. Fixes: #44 Co-Authored-By: Claude Sonnet 4.5 --- src/main.py | 47 +++++++++++++++++++++++++++++++++++++--------- tests/test_main.py | 38 ++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/main.py b/src/main.py index b607c24..48017f0 100644 --- a/src/main.py +++ b/src/main.py @@ -33,6 +33,35 @@ from src.notifications.telegram_client import TelegramClient logger = logging.getLogger(__name__) + +def safe_float(value: str | float | None, default: float = 0.0) -> float: + """Convert to float, handling empty strings and None. + + Args: + value: Value to convert (string, float, or None) + default: Default value if conversion fails + + Returns: + Converted float or default value + + Examples: + >>> safe_float("123.45") + 123.45 + >>> safe_float("") + 0.0 + >>> safe_float(None) + 0.0 + >>> safe_float("invalid", 99.0) + 99.0 + """ + if value is None or value == "": + return default + try: + return float(value) + except (ValueError, TypeError): + return default + + # Target stock codes to monitor per market WATCHLISTS = { "KR": ["005930", "000660", "035420"], # Samsung, SK Hynix, NAVER @@ -77,16 +106,16 @@ async def trading_cycle( balance_data = await broker.get_balance() output2 = balance_data.get("output2", [{}]) - total_eval = float(output2[0].get("tot_evlu_amt", "0")) if output2 else 0 - total_cash = float( + total_eval = safe_float(output2[0].get("tot_evlu_amt", "0")) if output2 else 0 + total_cash = safe_float( balance_data.get("output2", [{}])[0].get("dnca_tot_amt", "0") if output2 else "0" ) - purchase_total = float(output2[0].get("pchs_amt_smtl_amt", "0")) if output2 else 0 + purchase_total = safe_float(output2[0].get("pchs_amt_smtl_amt", "0")) if output2 else 0 - current_price = float(orderbook.get("output1", {}).get("stck_prpr", "0")) - foreigner_net = float(orderbook.get("output1", {}).get("frgn_ntby_qty", "0")) + current_price = safe_float(orderbook.get("output1", {}).get("stck_prpr", "0")) + foreigner_net = safe_float(orderbook.get("output1", {}).get("frgn_ntby_qty", "0")) else: # Overseas market price_data = await overseas_broker.get_overseas_price( @@ -103,11 +132,11 @@ async def trading_cycle( else: balance_info = {} - total_eval = float(balance_info.get("frcr_evlu_tota", "0") or "0") - 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") + total_eval = safe_float(balance_info.get("frcr_evlu_tota", "0") or "0") + total_cash = safe_float(balance_info.get("frcr_dncl_amt_2", "0") or "0") + purchase_total = safe_float(balance_info.get("frcr_buy_amt_smtl", "0") or "0") - current_price = float(price_data.get("output", {}).get("last", "0")) + current_price = safe_float(price_data.get("output", {}).get("last", "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..d290e33 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,7 +6,43 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.core.risk_manager import CircuitBreakerTripped, FatFingerRejected -from src.main import trading_cycle +from src.main import safe_float, trading_cycle + + +class TestSafeFloat: + """Test safe_float() helper function.""" + + def test_converts_valid_string(self): + """Test conversion of valid numeric string.""" + assert safe_float("123.45") == 123.45 + assert safe_float("0") == 0.0 + assert safe_float("-99.9") == -99.9 + + def test_handles_empty_string(self): + """Test empty string returns default.""" + assert safe_float("") == 0.0 + assert safe_float("", 99.0) == 99.0 + + def test_handles_none(self): + """Test None returns default.""" + assert safe_float(None) == 0.0 + assert safe_float(None, 42.0) == 42.0 + + def test_handles_invalid_string(self): + """Test invalid string returns default.""" + assert safe_float("invalid") == 0.0 + assert safe_float("not_a_number", 100.0) == 100.0 + assert safe_float("12.34.56") == 0.0 + + def test_handles_float_input(self): + """Test float input passes through.""" + assert safe_float(123.45) == 123.45 + assert safe_float(0.0) == 0.0 + + def test_custom_default(self): + """Test custom default value.""" + assert safe_float("", -1.0) == -1.0 + assert safe_float(None, 999.0) == 999.0 class TestTradingCycleTelegramIntegration: