Files
kis_trader/remove/legacy_root/test_psearch.py
Your Name 6d2a706a48 커밋 1 — 실매 가격 TTL 구멍 (본체)
왜: 체결이 없어도 마지막가는 유지인데, TTL로 None 만들고 매도/EOD를 건너뛰어 8/5 돌파·금요일 leftover가 남음. 호가필터 TTL 구멍과 같은 병.

넣을 파일

신규: kis_trader/engine/live_sell_price.py
kis_trader/strategies/base.py (_ws_last_quote, _resolve_sell_price)
전략: momentum.py scalping.py tail_catch.py breakout.py range_break.py dart_strategy.py updow_strategy.py updown_feed.py us_momentum.py
WS: ws_manager.py kis_ws.py kiwoom_ws.py ls_ws.py kis_ws_overseas.py
kis_trader/web/live_config_schema.py (WS_PRICE_MAX_AGE_SEC 기본 0)
database.py (키 주석 + legacy/ sys.path)
kis_trader/execution/order_manager.py (잔고 있는데 40240000 ghost_purge 금지 — 같은 EOD 사고)
EOD가 min_hold에 안 막히게 손본 momentum_hts_logic.py / scalping_engine.py / tail_engine.py (이 대화에서 손본 부분만 확인 후)
문서: docs/like_mcp.md/db_erd.md code_architecture.md (가격 TTL 문구)
메시지 초안

fix: 매수·매도 현재가를 TTL로 버리지 않음 (마지막 RAM)
횡보·체결 공백을 죽은 캐시로 오인해 None 처리하면 손절·EOD가 스킵된다.
호가필터와 같이 나이는 무시하고 마지막 체결가를 유지한다. EOD는 매수가 폴백.
영향: 실매 O / 백테·옵투나 봉 경로 거의 무관 (엔진 식 변경 아님)

커밋 2 — 루트 정리 (remove/ vs legacy/)
왜: 루트 단독봇·테스트는 지울 보관함으로. 웹·알람이 아직 쓰는 모듈은 remove에 두면 나중에 폴더째 삭제 때 깨짐.

넣을 파일

이동: 미사용 → remove/legacy_root/ (래퍼, ETF/키움 옛봇, 테스트, kiwoom_rest_api 등)
이동: 사용 중 → legacy/ (holding_bot kis_holding_ver1 news_analyzer kis_long_ver1/2)
신규: kis_trader/utils/legacy_root.py legacy/README.md remove/README.md
import 경로: backtest_web.py mm_butler.py mm_remote.py updow_holding_cfg.py dbband_stock_cfg.py param_search_updow*.py dbband_param_search.py param_search_apply_snapshot.py verify_three_paths.py
docs/like_mcp.md/code_architecture.md 수동 노트
메시지 초안

chore: 미사용 루트는 remove/, 웹·알람 구모듈은 legacy/
remove는 나중에 통째 삭제 예정. holding_bot·news_analyzer·kis_long은
ensure_legacy_root로 legacy/만 본다.
빼기: scratch/set_ws_price_max_age_zero.py (일회성)
2026-08-18 00:13:17 +09:00

300 lines
13 KiB
Python

#!/usr/bin/env python3
"""
test_psearch.py — KIS 종목조건검색 API 응답 확인용 단독 스크립트
==================================================================
[국내주식] 시세분석
① psearch-title (HHKST03900300) — 서버 저장 조건식 목록 조회
② psearch-result (HHKST03900400) — 특정 조건식 현재 결과 조회
⚠️ KIS 종목조건검색은 **모의투자 미지원**이라 실키(KIS_APP_KEY_REAL /
KIS_APP_SECRET_REAL) + 실전 도메인으로만 호출 가능.
본 스크립트는 KIS_MOCK 값과 무관하게 항상 실전 키로만 호출한다.
이 스크립트의 목적:
- 두 API 의 **요청 (URL/헤더/params)** 과 **응답 (status / 응답헤더 /
원본 JSON)** 을 그대로 찍어서 필드명·구조를 사람이 직접 검증.
- condition_manager 가 파싱하기 전 원본 응답을 보고 매핑 정확도 점검.
사용:
# 1) 목록만 보고 끝내기
$ python test_psearch.py --list
# 2) 목록 출력 후 seq 인터랙티브 입력
$ python test_psearch.py
# 3) 특정 seq 결과까지 한 번에
$ python test_psearch.py --seq 0
$ python test_psearch.py --seq 3 --user-id mylogin # HTS ID 임시 지정
옵션:
--seq <번호> 결과조회 대상 조건식 번호 (없으면 인터랙티브)
--user-id <ID> KIS_HTS_ID 대신 임시 지정 (테스트용)
--list 목록(psearch-title)만 호출하고 종료
--raw 응답 JSON 을 indent 없이 한 줄로 출력 (grep 용이)
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(SCRIPT_DIR))
# kis_trader 패키지의 검증된 헬퍼들을 그대로 재사용 (별도 토큰/SafeRequest 구현 X)
from kis_trader.execution.kis_client import KISClient # noqa: E402
from kis_trader.utils.env import get_env_from_db # noqa: E402
# ──────────────────────────────────────────────────────────────────
# 출력 헬퍼
# ──────────────────────────────────────────────────────────────────
def _mask(s: str, head: int = 6, tail: int = 4) -> str:
"""앱키 같은 시크릿을 로그에 찍을 때 앞뒤만 남기고 가린다."""
if not s:
return "(empty)"
if len(s) <= head + tail:
return "*" * len(s)
return f"{s[:head]}{s[-tail:]}"
def _print_section(title: str) -> None:
print()
print("=" * 78)
print(f" {title}")
print("=" * 78)
def _dump_request(url: str, headers: dict, params: dict) -> None:
"""요청 정보 출력. authorization/appkey/appsecret 은 마스킹."""
print(f"[REQUEST]")
print(f" URL : {url}")
print(f" PARAMS : {json.dumps(params, ensure_ascii=False)}")
safe_h = dict(headers)
if "authorization" in safe_h:
tok = safe_h["authorization"].replace("Bearer ", "")
safe_h["authorization"] = "Bearer " + _mask(tok, 8, 6)
if "appkey" in safe_h:
safe_h["appkey"] = _mask(safe_h["appkey"])
if "appsecret" in safe_h:
safe_h["appsecret"] = _mask(safe_h["appsecret"])
print(f" HEADERS: {json.dumps(safe_h, ensure_ascii=False)}")
def _dump_response(r, raw: bool) -> dict:
"""응답 정보 출력 + 파싱한 dict 반환 (실패 시 빈 dict)."""
print(f"[RESPONSE]")
print(f" STATUS : {r.status_code}")
interesting_resp_headers = {
k: v for k, v in r.headers.items()
if k.lower() in (
"content-type", "tr_id", "tr_cont", "gt_uid",
"rate-limit-remaining", "rate-limit-reset",
)
}
if interesting_resp_headers:
print(f" HEADERS: {json.dumps(interesting_resp_headers, ensure_ascii=False)}")
try:
j = r.json()
except Exception as e:
print(f" BODY : <JSON 파싱 실패: {e}>")
print(f" TEXT : {r.text[:1000]}")
return {}
print(f" rt_cd : {j.get('rt_cd')} (0=정상, 그 외=에러)")
print(f" msg_cd : {j.get('msg_cd')}")
print(f" msg1 : {j.get('msg1')}")
print()
print("[RAW BODY]")
if raw:
print(json.dumps(j, ensure_ascii=False))
else:
print(json.dumps(j, ensure_ascii=False, indent=2))
return j
# ──────────────────────────────────────────────────────────────────
# 핵심: 두 API 호출 (raw)
# ──────────────────────────────────────────────────────────────────
def call_psearch_title(client: KISClient, user_id: str, raw: bool) -> list:
"""
psearch-title (HHKST03900300) 호출 + 응답 출력.
Returns: parsed output2 list (사용자가 보기 쉽게 seq/이름 표 만들기용)
"""
_print_section("① psearch-title — 서버 저장 조건식 목록")
path = "/uapi/domestic-stock/v1/quotations/psearch-title"
tr_id = "HHKST03900300"
params = {"user_id": user_id}
headers = client._headers(tr_id)
url = client.base_url + path
_dump_request(url, headers, params)
r = client._get(path, tr_id, params)
j = _dump_response(r, raw)
out2 = j.get("output2") or []
if isinstance(out2, dict):
out2 = [out2]
if out2:
print()
print("[요약 — 사용자에게 의미 있는 필드만]")
print(f"{len(out2)}")
# KIS 실제 응답 키: condition_nm / grp_nm
# (문서엔 condition_name 처럼 적힌 자료가 많아 폴백 후보 다중 적용)
print(" ┌─────┬──────────────────────────────┬────────────────────┐")
print(" │ seq │ condition_nm │ grp_nm │")
print(" ├─────┼──────────────────────────────┼────────────────────┤")
for row in out2:
seq = str(row.get("seq", "?"))[:3].rjust(3)
nm = (
row.get("condition_nm")
or row.get("condition_name")
or row.get("cond_nm")
or ""
)
grp = (row.get("grp_nm") or row.get("group_nm") or "")
print(
f"{seq}{str(nm)[:28].ljust(28)}"
f"{str(grp)[:18].ljust(18)}"
)
print(" └─────┴──────────────────────────────┴────────────────────┘")
return out2
def call_psearch_result(
client: KISClient, user_id: str, seq: str, raw: bool,
) -> list:
"""
psearch-result (HHKST03900400) 호출 + 응답 출력.
Returns: parsed output2 list
"""
_print_section(f"② psearch-result — 조건식 결과 (seq={seq})")
path = "/uapi/domestic-stock/v1/quotations/psearch-result"
tr_id = "HHKST03900400"
params = {"user_id": user_id, "seq": str(seq)}
headers = client._headers(tr_id)
url = client.base_url + path
_dump_request(url, headers, params)
r = client._get(path, tr_id, params)
j = _dump_response(r, raw)
out2 = j.get("output2") or []
if isinstance(out2, dict):
out2 = [out2]
if out2:
print()
print("[요약 — 가능한 키 후보 모두 시도해서 추출]")
print(f"{len(out2)}종목")
print(" ┌────────┬──────────────────────────────┬──────────────┐")
print(" │ code │ name │ price │")
print(" ├────────┼──────────────────────────────┼──────────────┤")
for it in out2[:50]: # 상위 50개만
code = (
it.get("code") or it.get("stck_shrn_iscd")
or it.get("mksc_shrn_iscd") or ""
)
name = (
it.get("name") or it.get("hts_kor_isnm") or ""
)
price = (
it.get("stck_prpr") or it.get("price") or ""
)
print(
f"{str(code)[:6].ljust(6)}"
f"{str(name)[:28].ljust(28)}"
f"{str(price)[:12].rjust(12)}"
)
if len(out2) > 50:
print(f" …(이하 {len(out2) - 50}종목 생략)")
print(" └────────┴──────────────────────────────┴──────────────┘")
return out2
# ──────────────────────────────────────────────────────────────────
# 메인
# ──────────────────────────────────────────────────────────────────
def main() -> int:
parser = argparse.ArgumentParser(
description="KIS 종목조건검색(psearch-title / psearch-result) 응답 확인",
)
parser.add_argument("--seq", help="조건식 번호 (지정 시 결과조회까지 자동 실행)")
parser.add_argument("--user-id", help="KIS_HTS_ID 대신 임시 지정")
parser.add_argument("--list", action="store_true", help="목록만 호출하고 종료")
parser.add_argument("--raw", action="store_true", help="응답 JSON 을 한 줄로 출력")
args = parser.parse_args()
# ── HTS ID 결정 ──
user_id = (args.user_id or get_env_from_db("KIS_HTS_ID", "") or "").strip()
if not user_id:
print("❌ KIS_HTS_ID 미설정.")
print(" update_env_simple.py 로 KIS_HTS_ID 를 DB 에 저장하거나,")
print(" --user-id <ID> 옵션으로 임시 지정하세요.")
return 1
# ── 실키 검증 (모의 미지원) ──
real_key = (get_env_from_db("KIS_APP_KEY_REAL", "") or "").strip()
real_secret = (get_env_from_db("KIS_APP_SECRET_REAL", "") or "").strip()
if not real_key or not real_secret:
print("❌ KIS_APP_KEY_REAL / KIS_APP_SECRET_REAL 미설정.")
print(" psearch 는 모의투자 미지원이라 실키가 필수입니다.")
return 1
print(f"🔑 KIS_APP_KEY_REAL : {_mask(real_key)}")
print(f"🔑 KIS_APP_SECRET : {_mask(real_secret)}")
print(f"🪪 KIS_HTS_ID : {user_id}")
# ── 실전 도메인 KISClient (mock=False 강제) ──
try:
client = KISClient(
mock=False,
app_key=real_key,
app_secret=real_secret,
account_no=(get_env_from_db("KIS_ACCOUNT_NO_REAL", "") or "").strip(),
account_code=(get_env_from_db("KIS_ACCOUNT_CODE_REAL", "01") or "01").strip(),
)
except Exception as e:
print(f"❌ KISClient 초기화 실패: {e}")
return 1
if not client._token:
print("❌ 접근토큰 발급 실패. kis_token_manager 로그/실키 유효성을 확인하세요.")
return 1
print(f"🎫 access_token : {_mask(client._token, 10, 6)}")
print(f"🌐 base_url : {client.base_url}")
# ── ① 조건식 목록 ──
titles = call_psearch_title(client, user_id, args.raw)
if args.list:
return 0
# ── seq 결정 ──
seq = (args.seq or "").strip() if args.seq else ""
if not seq:
if not titles:
print()
print("⚠️ 조건식 목록이 비어 결과조회 단계로 진행할 수 없습니다.")
print(" HTS/MTS 에 먼저 조건식을 저장한 뒤 다시 실행하세요.")
return 0
print()
try:
seq = input(f"조회할 seq 입력 (예: {titles[0].get('seq', '0')}, 비우면 종료) > ").strip()
except EOFError:
seq = ""
if not seq:
print("⏹ 사용자 취소")
return 0
# ── ② 조건식 결과 ──
call_psearch_result(client, user_id, seq, args.raw)
return 0
if __name__ == "__main__":
sys.exit(main())