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:
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""매월 1일 04:30 — 본인 계좌 잔액을 골디 inbox에 envelope로 떨어뜨린다.
|
||||
|
||||
가희 계좌는 제외 (label prefix '가희_'). 본인 계좌별 평가액(kt00018)·예수금(kt00001)·총자산을 집계.
|
||||
LLM 불필요 — launchd로 실행. 실패 시 레이 텔레그램으로 자가 알림.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
KST = ZoneInfo('Asia/Seoul')
|
||||
WORKSPACE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace')
|
||||
sys.path.insert(0, str(WORKSPACE / 'scripts'))
|
||||
|
||||
import kiwoom_client as kw # noqa: E402
|
||||
|
||||
INBOX_DIR = Path('/Users/snowoyh/.openclaw/agents/budget/inbox/incoming')
|
||||
CONFIG_PATH = Path('/Users/snowoyh/.openclaw/openclaw.json')
|
||||
TELEGRAM_ACCOUNT = 'stock'
|
||||
TOPIC = 'securities_balance'
|
||||
SCHEMA_VERSION = 1
|
||||
GAHEE_PREFIX = '가희_'
|
||||
|
||||
|
||||
def send_telegram(text: str) -> bool:
|
||||
try:
|
||||
cfg = json.loads(CONFIG_PATH.read_text())
|
||||
acct = cfg['channels']['telegram']['accounts'][TELEGRAM_ACCOUNT]
|
||||
token = acct['botToken']
|
||||
chat_ids = acct.get('allowFrom') or []
|
||||
except Exception as e:
|
||||
print(f'telegram cfg load failed: {e}', file=sys.stderr)
|
||||
return False
|
||||
if not chat_ids:
|
||||
return False
|
||||
url = f'https://api.telegram.org/bot{token}/sendMessage'
|
||||
ok = True
|
||||
for chat_id in chat_ids:
|
||||
data = urllib.parse.urlencode({
|
||||
'chat_id': chat_id,
|
||||
'text': text[:4000],
|
||||
'disable_web_page_preview': 'true',
|
||||
}).encode()
|
||||
try:
|
||||
req = urllib.request.Request(url, data=data, method='POST')
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
if r.status != 200:
|
||||
ok = False
|
||||
except Exception as e:
|
||||
print(f'telegram send failed: {e}', file=sys.stderr)
|
||||
ok = False
|
||||
return ok
|
||||
|
||||
|
||||
def collect_owner_accounts() -> list[dict]:
|
||||
accounts = kw.list_accounts()
|
||||
out = []
|
||||
for a in accounts:
|
||||
label = a['label']
|
||||
if label.startswith(GAHEE_PREFIX):
|
||||
continue
|
||||
balance = kw.get_balance(label)
|
||||
positions = kw.get_positions(label)
|
||||
eval_amount = sum(p.get('evlt_amt', 0) for p in positions)
|
||||
# d2_entra(D+2 정산예수금) — 미결제 매수/매도까지 반영한 실제 가용 예수금.
|
||||
# entr은 D+2 결제 전까지 변하지 않아 미결제 매도대금 누락 가능.
|
||||
deposit = balance.get('d2_entra', 0)
|
||||
out.append({
|
||||
'label': label,
|
||||
'account_no': a.get('account_no', ''),
|
||||
'deposit': deposit,
|
||||
'eval_amount': eval_amount,
|
||||
'total': deposit + eval_amount,
|
||||
'position_count': len(positions),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def build_message(accounts: list[dict]) -> dict:
|
||||
now = datetime.now(KST)
|
||||
return {
|
||||
'message_id': str(uuid.uuid4()),
|
||||
'from': 'stock',
|
||||
'to': 'budget',
|
||||
'topic': TOPIC,
|
||||
'created_at': now.isoformat(),
|
||||
'schema_version': SCHEMA_VERSION,
|
||||
'payload': {
|
||||
'as_of': now.strftime('%Y-%m-%d'),
|
||||
'owner_scope': 'self_only',
|
||||
'accounts': accounts,
|
||||
'totals': {
|
||||
'deposit': sum(a['deposit'] for a in accounts),
|
||||
'eval_amount': sum(a['eval_amount'] for a in accounts),
|
||||
'total': sum(a['total'] for a in accounts),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def write_message(msg: dict) -> Path:
|
||||
INBOX_DIR.mkdir(parents=True, exist_ok=True)
|
||||
iso_compact = msg['created_at'].replace(':', '').replace('-', '').split('+')[0]
|
||||
filename = f"stock__{TOPIC}__{iso_compact}.json"
|
||||
path = INBOX_DIR / filename
|
||||
path.write_text(json.dumps(msg, ensure_ascii=False, indent=2))
|
||||
return path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
accounts = collect_owner_accounts()
|
||||
if not accounts:
|
||||
raise RuntimeError('본인 계좌 0개 — 자격증명 또는 prefix 필터 점검 필요')
|
||||
msg = build_message(accounts)
|
||||
path = write_message(msg)
|
||||
total = msg['payload']['totals']['total']
|
||||
print(f'sent: {path.name} | accounts={len(accounts)} | total={total:,}원')
|
||||
return 0
|
||||
except Exception as e:
|
||||
err = f'⚠️ [send_balance_to_budget] 실패\n{type(e).__name__}: {e}\n\n{traceback.format_exc()[-500:]}'
|
||||
print(err, file=sys.stderr)
|
||||
send_telegram(err)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user