변경 사항 ---- - _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>
188 lines
6.5 KiB
Python
188 lines
6.5 KiB
Python
"""
|
|
permanent_subs.py
|
|
=================
|
|
영구구독(Permanent WS Subscriptions) 통합 테이블 — **국내(KR)·해외(US) 단일 소스**.
|
|
|
|
배경:
|
|
- 기존 ``env_config.PERMANENT_WS_CODES`` 는 콤마 문자열이라 **시장/거래소/심볼 메타를
|
|
담지 못함.** 해외(QQQM 등)는 국내 WS(H0STCNT0)로는 못 받고, 거래소 코드가 필요하다.
|
|
- 이 테이블은 코드별로 ``market_type/exchange/symbol/tf_min`` 을 보관해
|
|
- KR → 국내 실시간 WS(H0STCNT0)
|
|
- US → 해외 실시간 WS(HDFSCNT0, tr_key=D{EXCD}{SYMBOL})
|
|
로 각각 라우팅할 수 있게 한다.
|
|
|
|
호환:
|
|
- ``env_config.PERMANENT_WS_CODES`` 는 폴백으로 유지(테이블이 비어 있을 때만).
|
|
- ``migrate_env_codes()`` 로 기존 콤마 코드를 1회 이관(중복은 건너뜀).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
logger = logging.getLogger("permanent_subs")
|
|
|
|
# 해외(US) 실시간 WS tr_key 거래소 코드 — 주문용(NASD/NYSE/AMEX) → 시세용(NAS/NYS/AMS)
|
|
_US_EXCD_MAP = {
|
|
"NASD": "NAS", "NAS": "NAS",
|
|
"NYSE": "NYS", "NYS": "NYS",
|
|
"AMEX": "AMS", "AMS": "AMS",
|
|
}
|
|
|
|
_PERM_DDL = """
|
|
CREATE TABLE IF NOT EXISTS permanent_subscriptions (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
code VARCHAR(16) NOT NULL,
|
|
market_type VARCHAR(8) NOT NULL DEFAULT 'KR',
|
|
exchange VARCHAR(16) NOT NULL DEFAULT 'KRX',
|
|
symbol VARCHAR(32) NOT NULL DEFAULT '',
|
|
tf_min INT NOT NULL DEFAULT 60,
|
|
enabled TINYINT NOT NULL DEFAULT 1,
|
|
note VARCHAR(100) NOT NULL DEFAULT '',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE KEY uq_perm_code (code)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='영구구독(국내 WS + 해외 WS) 종목'
|
|
"""
|
|
|
|
|
|
def _core(db: Any):
|
|
"""TradeDBExt 이면 ``.raw``, 아니면 그대로."""
|
|
return getattr(db, "raw", db)
|
|
|
|
|
|
def ensure_permanent_subs_table(db: Any) -> None:
|
|
raw = _core(db)
|
|
raw.conn.execute(_PERM_DDL.strip())
|
|
raw.conn.commit()
|
|
|
|
|
|
def classify_market(code: str) -> tuple[str, str, str]:
|
|
"""
|
|
코드 형태로 (market_type, exchange, symbol) 추정.
|
|
- 6자리 숫자 → KR/KRX
|
|
- 영문 1~8자 → US/NASD (거래소는 이후 UI/DB에서 보정 가능)
|
|
"""
|
|
c = str(code or "").strip().upper()
|
|
if c.isdigit() and len(c) == 6:
|
|
return "KR", "KRX", c
|
|
if c.isalpha() and 1 <= len(c) <= 8:
|
|
return "US", "NASD", c
|
|
# 알 수 없으면 KR 취급(보수적)
|
|
return "KR", "KRX", c
|
|
|
|
|
|
def us_ws_tr_key(exchange: str, symbol: str) -> str:
|
|
"""해외 실시간 WS tr_key — D + 시세거래소(NAS/NYS/AMS) + 심볼 (예: DNASQQQM)."""
|
|
ex = str(exchange or "NASD").strip().upper()
|
|
excd = _US_EXCD_MAP.get(ex, "NAS")
|
|
sym = str(symbol or "").strip().upper()
|
|
return f"D{excd}{sym}"
|
|
|
|
|
|
def upsert_permanent_sub(
|
|
db: Any,
|
|
code: str,
|
|
market_type: Optional[str] = None,
|
|
exchange: Optional[str] = None,
|
|
symbol: Optional[str] = None,
|
|
tf_min: int = 60,
|
|
enabled: bool = True,
|
|
note: str = "",
|
|
) -> None:
|
|
"""코드 1건 등록/갱신 (code UNIQUE)."""
|
|
ensure_permanent_subs_table(db)
|
|
code = str(code or "").strip().upper()
|
|
if not code:
|
|
raise ValueError("code 필수")
|
|
g_mt, g_ex, g_sym = classify_market(code)
|
|
mt = (str(market_type).strip().upper() if market_type else "") or g_mt
|
|
ex = (str(exchange).strip().upper() if exchange else "") or (g_ex if mt == g_mt else ("KRX" if mt == "KR" else "NASD"))
|
|
sym = (str(symbol).strip().upper() if symbol else "") or g_sym or code
|
|
try:
|
|
tfv = int(tf_min)
|
|
except (TypeError, ValueError):
|
|
tfv = 60
|
|
if tfv < 1:
|
|
tfv = 60
|
|
raw = _core(db)
|
|
raw.conn.execute(
|
|
"""
|
|
INSERT INTO permanent_subscriptions
|
|
(code, market_type, exchange, symbol, tf_min, enabled, note)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
|
ON DUPLICATE KEY UPDATE
|
|
market_type=VALUES(market_type), exchange=VALUES(exchange),
|
|
symbol=VALUES(symbol), tf_min=VALUES(tf_min),
|
|
enabled=VALUES(enabled), note=VALUES(note)
|
|
""",
|
|
[code, mt, ex, sym, tfv, 1 if enabled else 0, str(note or "")[:100]],
|
|
)
|
|
raw.conn.commit()
|
|
|
|
|
|
def remove_permanent_sub(db: Any, code: str) -> bool:
|
|
ensure_permanent_subs_table(db)
|
|
code = str(code or "").strip().upper()
|
|
if not code:
|
|
return False
|
|
raw = _core(db)
|
|
cur = raw.conn.execute("DELETE FROM permanent_subscriptions WHERE code=%s", [code])
|
|
raw.conn.commit()
|
|
try:
|
|
return bool(cur.rowcount)
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def set_enabled(db: Any, code: str, enabled: bool) -> None:
|
|
ensure_permanent_subs_table(db)
|
|
raw = _core(db)
|
|
raw.conn.execute(
|
|
"UPDATE permanent_subscriptions SET enabled=%s WHERE code=%s",
|
|
[1 if enabled else 0, str(code or "").strip().upper()],
|
|
)
|
|
raw.conn.commit()
|
|
|
|
|
|
def list_permanent_subs(db: Any, enabled_only: bool = False) -> List[Dict[str, Any]]:
|
|
ensure_permanent_subs_table(db)
|
|
raw = _core(db)
|
|
sql = "SELECT code, market_type, exchange, symbol, tf_min, enabled, note FROM permanent_subscriptions"
|
|
if enabled_only:
|
|
sql += " WHERE enabled=1"
|
|
sql += " ORDER BY market_type, code"
|
|
cur = raw.conn.execute(sql)
|
|
rows = cur.fetchall() if cur else []
|
|
out: List[Dict[str, Any]] = []
|
|
for r in rows:
|
|
d = dict(r) if not isinstance(r, dict) else dict(r)
|
|
d["enabled"] = int(d.get("enabled", 1))
|
|
out.append(d)
|
|
return out
|
|
|
|
|
|
def codes_by_market(db: Any, market_type: str, enabled_only: bool = True) -> List[Dict[str, Any]]:
|
|
"""특정 시장(KR/US) 영구구독 목록 (WS 배선용)."""
|
|
mt = str(market_type or "").strip().upper()
|
|
return [r for r in list_permanent_subs(db, enabled_only=enabled_only)
|
|
if str(r.get("market_type", "KR")).strip().upper() == mt]
|
|
|
|
|
|
def migrate_env_codes(db: Any, env_csv: str) -> int:
|
|
"""
|
|
기존 ``PERMANENT_WS_CODES`` 콤마 문자열 → 테이블 1회 이관.
|
|
이미 있는 code 는 건드리지 않음(덮어쓰기 방지). 신규 삽입 건수 반환.
|
|
"""
|
|
ensure_permanent_subs_table(db)
|
|
existing = {r["code"] for r in list_permanent_subs(db)}
|
|
n = 0
|
|
for raw_code in str(env_csv or "").split(","):
|
|
c = raw_code.strip().upper()
|
|
if not c or c in existing:
|
|
continue
|
|
mt, ex, sym = classify_market(c)
|
|
upsert_permanent_sub(db, c, mt, ex, sym, note="env 이관")
|
|
existing.add(c)
|
|
n += 1
|
|
return n
|