Files
kis_bot/kis_trader/utils/non_stock.py
2026-07-30 18:05:07 +09:00

158 lines
4.6 KiB
Python

"""
비본주(우선주·ETF·ETN·스팩) 판별 — 후보 매수 차단 공용.
키움 조건검색은 종목명 없이 code 만 주는 경우가 많아(name==code),
이름 끝 '' 만으로는 우선주를 못 막는다. 코드 끝자리 규칙으로 보완한다.
KRX 단축코드 끝자리(종목구분):
- 보통주: 대개 '0'
- 우선주: '5','7','9' (구형) + 'K','L','M'… (2013년 이후 알파벳)
"""
from __future__ import annotations
from typing import FrozenSet
from .env import get_env_bool, get_env_from_db
# ETF/ETN 브랜드·유형 (이름 부분일치). '파워' 단독은 뉴파워프라즈마 오탐 → 넣지 않음.
_DEFAULT_NON_STOCK_NAME_KEYWORDS = (
"ETN",
"ETF",
"레버리지",
"인버스",
"2X",
"3X",
"선물",
"KODEX",
"TIGER",
"KBSTAR",
"ARIRANG",
"HANARO",
"SOL ",
"KOSEF",
"ACE ",
"KINDEX",
"RISE ",
"PLUS ",
"TIMEFOLIO",
"히어로",
"SMART",
"TREX",
"WON",
"KOACT",
)
def _pref_digit_suffixes() -> FrozenSet[str]:
"""우선주 숫자 끝자리 — NON_STOCK_PREF_CODE_SUFFIXES (기본 5,7,9)."""
raw = get_env_from_db("NON_STOCK_PREF_CODE_SUFFIXES", "5,7,9") or "5,7,9"
return frozenset(s.strip().upper() for s in str(raw).split(",") if s.strip())
def is_preferred_code(code: str) -> bool:
"""종목코드만으로 우선주·특수코드 여부 (name 없어도 판단).
KRX 단축코드:
- 끝자리 5/7/9 → 구형 우선주
- 끝자리 알파벳 → 신형 우선주
- 코드 중 알파벳 (예: 0039P0, 0030R0) → 신형 우선주·특수종목
(끝자리만 보면 보통주 0 으로 오인 → 캔들/시세 0 폭주)
"""
c = (code or "").strip()
if len(c) < 1:
return False
last = c[-1].upper()
if last in _pref_digit_suffixes():
return True
# K,L,M… 알파벳 끝 = 신형 우선주 코드
if not get_env_bool("NON_STOCK_EXCLUDE_ALPHA_SUFFIX", True):
return False
if last.isalpha():
return True
# 중간 알파벳 (0039P0 등) — 동일 토글. 끄려면 NON_STOCK_EXCLUDE_ALPHA_SUFFIX=false
if any(ch.isalpha() for ch in c):
return True
return False
def is_etn_code(code: str) -> bool:
c = (code or "").strip()
return len(c) == 6 and c[0] == "7"
def _name_keywords() -> tuple:
"""NON_STOCK_NAME_KEYWORDS — 콤마 구분. 비우면 기본 튜플."""
raw = (get_env_from_db("NON_STOCK_NAME_KEYWORDS", "") or "").strip()
if not raw:
return _DEFAULT_NON_STOCK_NAME_KEYWORDS
parts = tuple(s.strip() for s in raw.split(",") if s.strip())
return parts or _DEFAULT_NON_STOCK_NAME_KEYWORDS
def _name_hits_keyword(name: str) -> bool:
nm = name.upper()
for k in _name_keywords():
if not k:
continue
if k.isascii():
if k.upper() in nm:
return True
elif k in name:
return True
return False
def is_non_stock(name: str, code: str) -> bool:
"""ETN/ETF/스팩/우선주 등 매수 금지 종목이면 True.
name 이 비었거나 name==code 여도 코드 규칙으로 우선주·ETN 을 잡는다.
"""
code = (code or "").strip()
name = (name or "").strip()
if not code:
return True
if is_preferred_code(code):
return True
if is_etn_code(code):
return True
if name:
if _name_hits_keyword(name):
return True
nm = name.upper()
if "스팩" in name or "SPAC" in nm:
return True
# 리츠·부동산펀드 (시세 TR 키 이슈·봇 매매 비대상)
if "리츠" in name or "REIT" in nm:
return True
# 우선주 표기 (우 / 우B / 우C …)
if name.endswith("") or "우B" in name or "우C" in name or name.endswith("(전환)"):
return True
return False
def is_unmanageable_holding(name: str, code: str) -> bool:
"""보유 동기화에서 빼서 봇이 매도하지 않을 종목 (시세 불가 ETF/ETN·스팩).
우선주는 시세·매도 가능 → False. (실수로 산 우선주는 매도로 정리)
"""
code = (code or "").strip()
name = (name or "").strip()
if is_etn_code(code):
return True
if not name or name == code:
return False
nm = name.upper()
for k in ("ETN", "ETF", "레버리지", "인버스", "KODEX", "TIGER", "KBSTAR"):
if k.isascii():
if k in nm:
return True
elif k in name:
return True
if "스팩" in name or "SPAC" in nm:
return True
return False