408a3e0d4a
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
4.8 KiB
Python
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())
|