Initial commit: OpenClaw 워크스페이스 버전관리 시작

설정·스크립트·스킬·문서·큐레이션 메모리 추적.
시크릿(credentials/identity)·런타임 상태(state/logs/sessions/sqlite)·
백업(clobbered/bak)·dream 캐시는 .gitignore로 제외.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-06-04 15:39:41 +09:00
commit fed3526b20
199 changed files with 49671 additions and 0 deletions
@@ -0,0 +1,177 @@
"""
키움 REST API → market_data dict.
guards.validate_request 가 요구하는 모든 필드를 채워 반환한다.
기존 kiwoom_client 의 조회 함수를 재사용.
호가창(ka10004)·NXT 가능여부 등 일부 필드는 키움 응답 필드명이 환경마다 다를 수 있어
best-effort 파싱이며, 실패 시 None/0 으로 떨어진다. 첫 실거래 검증 시 실데이터로 보강 필요.
"""
from __future__ import annotations
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Optional
_PARENT = Path(__file__).resolve().parent.parent
if str(_PARENT) not in sys.path:
sys.path.insert(0, str(_PARENT))
import kiwoom_client as kc
from . import guards
KST = timezone(timedelta(hours=9))
def _to_int(s) -> int:
if s is None or s == '':
return 0
try:
return int(str(s).replace(',', '').replace('+', '').strip() or 0)
except (ValueError, AttributeError):
return 0
def _safe_quote(symbol: str, account_label: str) -> dict:
"""ka10001 (주식기본정보) 응답에서 가드용 필드 추출.
공식 명세 필드명:
- cur_prc: 현재가, base_pric: 기준가(전일종가)
- upl_pric: 상한가, lst_pric: 하한가
- 거래정지(halt) 필드는 ka10001 명세에 없음 — 별도 TR 보강 필요. 보수적으로 False.
"""
try:
q_wrap = kc.get_stock_quote(symbol, account_label=account_label, exchange='AL')
except Exception:
return {}
if not isinstance(q_wrap, dict):
return {}
raw = q_wrap.get('raw') if isinstance(q_wrap.get('raw'), dict) else q_wrap
out = {
'cur_price': abs(_to_int(raw.get('cur_prc'))),
'prev_close': abs(_to_int(raw.get('base_pric'))),
'upper_limit': abs(_to_int(raw.get('upl_pric'))),
'lower_limit': abs(_to_int(raw.get('lst_pric'))),
'halt': False, # ka10001 명세에 거래정지 플래그 없음 — 별도 TR 보강 예정
}
out['_raw'] = raw
return out
def _safe_orderbook(symbol: str, account_label: str) -> Optional[dict]:
"""ka10004 (주식호가요청) — /api/dostk/mrkcond.
필드 매핑 (키움 공식 명세):
- 1호가: sel_fpr_bid / sel_fpr_req (매도), buy_fpr_bid / buy_fpr_req (매수)
- 2~10호가: sel_{N}th_pre_bid / sel_{N}th_pre_req (매도), buy_{N}th_pre_bid / buy_{N}th_pre_req (매수)
가격에 부호(+/-) 붙어올 수 있어 abs 처리.
"""
try:
resp = kc._call(account_label, 'ka10004', {'stk_cd': symbol},
endpoint=kc.ENDPOINT_MRKCOND)
except Exception:
return None
if not isinstance(resp, dict):
return None
asks, bids = [], []
# 1호가
ap1 = abs(_to_int(resp.get('sel_fpr_bid')))
aq1 = abs(_to_int(resp.get('sel_fpr_req')))
if ap1 and aq1:
asks.append({'price': ap1, 'qty': aq1})
bp1 = abs(_to_int(resp.get('buy_fpr_bid')))
bq1 = abs(_to_int(resp.get('buy_fpr_req')))
if bp1 and bq1:
bids.append({'price': bp1, 'qty': bq1})
# 2~10호가
for i in range(2, 11):
ap = abs(_to_int(resp.get(f'sel_{i}th_pre_bid')))
aq = abs(_to_int(resp.get(f'sel_{i}th_pre_req')))
if ap and aq:
asks.append({'price': ap, 'qty': aq})
bp = abs(_to_int(resp.get(f'buy_{i}th_pre_bid')))
bq = abs(_to_int(resp.get(f'buy_{i}th_pre_req')))
if bp and bq:
bids.append({'price': bp, 'qty': bq})
if not asks and not bids:
return None
return {'asks': asks, 'bids': bids}
def _nxt_eligible(symbol: str, quote: dict) -> bool:
"""NXT 거래가능 여부.
1차 소스: ka10099 종목정보 캐시 (`state/stock_codes.json`)의 `nxt_enable` 플래그.
캐시 미스 또는 구 스키마(필드 없음) → 보수적 True (= 가드 통과 → 키움이 사후 거부).
캐시 갱신 명령: `python3 kiwoom_client.py refresh-codes`.
"""
try:
meta = kc.lookup_stock_meta(symbol)
except Exception:
return True
if not meta:
return True
if 'nxt_enable' not in meta: # 구 스키마 캐시 — 갱신 전엔 보수적 True
return True
return bool(meta['nxt_enable'])
def _safe_broker_executions(account_label: str, now: datetime) -> tuple[Optional[list], Optional[str]]:
"""kt00007 기준 오늘자 매매 행 조회. 실패 시 (None, error_repr).
가드(validate_delay_same_symbol_via_broker)에서 사용. 정확도 우선이라 실패 시 보수적 차단.
"""
try:
executions = kc.get_order_executions(account_label, base_dt=now.strftime('%Y%m%d'))
except Exception as e:
return None, repr(e)
return executions, None
def collect_market_data(account_label: str, symbol: str, side: str, qty: int) -> dict:
now = datetime.now(KST)
quote = _safe_quote(symbol, account_label)
orderbook = _safe_orderbook(symbol, account_label)
broker_executions, broker_query_error = _safe_broker_executions(account_label, now)
try:
stock_meta = kc.lookup_stock_meta(symbol)
except Exception:
stock_meta = None
md = {
'now': now,
'is_holiday': guards.is_today_holiday(now),
'nxt_eligible': _nxt_eligible(symbol, quote),
'current_price': quote.get('cur_price', 0),
'prev_close': quote.get('prev_close', 0),
'upper_limit': quote.get('upper_limit', 0),
'lower_limit': quote.get('lower_limit', 0),
'halt': quote.get('halt', False),
'vi': False, # VI 실시간 감지는 별도 채널 필요. 보강 예정.
'orderbook': orderbook,
'broker_executions': broker_executions,
'broker_query_error': broker_query_error,
'stock_meta': stock_meta,
}
if side == 'BUY':
try:
bal = kc.get_balance(account_label)
md['balance_d2'] = _to_int(bal.get('d2_entra'))
except Exception:
md['balance_d2'] = 0
else:
try:
positions = kc.get_positions(account_label)
md['position_qty'] = next(
(_to_int(p.get('trde_able_qty') or p.get('qty') or p.get('hold_qty') or p.get('rmnd_qty'))
for p in positions
if (p.get('code') == symbol) or (p.get('symbol') == symbol) or (p.get('stk_cd') == symbol)),
0,
)
except Exception:
md['position_qty'] = 0
return md