#!/usr/bin/env python3 """ kis_trader/engine/live_sell_price.py — 실매 매도 현재가 ================================================================================ 판정 = WS 읽기 체인(메인 2초 실패→2차→3차). 한투 inquire_price 60초 캐시 금지. 1·2·3 전부 실패일 때만 키움 ka10007 REST 1회(쿨다운). EOD = 마지막 WS → REST → 매수가 폴백. """ from __future__ import annotations import threading import time from typing import Any, Callable, Optional, Tuple from kis_trader.utils.env import get_env_float _stale_rest_lock = threading.Lock() _stale_rest_ts: dict = {} _stale_log_ts: dict = {} def _stck_prpr(raw: Any) -> float: if raw is None: return 0.0 if isinstance(raw, dict): v = raw.get("stck_prpr") or raw.get("price") or 0 else: v = raw try: return abs(float(str(v).replace(",", ""))) except (TypeError, ValueError): return 0.0 def _ws_chain_price(ws: Any, code: str) -> Tuple[float, str]: """get_price 생략=2초 체인. 성공 시 (px, vendor).""" getter = getattr(ws, "get_price", None) if not callable(getter): return 0.0, "" try: wsd = getter(code) except TypeError: try: wsd = getter(code, max_age_sec=0) except Exception: wsd = None except Exception: wsd = None px = _stck_prpr(wsd) vendor = "" if isinstance(wsd, dict): vendor = str(wsd.get("_feed_vendor") or "").strip() if not vendor: lab = getattr(ws, "get_tick_feed_label", None) if callable(lab): try: vendor = str(lab(code) or "").strip() except Exception: vendor = "" if px > 0: return px, vendor or "WS" return 0.0, "" def _ws_last_and_age(ws: Any, code: str) -> Tuple[float, float]: """마지막 RAM 체결가 + 나이(초). 캐시 없으면 (0, inf). EOD 전용.""" getter = getattr(ws, "get_price", None) if not callable(getter): return 0.0, float("inf") wsd = None try: wsd = getter(code, max_age_sec=None) except TypeError: try: wsd = getter(code) except Exception: wsd = None except Exception: wsd = None px = _stck_prpr(wsd) age = float("inf") if isinstance(wsd, dict): try: age = float(wsd.get("_age_ms") or 0) / 1000.0 except (TypeError, ValueError): age = 0.0 if px > 0 else float("inf") elif px > 0: age = 0.0 return px, age def _kiwoom_rest_once(ws: Any, code: str) -> float: fn = getattr(ws, "inquire_kiwoom_rest_price", None) if not callable(fn): return 0.0 try: return abs(float(fn(code) or 0.0)) except (TypeError, ValueError): return 0.0 except Exception: return 0.0 def _stale_rest_allowed(code: str, cooldown_sec: float) -> bool: now = time.time() with _stale_rest_lock: last = float(_stale_rest_ts.get(code, 0.0) or 0.0) if last > 0.0 and (now - last) < float(cooldown_sec): return False _stale_rest_ts[code] = now return True def _log_throttled(logger: Any, key: str, msg: str, *args: Any) -> None: if logger is None: return now = time.time() last = float(_stale_log_ts.get(key, 0.0) or 0.0) if now - last < 60.0: return _stale_log_ts[key] = now try: logger.info(msg, *args) except Exception: pass def resolve_live_sell_price( ws: Any, inquire_price: Optional[Callable], code: str, *, is_eod: bool = False, fallback_price: float = 0.0, logger: Any = None, ) -> Tuple[float, str]: """ 실매 매도용 현재가. Returns: (price, src) src = kiwoom|kis|ls|kiwoom_rest|EOD_FALLBACK|"" inquire_price 인자는 호환용으로 받지만 한투 60초 캐시는 쓰지 않음. """ code = str(code or "").strip() fb = float(fallback_price or 0.0) stale_sec = float(get_env_float("SELL_WS_STALE_REST_SEC", 1.0) or 0.0) cooldown = float(get_env_float("SELL_WS_STALE_REST_COOLDOWN_SEC", 30.0) or 0.0) if cooldown < 1.0: cooldown = 1.0 _ = inquire_price # 한투 경로 사용 금지 (시그니처 유지) if is_eod: ws_px, _age = _ws_last_and_age(ws, code) if ws_px > 0: return ws_px, "WS" if stale_sec > 0 and _stale_rest_allowed(code, cooldown): rest_px = _kiwoom_rest_once(ws, code) if rest_px > 0: return rest_px, "kiwoom_rest" if fb > 0: _log_throttled( logger, "eod_fb:" + code, "📌 [EOD가격] %s WS·키움REST 없음 → 매수가 폴백 %.0f (시장가 청산)", code, fb, ) return fb, "EOD_FALLBACK" return 0.0, "" chain_px, vendor = _ws_chain_price(ws, code) if chain_px > 0: return chain_px, vendor or "WS" if stale_sec > 0 and _stale_rest_allowed(code, cooldown): rest_px = _kiwoom_rest_once(ws, code) if rest_px > 0: _log_throttled( logger, "stale_rest:" + code, "📌 [매도시세] %s 1·2·3차 실패 → 키움 REST(ka10007) %.0f", code, rest_px, ) lab = getattr(ws, "_remember_feed_read", None) if callable(lab): try: lab("tick", code, "kiwoom_rest") except Exception: pass return rest_px, "kiwoom_rest" _log_throttled( logger, "stale_keep:" + code, "📌 [매도시세] %s 키움 REST 실패·쿨다운 — 가격 없음", code, ) return 0.0, ""