fix: VI 1h 매핑 실증 반영 — active=해제시각(1224) 유무로 판정

2026-06-12 사이드카 날 VI 폭주(~1,000건) 실증 결과:
- 9068은 발동/해제가 아니라 VI 종류(1=정적, 2=동적, 3=동적+정적)
  → 구 매핑은 동적 VI를 통째로 무시하고, 해제 이벤트도 active로 덮어썼음
- 발동 이벤트는 1224(해제시각)='000000', 해제 이벤트는 시각이 참
  → active = release_time in ('','000000') 으로 교체, 1223 발동시각 추가 저장
- 1h는 구독 무관 시장 전역 broadcast임을 확인 (주석 기록)
- 실증용 raw 로그 제거 (폭주 날 ~2,000줄 — 역할 완료)
- 토스트: 발동시각 표시 추가, '000000'은 해제 표시 생략
- TTL 백스톱(_vi_expired 180s)은 해제 메시지 유실 대비로 유지

라이브 검증: 002780 동적 VI 발동(09:09:08) → active=True 정상 포착

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-06-12 09:09:42 +09:00
parent defd5c0bf6
commit b5e3fc0f4e
2 changed files with 12 additions and 8 deletions
+2 -1
View File
@@ -9464,7 +9464,8 @@ window.openPinModal = openPinModal;
if(v.trigger_price) L.push('발동가: '+Number(v.trigger_price).toLocaleString()+''); if(v.trigger_price) L.push('발동가: '+Number(v.trigger_price).toLocaleString()+'');
if(v.trigger_pct!=null&&v.trigger_pct!==0) L.push('발동 등락률: '+(v.trigger_pct>=0?'+':'')+v.trigger_pct+'%'); if(v.trigger_pct!=null&&v.trigger_pct!==0) L.push('발동 등락률: '+(v.trigger_pct>=0?'+':'')+v.trigger_pct+'%');
if(v.count) L.push('당일 발동: '+v.count+''); if(v.count) L.push('당일 발동: '+v.count+'');
if(v.release_time) L.push('해제 예정: '+_fmtViTime(v.release_time)); if(v.trigger_time&&v.trigger_time!=='000000') L.push('발동시각: '+_fmtViTime(v.trigger_time));
if(v.release_time&&v.release_time!=='000000') L.push('해제: '+_fmtViTime(v.release_time));
head=L.join('\n')+'\n\n─────\n'; head=L.join('\n')+'\n\n─────\n';
} }
var desc='⚡VI (변동성완화장치)\n\n주가가 짧은 시간에 급변하면 약 2분간\n단일가매매로 전환해 과열을 식히는 장치예요.\n\n• 동적VI: 직전 체결가 대비\n• 정적VI: 당일 기준가 대비\n(코스피200 3%·그 외 6% 등에서 발동)'; var desc='⚡VI (변동성완화장치)\n\n주가가 짧은 시간에 급변하면 약 2분간\n단일가매매로 전환해 과열을 식히는 장치예요.\n\n• 동적VI: 직전 체결가 대비\n• 정적VI: 당일 기준가 대비\n(코스피200 3%·그 외 6% 등에서 발동)';
+10 -7
View File
@@ -349,19 +349,22 @@ class RealtimeHub:
'ts': datetime.now(KST).isoformat(), 'ts': datetime.now(KST).isoformat(),
} }
elif typ == '1h': # VI 발동/해제 elif typ == '1h': # VI 발동/해제
# active 판정: 9068 VI발동구분. 해제시각(1224) 유무와 함께 실증 확인 예정(A단계). # 매핑 실증 완료 (2026-06-12 사이드카 날 VI 폭주 ~1,000건 관찰):
# 실증 로그 — VI는 드물어 부담 없음. 2026-06-11 관찰: 발동 후 해제 1h가 안 오고 # - 9068 = VI 종류 (1=정적, 2=동적, 3=동적+정적) — 발동/해제 구분 아님
# 1224(해제시각)가 발동시각과 동일 → 읽기 시점 TTL 만료(_vi_expired)로 방어 중. # - 1223 = 발동시각, 1224 = 해제시각. 발동 이벤트는 1224='000000', 해제 이벤트는 시각이 참
print(f'[1h raw] {code} {vals}', file=sys.stderr, flush=True) # - 1h는 구독 무관 시장 전역 broadcast (모든 종목 VI가 들어옴)
# 해제 메시지 유실 대비 백스톱으로 읽기 시점 TTL(_vi_expired)은 유지.
kind_raw = (vals.get('9068') or '').strip() kind_raw = (vals.get('9068') or '').strip()
release_t = (vals.get('1224') or '').strip()
with self._lock: with self._lock:
self._vi[code] = { self._vi[code] = {
'active': kind_raw not in ('', '0', '2'), # ⚠️ 매핑 실증 확인 필요 'active': release_t in ('', '000000'), # 해제시각 없음 = 발동 중
'vi_gubun': kind_raw, 'vi_gubun': kind_raw, # VI 종류 (1정적/2동적/3동적+정적)
'apply_kind': (vals.get('1225') or '').strip(), # 정적/동적/동적+정적 'apply_kind': (vals.get('1225') or '').strip(), # 정적/동적/동적+정적
'trigger_price': _abs_int(vals.get('1221')), 'trigger_price': _abs_int(vals.get('1221')),
'trigger_pct': kc._to_float(vals.get('1489')), 'trigger_pct': kc._to_float(vals.get('1489')),
'release_time': (vals.get('1224') or '').strip(), 'trigger_time': (vals.get('1223') or '').strip(),
'release_time': release_t,
'direction': (vals.get('9069') or '').strip(), 'direction': (vals.get('9069') or '').strip(),
'count': kc._to_int(vals.get('1490')), # 당일 VI 발동 횟수 'count': kc._to_int(vals.get('1490')), # 당일 VI 발동 횟수
'ts': datetime.now(KST).isoformat(), 'ts': datetime.now(KST).isoformat(),