feat: 자산웹 실시간(WS) NXT 통합·시세 중복 제거·연결상태 점

- realtime_hub: 0B/1h 구독을 _AL(통합/SOR)로 전환 → 정규장 KRX + NXT 시간대
  NXT 체결을 한 구독으로 수신(수신 시 거래소 접미사 제거 후 bare 저장).
  NXT 당일평가손익 실시간 갱신 미동작 근본 수정. 0B 시/고/저(16/17/18) 캡처
- behive_web: WS 연결 시 보유 가격보정 ka10095 2콜 skip(완전 중복),
  관심·감시 시세도 허브 메모리 우선 충당(OHLC·마크는 서버 렌더 유지, mark는
  exchange 필드로 판정) → 시세성 키움 호출 중복 제거
- VI 배지: 자동갱신 swap 직후 마지막 WS 상태 즉시 재적용(깜빡임 제거),
  codesOnPage가 보유행(:held)·영문 6자리 코드도 수집 → 보유종목까지 VI 표시
- 상단바 WS 연결 상태 점(초록/빨강/회색) 추가 + 탭 시 상태 토스트

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-06-09 12:26:45 +09:00
parent 69ef9c09e8
commit 7f1c52a580
2 changed files with 92 additions and 14 deletions
+15 -4
View File
@@ -68,7 +68,7 @@ class RealtimeHub:
def __init__(self, account_label: str | None = None):
self._label = account_label or kc._default_account_label()
self._lock = threading.Lock()
self._quotes: dict[str, dict] = {} # code -> {price, change, pct, ts, exchange}
self._quotes: dict[str, dict] = {} # code -> {price, change, pct, open, high, low, ts, exchange}
self._vi: dict[str, dict] = {} # code -> {active, kind, trigger_price, ...}
self._subs: set[str] = set() # 현재 구독 종목코드(정규화)
self._ws: websocket.WebSocket | None = None
@@ -261,17 +261,23 @@ class RealtimeHub:
return
self._ws.send(json.dumps(payload))
@staticmethod
def _al(codes: list[str]) -> list[str]:
# 통합(SOR) 거래소코드 `_AL` 접미사 — 정규장 KRX 체결 + NXT 시간대 NXT 체결을
# 한 구독으로 수신. bare 코드는 KRX 단독이라 NXT(15:30~20:00)에 시세가 끊긴다. (PDF 0B/1h 스펙)
return [f'{c}_AL' for c in codes]
def _send_reg(self, codes: list[str]) -> None:
# 0B(체결) + 1h(VI) 동시 등록. refresh=1 → 기존 구독 유지하며 추가.
self._send({
'trnm': 'REG', 'grp_no': '1', 'refresh': '1',
'data': [{'item': codes, 'type': ['0B', '1h']}],
'data': [{'item': self._al(codes), 'type': ['0B', '1h']}],
})
def _send_remove(self, codes: list[str]) -> None:
self._send({
'trnm': 'REMOVE', 'grp_no': '1',
'data': [{'item': codes, 'type': ['0B', '1h']}],
'data': [{'item': self._al(codes), 'type': ['0B', '1h']}],
})
# ---- 수신 처리 ----
@@ -302,7 +308,9 @@ class RealtimeHub:
def _handle_real(self, blk: dict) -> None:
typ = blk.get('type')
code = kc._clean_code(blk.get('item') or '')
# 통합(_AL)·NXT(_NX) 구독은 item이 `039490_AL` 형태로 echo됨 → 거래소 접미사를 떼고
# bare 코드로 저장해야 get_quotes(bare) 매칭이 유지된다. 키움 종목코드엔 '_'가 없다.
code = kc._clean_code((blk.get('item') or '').split('_')[0])
vals = blk.get('values') or {}
if not code:
return
@@ -312,6 +320,9 @@ class RealtimeHub:
'price': _abs_int(vals.get('10')),
'change': kc._to_int(vals.get('11')),
'pct': kc._to_float(vals.get('12')),
'open': _abs_int(vals.get('16')), # 시가
'high': _abs_int(vals.get('17')), # 고가
'low': _abs_int(vals.get('18')), # 저가
'exchange': (vals.get('9081') or '').strip(),
'ts': datetime.now(KST).isoformat(),
}