Files
kis_bot/kis_trader/share/stock_share.py
Hwang 61c72a8a4c feat(tests): 신규 키움 웹소켓 조건검색 및 실시간 조건검색 테스트 추가
변경 사항
----
- _test_kiwoom_condition_list.py: 키움 웹소켓 조건검색 '목록조회' 기능을 단독으로 테스트하는 스크립트 추가
- _test_kiwoom_condition_realtime.py: 'momentum' 조건식을 실시간으로 등록하고 초기 매칭 종목 리스트 및 실시간 편입/이탈을 수신하는 테스트 스크립트 추가
- _verify_columnar_bitid.py, _verify_shared_e2e_breakout.py, _verify_shared_e2e.py: 공유 메모리 및 dict 간의 데이터 일관성을 검증하는 테스트 추가

영향
----
- 신규 테스트 스크립트 추가로 키움 웹소켓 API의 기능 검증 및 안정성을 높임
- 기존 기능에 대한 영향 없음

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 01:27:00 +09:00

211 lines
6.6 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
전략 공통 유통주식수 메타 — 라이브(WSManager) · 백테(stock_share_meta 테이블) 공용.
키움 ka10001 만 ``dstr_stk``(유통주식수)를 제공한다. 일괄 API(ka10095/ka10099)는 상장주식수만
있어 회전율 분모로는 부적합 → 후보·보유 종목만 종목별 ka10001 호출 + DB 캐시.
"""
from __future__ import annotations
import datetime
from typing import Any, Dict, Iterable, List, Optional
from kis_trader.utils.env import get_env_from_db
from kis_trader.utils.logger import get_logger
logger = get_logger("kis_trader.share")
def share_denom_key() -> str:
"""회전율 분모 필드 — ``STOCK_SHARE_DENOM`` 우선, 레거시 ``BREAKOUT_SHARE_DENOM`` 폴백."""
raw = (
get_env_from_db("STOCK_SHARE_DENOM", "")
or get_env_from_db("BREAKOUT_SHARE_DENOM", "dstr_stk")
or "dstr_stk"
)
return str(raw).strip().lower()
def share_denom_from_meta(meta: Optional[Dict[str, int]]) -> float:
"""
캐시/DB 행에서 회전율 분모(float) 반환.
유통(`dstr_stk`) 우선 설정이어도 값이 없으면 상장(`flo_stk`) 폴백.
"""
if not meta:
return 0.0
key = share_denom_key()
flo = float(meta.get("flo_stk") or 0)
dstr = float(meta.get("dstr_stk") or 0)
if key in ("flo_stk", "flo", "listed", "상장"):
return flo if flo > 0 else dstr
return dstr if dstr > 0 else flo
def upsert_stock_share_meta_row(
db,
code: str,
flo_stk: int,
dstr_stk: int,
*,
dstr_rt: Optional[float] = None,
source: str = "ka10001",
) -> None:
"""stock_share_meta 테이블에 즉시 저장 (atomic)."""
code = str(code or "").strip()
if not code or not db:
return
now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
with db.conn:
db.conn.execute(
"""
INSERT INTO stock_share_meta
(code, flo_stk, dstr_stk, dstr_rt, source, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
flo_stk = VALUES(flo_stk),
dstr_stk = VALUES(dstr_stk),
dstr_rt = COALESCE(VALUES(dstr_rt), dstr_rt),
source = VALUES(source),
updated_at = VALUES(updated_at)
""",
(
code,
int(flo_stk or 0),
int(dstr_stk or 0),
float(dstr_rt) if dstr_rt is not None else None,
str(source or "ka10001"),
now_str,
),
)
except Exception as e:
logger.warning("stock_share_meta 저장 실패 (%s): %s", code, e)
def load_stock_share_meta_map(
db,
codes: Optional[Iterable[str]] = None,
) -> Dict[str, Dict[str, int]]:
"""DB → {code: {flo_stk, dstr_stk}}."""
out: Dict[str, Dict[str, int]] = {}
if not db:
return out
try:
if codes is not None:
code_list = sorted({str(c).strip() for c in codes if c})
if not code_list:
return out
placeholders = ",".join("?" * len(code_list))
rows = db.conn.execute(
f"""
SELECT code, flo_stk, dstr_stk
FROM stock_share_meta
WHERE code IN ({placeholders})
""",
code_list,
).fetchall()
else:
rows = db.conn.execute(
"SELECT code, flo_stk, dstr_stk FROM stock_share_meta",
).fetchall()
for r in rows:
code = str(r["code"]).strip()
out[code] = {
"flo_stk": int(r["flo_stk"] or 0),
"dstr_stk": int(r["dstr_stk"] or 0),
}
except Exception as e:
logger.debug("stock_share_meta 로드 실패: %s", e)
return out
def load_share_denom_map(
db,
codes: Iterable[str],
) -> Dict[str, float]:
"""백테스트용 — 종목별 회전율 분모."""
meta = load_stock_share_meta_map(db, codes)
return {c: share_denom_from_meta(meta.get(c)) for c in codes}
def merge_share_meta_into_cache(
cache: Dict[str, Dict[str, int]],
db_map: Dict[str, Dict[str, int]],
) -> None:
"""메모리 캐시에 DB 맵 병합."""
for code, info in db_map.items():
if code not in cache:
cache[code] = dict(info)
continue
for k in ("flo_stk", "dstr_stk"):
v = int(info.get(k) or 0)
if v > 0:
cache[code][k] = v
def codes_missing_dstr(
cache: Dict[str, Dict[str, int]],
codes: Iterable[str],
) -> List[str]:
"""유통주식수가 아직 없는 종목 (ka10001 대상)."""
missing: List[str] = []
for raw in codes:
code = str(raw or "").strip()
if not code:
continue
info = cache.get(code)
if not info or int(info.get("dstr_stk") or 0) <= 0:
missing.append(code)
return missing
def apply_fetched_meta(
cache: Dict[str, Dict[str, int]],
db,
code: str,
meta: Dict[str, int],
*,
dstr_rt: Optional[float] = None,
) -> None:
"""ka10001 응답 → RAM + DB."""
code = str(code or "").strip()
if not code or not meta:
return
flo = int(meta.get("flo_stk") or 0)
dstr = int(meta.get("dstr_stk") or 0)
if flo <= 0 and dstr <= 0:
return
cache[code] = {"flo_stk": flo, "dstr_stk": dstr}
upsert_stock_share_meta_row(db, code, flo, dstr, dstr_rt=dstr_rt, source="ka10001")
def share_denom_for_code(params: Dict[str, Any], code: str) -> float:
"""백테/라이브 공통 — params 또는 share_denom_by_code 에서 분모 조회."""
by_code = params.get("share_denom_by_code") or {}
if code in by_code:
v = float(by_code.get(code) or 0)
if v > 0:
return v
return float(params.get("share_denom", 0) or 0)
def attach_share_denoms_to_params(
params: Dict[str, Any],
db,
codes: Iterable[str],
) -> Dict[str, Any]:
"""백테 엔진 params에 ``share_denom_by_code`` 주입."""
out = dict(params)
code_list = list(codes)
if not code_list:
out.setdefault("share_denom_by_code", {})
return out
out["share_denom_by_code"] = load_share_denom_map(db, code_list)
missing = [c for c, v in out["share_denom_by_code"].items() if v <= 0]
if missing:
logger.info(
" stock_share_meta 미적재 %d종목 — 회전율 필터는 해당 종목 스킵 "
"(fill_stock_share_meta.py 로 보강 가능)",
len(missing),
)
return out