diff --git a/agents/stock/workspace/scripts/kiwoom_client.py b/agents/stock/workspace/scripts/kiwoom_client.py index d402090..2253a8d 100644 --- a/agents/stock/workspace/scripts/kiwoom_client.py +++ b/agents/stock/workspace/scripts/kiwoom_client.py @@ -13,9 +13,12 @@ from __future__ import annotations import json +import http.client +import socket import sys import threading import time +import urllib.error import urllib.request from datetime import datetime, timezone, timedelta from pathlib import Path @@ -48,6 +51,8 @@ TR_MINUTE_CHART = 'ka10080' # 주식분봉차트조회 (1/3/5/10/15/30/45/60분 TR_INVESTOR_FLOW = 'ka10059' # 종목별투자자기관별 (외국인·기관 일별 매매) STOCK_CODES_CACHE = STATE_DIR / 'stock_codes.json' +_HTTP_TRANSIENT_RETRIES = 2 +_HTTP_TRANSIENT_BACKOFF_SEC = 0.8 def load_credentials() -> dict: @@ -86,13 +91,28 @@ def _http_post_full(url: str, body: dict, headers: dict | None = None) -> tuple[ if headers: h.update(headers) req = urllib.request.Request(url, data=data, method='POST', headers=h) - try: - with urllib.request.urlopen(req, timeout=15) as r: - resp_headers = {k.lower(): v for k, v in r.headers.items()} - return json.loads(r.read().decode('utf-8', 'ignore')), resp_headers - except urllib.error.HTTPError as e: - body_text = e.read().decode('utf-8', 'ignore') - raise RuntimeError(f'HTTP {e.code} {url}: {body_text}') from e + transient_errors = ( + ConnectionResetError, + TimeoutError, + socket.timeout, + urllib.error.URLError, + http.client.RemoteDisconnected, + http.client.IncompleteRead, + ) + for attempt in range(_HTTP_TRANSIENT_RETRIES + 1): + try: + with urllib.request.urlopen(req, timeout=15) as r: + resp_headers = {k.lower(): v for k, v in r.headers.items()} + return json.loads(r.read().decode('utf-8', 'ignore')), resp_headers + except urllib.error.HTTPError as e: + body_text = e.read().decode('utf-8', 'ignore') + raise RuntimeError(f'HTTP {e.code} {url}: {body_text}') from e + except transient_errors: + if attempt >= _HTTP_TRANSIENT_RETRIES: + raise + time.sleep(_HTTP_TRANSIENT_BACKOFF_SEC * (attempt + 1)) + + raise RuntimeError(f'HTTP POST failed unexpectedly: {url}') def _token_cache_path(label: str) -> Path: diff --git a/agents/stock/workspace/sim/sim_web.py b/agents/stock/workspace/sim/sim_web.py index ef702f8..8a7f821 100644 --- a/agents/stock/workspace/sim/sim_web.py +++ b/agents/stock/workspace/sim/sim_web.py @@ -68,25 +68,31 @@ PTR_HTML = """ # 전체 리로드여도 보던 화면이 유지되게 한다 (자동·당겨서·수동 새로고침 공통). REFRESH_JS = """ """ @@ -1854,7 +1858,8 @@ def _decision_card(d, watch_n: int = 0, held_n: int = 0, acc_detail: str = '', b def render_html(view: str | None = None, day_date: str | None = None, - tab: str | None = None, sub: str | None = None) -> str: + tab: str | None = None, sub: str | None = None, + wsub: str | None = None) -> str: snap = _load(config.LAST_SCAN_PATH, {}) from . import variants as vmod _cv = [c for c in (_load(vmod.COMPARE_PATH, {}) or {}).get('variants', []) @@ -2050,7 +2055,8 @@ def render_html(view: str | None = None, day_date: str | None = None, f'[{_BEAR_WORD[_bear_mode(_vp)]}{_rx}{_im}{_av}] · {_esc(_cat_name(_vp))}') view_sel = (f'