diff --git a/src/analysis/smart_scanner.py b/src/analysis/smart_scanner.py index 0f4f8c8..9972a57 100644 --- a/src/analysis/smart_scanner.py +++ b/src/analysis/smart_scanner.py @@ -315,6 +315,11 @@ class SmartVolatilityScanner: logger.info("Overseas scanner: no symbol universe for %s", market.name) return [] + logger.info( + "Overseas scanner: scanning %d fallback symbols for %s", + len(symbols), + market.name, + ) candidates: list[ScanCandidate] = [] for stock_code in symbols: try: @@ -350,6 +355,11 @@ class SmartVolatilityScanner: logger.warning("Failed to analyze overseas %s: %s", stock_code, exc) except Exception as exc: logger.error("Unexpected error analyzing overseas %s: %s", stock_code, exc) + logger.info( + "Overseas symbol fallback scan found %d candidates for %s", + len(candidates), + market.name, + ) return candidates def get_stock_codes(self, candidates: list[ScanCandidate]) -> list[str]: diff --git a/src/broker/kis_api.py b/src/broker/kis_api.py index 15381c4..1499197 100644 --- a/src/broker/kis_api.py +++ b/src/broker/kis_api.py @@ -104,12 +104,14 @@ class KISBroker: time_since_last_attempt = now - self._last_refresh_attempt if time_since_last_attempt < self._refresh_cooldown: remaining = self._refresh_cooldown - time_since_last_attempt - error_msg = ( - f"Token refresh on cooldown. " - f"Retry in {remaining:.1f}s (KIS allows 1/minute)" + # Do not fail fast here. If token is unavailable, upstream calls + # will all fail for up to a minute and scanning returns no trades. + logger.warning( + "Token refresh on cooldown. Waiting %.1fs before retry (KIS allows 1/minute)", + remaining, ) - logger.warning(error_msg) - raise ConnectionError(error_msg) + await asyncio.sleep(remaining) + now = asyncio.get_event_loop().time() logger.info("Refreshing KIS access token") self._last_refresh_attempt = now diff --git a/src/broker/overseas.py b/src/broker/overseas.py index 541d2da..f1a3c43 100644 --- a/src/broker/overseas.py +++ b/src/broker/overseas.py @@ -82,14 +82,19 @@ class OverseasBroker: session = self._broker._get_session() if ranking_type == "volume": - tr_id = self._broker._settings.OVERSEAS_RANKING_VOLUME_TR_ID - path = self._broker._settings.OVERSEAS_RANKING_VOLUME_PATH + configured_tr_id = self._broker._settings.OVERSEAS_RANKING_VOLUME_TR_ID + configured_path = self._broker._settings.OVERSEAS_RANKING_VOLUME_PATH + default_tr_id = "HHDFS76200200" + default_path = "/uapi/overseas-price/v1/quotations/inquire-volume-rank" else: - tr_id = self._broker._settings.OVERSEAS_RANKING_FLUCT_TR_ID - path = self._broker._settings.OVERSEAS_RANKING_FLUCT_PATH + configured_tr_id = self._broker._settings.OVERSEAS_RANKING_FLUCT_TR_ID + configured_path = self._broker._settings.OVERSEAS_RANKING_FLUCT_PATH + default_tr_id = "HHDFS76200100" + default_path = "/uapi/overseas-price/v1/quotations/inquire-updown-rank" - headers = await self._broker._auth_headers(tr_id) - url = f"{self._broker._base_url}{path}" + endpoint_specs: list[tuple[str, str]] = [(configured_tr_id, configured_path)] + if (configured_tr_id, configured_path) != (default_tr_id, default_path): + endpoint_specs.append((default_tr_id, default_path)) # Try common param variants used by KIS overseas quotation APIs. param_variants = [ @@ -100,24 +105,38 @@ class OverseasBroker: ] last_error: str | None = None - for params in param_variants: - try: - async with session.get(url, headers=headers, params=params) as resp: - text = await resp.text() - if resp.status != 200: - last_error = f"HTTP {resp.status}: {text}" - continue + saw_http_404 = False + for tr_id, path in endpoint_specs: + headers = await self._broker._auth_headers(tr_id) + url = f"{self._broker._base_url}{path}" + for params in param_variants: + try: + async with session.get(url, headers=headers, params=params) as resp: + text = await resp.text() + if resp.status != 200: + last_error = f"HTTP {resp.status}: {text}" + if resp.status == 404: + saw_http_404 = True + continue - data = await resp.json() - rows = self._extract_ranking_rows(data) - if rows: - return rows[:limit] + data = await resp.json() + rows = self._extract_ranking_rows(data) + if rows: + return rows[:limit] - # keep trying another param variant if response has no usable rows - last_error = f"empty output (keys={list(data.keys())})" - except (TimeoutError, aiohttp.ClientError) as exc: - last_error = str(exc) - continue + # keep trying another param variant if response has no usable rows + last_error = f"empty output (keys={list(data.keys())})" + except (TimeoutError, aiohttp.ClientError) as exc: + last_error = str(exc) + continue + + if saw_http_404: + logger.warning( + "Overseas ranking endpoint unavailable (404) for %s/%s; using symbol fallback scan", + exchange_code, + ranking_type, + ) + return [] raise ConnectionError( f"fetch_overseas_rankings failed for {exchange_code}/{ranking_type}: {last_error}"