Files
openclaw/agents/stock/workspace/scripts/aftermarket_probe.py
T
hyowons 408a3e0d4a auto: 일일 백업 2026-09-22 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-22 02:00:05 +09:00

121 lines
4.8 KiB
Python

#!/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())