158 lines
4.6 KiB
Python
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
|