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,196 @@
#!/usr/bin/env python3
"""일봉 캐시 (state/daily_candles.sqlite).
자산웹 차트보기·분석 페이지에서 사용. 키움 ka10081 rate limit(분당 ~20콜)이 빡빡해
한 번 받은 일봉은 sqlite 에 저장하고 어제까지의 봉은 재호출하지 않는다.
스키마:
daily_candles(code, date, open, high, low, close, volume, value, turnover_rate)
PRIMARY KEY (code, date)
정책:
- 어제까지 봉만 저장. 오늘 봉은 장중 변동이라 캐시에 두지 않는다 (호출자가 ka10001 로 별도 결합).
- upd_stkpc_tp=1 수정주가 기준
- lazy fill 시 새로 받은 봉 중 캐시와 겹치는 날짜의 close 를 비교 → 다르면 권리락(분할/증자/감자) →
250 개 전체 재구축. 현금배당은 차트 영향 없음 (배당락일에 한 봉 갭으로 자연스레 표시).
CLI (디버깅용):
python3 daily_candles_cache.py <code> [count]
"""
from __future__ import annotations
import sqlite3
import sys
from datetime import datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo
KST = ZoneInfo('Asia/Seoul')
WORKSPACE = Path(__file__).resolve().parent.parent
DB_PATH = WORKSPACE / 'state' / 'daily_candles.sqlite'
CACHE_TARGET_COUNT = 250 # 첫 fill 시 받을 봉 수 (분석 페이지 1Y 와 동일)
VERIFY_WINDOW = 5 # 권리락 감지: 캐시·신규 겹치는 마지막 N일 close 비교
KIWOOM_FETCH_MAX = 600 # ka10081 한 콜 최대치 (안전 상한)
MISMATCH_THRESHOLD = 2 # 겹친 봉 중 close 가 다른 게 N 개 이상이면 권리락으로 판정
def _conn() -> sqlite3.Connection:
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
c = sqlite3.connect(DB_PATH, isolation_level=None)
c.execute("""
CREATE TABLE IF NOT EXISTS daily_candles (
code TEXT NOT NULL,
date TEXT NOT NULL,
open INTEGER NOT NULL,
high INTEGER NOT NULL,
low INTEGER NOT NULL,
close INTEGER NOT NULL,
volume INTEGER NOT NULL,
value INTEGER NOT NULL,
turnover_rate REAL,
PRIMARY KEY (code, date)
)
""")
c.execute("CREATE INDEX IF NOT EXISTS idx_code_date ON daily_candles(code, date DESC)")
return c
def _yesterday_kst() -> str:
return (datetime.now(KST) - timedelta(days=1)).strftime('%Y%m%d')
def _today_kst() -> str:
return datetime.now(KST).strftime('%Y%m%d')
def _row_to_dict(row: tuple) -> dict:
return {
'date': row[0],
'open': row[1],
'high': row[2],
'low': row[3],
'close': row[4],
'volume': row[5],
'value': row[6],
'turnover_rate': row[7],
}
def _select_latest(code: str, count: int) -> list[dict]:
"""캐시에서 최신 count 개 봉 (내림차순)."""
with _conn() as c:
rows = c.execute("""
SELECT date, open, high, low, close, volume, value, turnover_rate
FROM daily_candles WHERE code = ?
ORDER BY date DESC LIMIT ?
""", (code, count)).fetchall()
return [_row_to_dict(r) for r in rows]
def _replace_all(code: str, candles_desc: list[dict]) -> None:
"""code 의 캐시 모두 삭제 후 신규 봉으로 교체. 권리락 발생 시 사용."""
with _conn() as c:
c.execute("BEGIN")
c.execute("DELETE FROM daily_candles WHERE code = ?", (code,))
c.executemany("""
INSERT INTO daily_candles
(code, date, open, high, low, close, volume, value, turnover_rate)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", [
(code, x['date'], x['open'], x['high'], x['low'], x['close'],
x['volume'], x['value'], x.get('turnover_rate'))
for x in candles_desc
])
c.execute("COMMIT")
def _upsert_many(code: str, candles_desc: list[dict]) -> None:
"""INSERT OR REPLACE — 갭 보충용."""
with _conn() as c:
c.executemany("""
INSERT OR REPLACE INTO daily_candles
(code, date, open, high, low, close, volume, value, turnover_rate)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", [
(code, x['date'], x['open'], x['high'], x['low'], x['close'],
x['volume'], x['value'], x.get('turnover_rate'))
for x in candles_desc
])
def _fetch_from_kiwoom(code: str, count: int) -> list[dict]:
"""ka10081 호출 후 오늘 봉 제외한 어제까지 봉 반환 (최신순)."""
import kiwoom_client as kc
candles = kc.get_daily_candles(code, count=count)
today = _today_kst()
return [c for c in candles if c['date'] != today]
def _date_gap_days(yyyymmdd_a: str, yyyymmdd_b: str) -> int:
a = datetime.strptime(yyyymmdd_a, '%Y%m%d')
b = datetime.strptime(yyyymmdd_b, '%Y%m%d')
return (b - a).days
def get_candles(code: str, count: int = CACHE_TARGET_COUNT) -> list[dict]:
"""code 의 어제까지 일봉 최신 count 개를 시간 오름차순으로 반환.
동작:
1) 캐시 조회. 비었으면 ka10081 으로 250 개 fetch 후 저장.
2) 캐시의 최신 봉 date 가 어제 이전이면 갭 + 검증분만큼 받아옴.
겹치는 봉의 close 가 MISMATCH_THRESHOLD 이상 다르면 권리락 → 250 개 재구축.
그 외엔 새 봉만 upsert.
3) 최신 count 개 슬라이스 후 reverse (render_svg_chart 가 오름차순 기대).
"""
cached_desc = _select_latest(code, max(count, CACHE_TARGET_COUNT))
yesterday = _yesterday_kst()
if not cached_desc:
fresh_desc = _fetch_from_kiwoom(code, CACHE_TARGET_COUNT)
if fresh_desc:
_replace_all(code, fresh_desc)
cached_desc = fresh_desc
else:
latest_cached = cached_desc[0]['date']
if latest_cached < yesterday:
gap_estimate = _date_gap_days(latest_cached, yesterday) + VERIFY_WINDOW
fetch_count = min(KIWOOM_FETCH_MAX, max(VERIFY_WINDOW + 2, gap_estimate * 2))
fresh_desc = _fetch_from_kiwoom(code, fetch_count)
cached_by_date = {c['date']: c for c in cached_desc}
overlap_mismatches = 0
overlap_total = 0
for fc in fresh_desc:
if fc['date'] in cached_by_date:
overlap_total += 1
if fc['close'] != cached_by_date[fc['date']]['close']:
overlap_mismatches += 1
if overlap_total >= 3 and overlap_mismatches >= MISMATCH_THRESHOLD:
sys.stderr.write(
f'[daily_candles_cache] {code}: 권리락 감지 '
f'({overlap_mismatches}/{overlap_total} close 불일치) → 250개 재구축\n'
)
full_desc = _fetch_from_kiwoom(code, CACHE_TARGET_COUNT)
if full_desc:
_replace_all(code, full_desc)
cached_desc = full_desc
else:
_upsert_many(code, fresh_desc)
cached_desc = _select_latest(code, max(count, CACHE_TARGET_COUNT))
desc_slice = cached_desc[:count]
return list(reversed(desc_slice))
if __name__ == '__main__':
if len(sys.argv) < 2:
print('usage: python3 daily_candles_cache.py <code> [count]')
sys.exit(1)
code = sys.argv[1]
cnt = int(sys.argv[2]) if len(sys.argv) > 2 else 10
sys.path.insert(0, str(Path(__file__).resolve().parent))
out = get_candles(code, count=cnt)
for c in out:
print(c)