Files
kis_bot/kis_trader/engine/limit_entry_common.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

197 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
"""
kis_trader/engine/limit_entry_common.py — ATR 지정가 진입(C안) 공통
꼬리(SHORT)·하락매수(UPDOW): 신호봉 확정 후 지정가 1회 → 유효 N봉 내 low 터치 시 체결,
미체결 시 취소(실매) / 백테 스킵.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Tuple
from ..utils.env import get_env_float, get_env_from_db, get_env_int
ENTRY_ALIGN = "align"
ENTRY_LIMIT_ATR = "limit_atr"
def short_entry_mode(params: Optional[Dict[str, Any]] = None) -> str:
if params is not None and params.get("entry_mode") is not None:
return str(params.get("entry_mode") or ENTRY_ALIGN).strip().lower()
return str(
get_env_from_db("TAIL_ENTRY_MODE", ENTRY_LIMIT_ATR) or ENTRY_LIMIT_ATR,
).strip().lower()
def updow_entry_mode(cfg: Optional[Dict[str, Any]] = None) -> str:
if cfg is not None and cfg.get("entry_mode") is not None:
return str(cfg.get("entry_mode") or ENTRY_ALIGN).strip().lower()
return str(
get_env_from_db("UPDOW_ENTRY_MODE", ENTRY_LIMIT_ATR) or ENTRY_LIMIT_ATR,
).strip().lower()
def is_limit_atr_entry(mode: str) -> bool:
return str(mode or "").strip().lower() in (ENTRY_LIMIT_ATR, "limit", "atr_limit")
def _limit_mult(params: Optional[Dict[str, Any]], prefix: str) -> float:
key = f"{prefix}_LIMIT_ATR_MULT"
if params is not None and params.get("limit_atr_mult") is not None:
# 0.0(저점 그대로 체결)도 유효값 — `or 1.5` 로 덮지 않도록 직접 float 변환
raw = params.get("limit_atr_mult")
if str(raw).strip() != "":
try:
return float(raw)
except (TypeError, ValueError):
pass
return get_env_float(key, get_env_float("LIMIT_ATR_MULT_DEFAULT", 1.5))
def _limit_anchor(params: Optional[Dict[str, Any]], prefix: str) -> str:
key = f"{prefix}_LIMIT_ANCHOR"
if params is not None and params.get("limit_anchor") is not None:
return str(params.get("limit_anchor") or "signal_low").strip().lower()
return str(get_env_from_db(key, "signal_low") or "signal_low").strip().lower()
def _limit_valid_bars(params: Optional[Dict[str, Any]], prefix: str) -> int:
key = f"{prefix}_LIMIT_VALID_BARS"
if params is not None and params.get("limit_valid_bars") is not None:
return max(1, int(params.get("limit_valid_bars") or 1))
return max(1, get_env_int(key, 1))
def _limit_fill_slip_pct(params: Optional[Dict[str, Any]], prefix: str) -> float:
key = f"{prefix}_LIMIT_FILL_SLIP_PCT"
if params is not None and params.get("limit_fill_slip_pct") is not None:
return float(params.get("limit_fill_slip_pct") or 0.0)
return get_env_float(key, 0.0)
def tail_limit_params(params: Dict[str, Any]) -> Dict[str, Any]:
return {
"mult": _limit_mult(params, "TAIL"),
"anchor": _limit_anchor(params, "TAIL"),
"valid_bars": _limit_valid_bars(params, "TAIL"),
"fill_slip_pct": _limit_fill_slip_pct(params, "TAIL"),
}
def updow_limit_params(cfg: Dict[str, Any]) -> Dict[str, Any]:
return {
"mult": _limit_mult(cfg, "UPDOW"),
"anchor": _limit_anchor(cfg, "UPDOW"),
"valid_bars": _limit_valid_bars(cfg, "UPDOW"),
"fill_slip_pct": _limit_fill_slip_pct(cfg, "UPDOW"),
}
def floor_limit_price_krw(price: float) -> int:
"""지정가(원) — 정수 호가."""
if price <= 0:
return 0
return max(1, int(price))
def resolve_limit_anchor_price(
anchor: str,
sig_bar: Dict[str, Any],
candles: List[Dict[str, Any]],
sig_i: int,
) -> float:
"""신호봉 기준 anchor 가격."""
mode = (anchor or "signal_low").strip().lower()
if mode == "signal_close":
return float(sig_bar.get("close") or 0)
if mode == "prev_close" and sig_i > 0:
return float(candles[sig_i - 1].get("close") or 0)
lo = float(sig_bar.get("low") or 0)
if lo > 0:
return lo
return float(sig_bar.get("close") or 0)
def compute_atr_limit_price(
anchor_px: float,
atr: Optional[float],
mult: float,
*,
min_price: float = 0.0,
) -> float:
"""anchor ATR×mult 지정가 (매수 대기)."""
if anchor_px <= 0:
return 0.0
a = float(atr or 0)
if a <= 0:
a = anchor_px * 0.01
m = float(mult) if float(mult) > 0 else 1.5
lp = anchor_px - a * m
if min_price > 0 and lp < min_price:
return 0.0
if lp <= 0:
return 0.0
return lp
def limit_valid_until_bar_key(candles: List[Dict], sig_i: int, valid_bars: int) -> str:
"""체결 허용 **마지막 봉** 의 candle_time (이 봉까지 low≤지정가면 체결)."""
j = sig_i + max(1, int(valid_bars))
if j >= len(candles):
j = len(candles) - 1
return str(candles[j].get("candle_time") or "")[:12]
def limit_cancel_after_bar_key(valid_until_key: str) -> str:
"""
이 키 **초과** 봉이 나오면 미체결 취소.
valid_until=신호+1봉(3분 1개) → 그 다음 봉 시각부터 취소.
"""
return str(valid_until_key or "")[:12]
def should_cancel_unfilled_limit(
latest_bar_key: str,
valid_until_key: str,
) -> bool:
"""최신 확정봉 시각이 유효 마지막 봉보다 크면 → 유효기간 종료, 취소."""
lb = str(latest_bar_key or "")[:12]
vu = str(valid_until_key or "")[:12]
if not lb or not vu:
return False
return lb > vu
def try_limit_fill_on_bar(
bar: Dict[str, Any],
limit_price: float,
fill_slip_pct: float = 0.0,
) -> Optional[float]:
"""백테: 해당 봉 low가 지정가 이하면 체결 (슬리피지는 불리하게만)."""
if limit_price <= 0:
return None
lo = float(bar.get("low") or bar.get("close") or 0)
if lo <= 0 or lo > limit_price:
return None
slip = float(fill_slip_pct or 0.0)
if slip > 0:
return limit_price * (1.0 + slip / 100.0)
return limit_price
def merge_limit_into_signal(
sig: Dict[str, Any],
*,
limit_price: float,
valid_until_key: str,
signal_bar_key: str,
) -> Dict[str, Any]:
out = dict(sig)
out["entry_mode"] = ENTRY_LIMIT_ATR
out["limit_price"] = limit_price
out["use_limit_buy"] = True
out["valid_until_bar_key"] = valid_until_key
out["signal_bar_key"] = signal_bar_key
out["entry_price"] = limit_price
return out