Files
kis_bot/kis_trader/engine/live_sell_price.py
Your Name 0ecac7cb95 이번에 들어간 내용
한투 호가 = 2번째 앱키 전용
키 없거나 start 실패 시 메인에 H0STASP0 안 붙임. 운영설정 WS_ORDERBOOK_SAVE_KIS 빨간 danger.

LS RAM 합집합
후보∪보유∪영구∪grace. sync_targets와 split reconcile 둘 다. 틱 DB 영구 게이트는 그대로.

분봉 쓰레기 → 다음 소스 봉 통째
그 분 틱 0건이거나 전부 봉끝 대비 LIVE_FEED_FALLBACK_MAX_AGE_SEC 초과면 구멍. 메인 WS → 2차 → LS → REST → rollup. CANDLE_GARBAGE_FALLBACK 기본 true.

파일: feed_fallback.py(신규), ws_manager.py, kis_ws.py, candle_series.py, bt_candle_source.py, live_config_schema.py, database.py, 스모크, MD 2개.

같은 ws_manager/database/kis_ws/live_config에는 직전 커밋 이후 쌓여 있던 시세 폴백·ENV 키 정리도 같이 들어갔습니다. 파일 단위로 나눌 수 없어서입니다.
2026-08-19 22:11:31 +09:00

200 lines
5.8 KiB
Python

#!/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, ""