#!/usr/bin/env python3 """KRX 애프터마켓(16:00~20:00, 2026-09-14 시행) 실측 probe — **조회 전용, 일회성**. 세션 모델을 고치기 전에 확인해야 할 4가지를 시각대별로 스냅샷으로 남긴다. 분석이 끝나면 이 스크립트와 plist(ai.openclaw.stock.aftermarket-probe)를 지운다. 확인 대상: 1. 애프터마켓 체결가가 KRX / NXT / SOR(AL) 중 어디로 오는가 (ka10095 3거래소 동시 조회) 2. 애프터마켓 가격제한폭이 정규장과 같은가 (`upl_pric`/`lst_pric` 시각대별 비교) 3. KRX 정규장 미체결이 15:35 증거금 해지 후 ka10075 에서 빠지는가 4. 애프터마켓에 호가가 뜨는가 (ka10004 bare=KRX / _AL) ⚠️ 주문 경로를 일절 건드리지 않는다 — orders 패키지를 import 하지 않고 kiwoom_client 만 쓴다. ⚠️ 응답을 **원시 그대로** 저장한다. 목적이 '새 필드가 생겼는지' 확인이라 정규화하면 안 된다. ⚠️ 한 항목이 실패해도 나머지는 계속 수집한다 (에러도 같이 기록). 결과: state/aftermarket_probe.jsonl — 1회차 = 1줄 사용: python3 aftermarket_probe.py [--tag <라벨>] """ from __future__ import annotations import json import sys import traceback from datetime import datetime, timedelta, timezone from pathlib import Path KST = timezone(timedelta(hours=9)) WORKSPACE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace') sys.path.insert(0, str(WORKSPACE / 'scripts')) import kiwoom_client as kc # noqa: E402 OUT = WORKSPACE / 'state' / 'aftermarket_probe.jsonl' # NXT 미상장 일반주 = 애프터마켓에서 새로 거래 가능해지는 쪽 (관심의 핵심) # NXT 상장 = 기존에도 15:30 이후 거래되던 쪽 (대조) # ETF = 애프터마켓 거래불가로 공지된 쪽 (대조) TARGETS = [ ('005935', '삼성전자우', 'nxt_미상장'), ('064400', 'LG씨엔에스', 'nxt_미상장'), ('381620', '제닉스로보틱스', 'nxt_미상장'), ('025980', '아난티', 'nxt_미상장'), ('005930', '삼성전자', 'nxt_상장'), ('000660', 'SK하이닉스', 'nxt_상장'), ('102110', 'TIGER 200', 'etf'), ] BOOK_CODES = ['005935', '005930', '102110'] # 호가는 유형별 대표 1종목씩만 def _safe(fn, *a, **kw): """수집 실패를 예외로 흘리지 않는다 — 한 항목 실패가 회차 전체를 날리면 안 된다.""" try: return {'ok': True, 'data': fn(*a, **kw)} except Exception as e: return {'ok': False, 'error': f'{e!r}', 'trace': traceback.format_exc(limit=3)} def _raw_ka10095(codes: list[str], suffix: str) -> dict: """ka10095 원시 응답. get_watchlist_quotes 는 필드를 추려버려서 직접 부른다.""" label = kc._default_account_label() param = '|'.join(f'{c}{suffix}' for c in codes) return kc._call(label, kc.TR_WATCHLIST_INFO, {'stk_cd': param}, endpoint=kc.ENDPOINT_STKINFO) def _raw_ka10004(code: str, suffix: str) -> dict: label = kc._default_account_label() url = kc.base_url() + kc.ENDPOINT_MRKCOND resp, _ = kc._http_post_full(url, {'stk_cd': f'{code}{suffix}'}, kc.auth_headers(label, tr_id='ka10004')) return resp def collect(tag: str) -> dict: now = datetime.now(KST) codes = [c for c, _, _ in TARGETS] rec: dict = { 'ts': now.isoformat(), 'tag': tag, 'targets': [{'code': c, 'name': n, 'kind': k} for c, n, k in TARGETS], 'quotes': {}, 'books': {}, 'open_orders': None, } # 1·2번 — 같은 종목을 세 거래소로 동시에 물어 값이 갈리는지 본다 for ex_name, suffix in (('KRX', ''), ('NXT', '_NX'), ('SOR', '_AL')): rec['quotes'][ex_name] = _safe(_raw_ka10095, codes, suffix) # 4번 — 애프터마켓에 호가가 실리는지 for code in BOOK_CODES: for ex_name, suffix in (('KRX', ''), ('SOR', '_AL')): rec['books'][f'{code}:{ex_name}'] = _safe(_raw_ka10004, code, suffix) # 3번 — KRX 미체결 생존 (증거금 해지 15:35 / NXT 20:05) rec['open_orders'] = _safe(kc.get_open_orders_all) return rec def main() -> int: tag = 'manual' if '--tag' in sys.argv: tag = sys.argv[sys.argv.index('--tag') + 1] rec = collect(tag) OUT.parent.mkdir(parents=True, exist_ok=True) with OUT.open('a', encoding='utf-8') as f: f.write(json.dumps(rec, ensure_ascii=False) + '\n') ok_q = sum(1 for v in rec['quotes'].values() if v['ok']) ok_b = sum(1 for v in rec['books'].values() if v['ok']) oo = rec['open_orders'] n_oo = len(oo['data']) if oo['ok'] else -1 print(f"[{rec['ts']}] tag={tag} 시세 {ok_q}/3 · 호가 {ok_b}/{len(rec['books'])} · 미체결 {n_oo}건 → {OUT}") return 0 if __name__ == '__main__': sys.exit(main())