From 2cf5b38da1e9a04efd5fed3ee56e7a6b2dd31ba4 Mon Sep 17 00:00:00 2001 From: hyowons Date: Wed, 22 Jul 2026 02:00:04 +0900 Subject: [PATCH] =?UTF-8?q?auto:=20=EC=9D=BC=EC=9D=BC=20=EB=B0=B1=EC=97=85?= =?UTF-8?q?=202026-07-22=2002:00?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 3 +- .../stock/workspace/memory/2026-07-20-0912.md | 66 + .../workspace/openclaw-workspace-state.json | 5 + agents/stock/workspace/scripts/behive_web.py | 606 ++- agents/stock/workspace/scripts/orders/card.py | 8 + .../workspace/scripts/orders/datasource.py | 30 +- .../stock/workspace/scripts/orders/guards.py | 23 +- .../stock/workspace/scripts/orders/handler.py | 17 +- .../workspace/scripts/orders/kiwoom_order.py | 21 +- .../stock/workspace/scripts/stock_analysis.py | 2 +- .../state/market_indicators_history.jsonl | 4 + .../workspace/state/sim/equity_history.json | 2 +- .../stock/workspace/state/sim/favorites.json | 2 +- .../workspace/state/sim/index_history.json | 2 +- .../state/sim/variants/v1/portfolio.json | 69 +- .../state/sim/variants/v1/trades.jsonl | 2 + .../state/sim/variants/v101/portfolio.json | 65 +- .../state/sim/variants/v101/trades.jsonl | 2 + .../state/sim/variants/v103/portfolio.json | 65 +- .../state/sim/variants/v103/trades.jsonl | 2 + .../state/sim/variants/v105/portfolio.json | 109 +- .../state/sim/variants/v105/trades.jsonl | 5 + .../state/sim/variants/v107/portfolio.json | 66 +- .../state/sim/variants/v107/trades.jsonl | 3 + .../state/sim/variants/v147/portfolio.json | 131 +- .../state/sim/variants/v147/trades.jsonl | 5 + .../state/sim/variants/v149/portfolio.json | 173 +- .../state/sim/variants/v149/trades.jsonl | 7 + .../state/sim/variants/v151/portfolio.json | 175 +- .../state/sim/variants/v151/trades.jsonl | 7 + .../state/sim/variants/v155/portfolio.json | 172 +- .../state/sim/variants/v155/trades.jsonl | 7 + .../state/sim/variants/v157/portfolio.json | 151 +- .../state/sim/variants/v157/trades.jsonl | 6 + .../state/sim/variants/v159/portfolio.json | 111 +- .../state/sim/variants/v159/trades.jsonl | 4 + .../state/sim/variants/v161/portfolio.json | 89 +- .../state/sim/variants/v161/trades.jsonl | 3 + .../state/sim/variants/v165/portfolio.json | 89 +- .../state/sim/variants/v165/trades.jsonl | 3 + .../state/sim/variants/v167/portfolio.json | 89 +- .../state/sim/variants/v167/trades.jsonl | 3 + .../state/sim/variants/v189/portfolio.json | 86 +- .../state/sim/variants/v189/trades.jsonl | 3 + .../state/sim/variants/v195/portfolio.json | 90 +- .../state/sim/variants/v195/trades.jsonl | 3 + .../state/sim/variants/v197/portfolio.json | 130 +- .../state/sim/variants/v197/trades.jsonl | 5 + .../state/sim/variants/v199/portfolio.json | 109 +- .../state/sim/variants/v199/trades.jsonl | 4 + .../state/sim/variants/v201/portfolio.json | 90 +- .../state/sim/variants/v201/trades.jsonl | 3 + .../state/sim/variants/v203/portfolio.json | 107 +- .../state/sim/variants/v203/trades.jsonl | 4 + .../state/sim/variants/v205/portfolio.json | 128 +- .../state/sim/variants/v205/trades.jsonl | 5 + .../state/sim/variants/v207/portfolio.json | 89 +- .../state/sim/variants/v207/trades.jsonl | 3 + .../state/sim/variants/v209/portfolio.json | 89 +- .../state/sim/variants/v209/trades.jsonl | 3 + .../state/sim/variants/v211/portfolio.json | 86 +- .../state/sim/variants/v211/trades.jsonl | 3 + .../state/sim/variants/v213/portfolio.json | 128 +- .../state/sim/variants/v213/trades.jsonl | 5 + .../state/sim/variants/v215/portfolio.json | 105 +- .../state/sim/variants/v215/trades.jsonl | 4 + .../state/sim/variants/v217/portfolio.json | 121 +- .../state/sim/variants/v217/trades.jsonl | 18 + .../state/sim/variants/v218/portfolio.json | 121 +- .../state/sim/variants/v218/trades.jsonl | 18 + .../state/sim/variants/v223/portfolio.json | 121 +- .../state/sim/variants/v223/trades.jsonl | 18 + .../state/sim/variants/v224/portfolio.json | 121 +- .../state/sim/variants/v224/trades.jsonl | 18 + .../state/sim/variants/v229/portfolio.json | 121 +- .../state/sim/variants/v229/trades.jsonl | 18 + .../state/sim/variants/v230/portfolio.json | 121 +- .../state/sim/variants/v230/trades.jsonl | 18 + .../state/sim/variants/v235/portfolio.json | 121 +- .../state/sim/variants/v235/trades.jsonl | 18 + .../state/sim/variants/v236/portfolio.json | 121 +- .../state/sim/variants/v236/trades.jsonl | 18 + .../state/sim/variants/v265/portfolio.json | 121 +- .../state/sim/variants/v265/trades.jsonl | 18 + .../state/sim/variants/v266/portfolio.json | 121 +- .../state/sim/variants/v266/trades.jsonl | 18 + .../state/sim/variants/v271/portfolio.json | 121 +- .../state/sim/variants/v271/trades.jsonl | 18 + .../state/sim/variants/v272/portfolio.json | 121 +- .../state/sim/variants/v272/trades.jsonl | 18 + .../state/sim/variants/v277/portfolio.json | 121 +- .../state/sim/variants/v277/trades.jsonl | 18 + .../state/sim/variants/v278/portfolio.json | 121 +- .../state/sim/variants/v278/trades.jsonl | 18 + .../state/sim/variants/v283/portfolio.json | 121 +- .../state/sim/variants/v283/trades.jsonl | 18 + .../state/sim/variants/v284/portfolio.json | 121 +- .../state/sim/variants/v284/trades.jsonl | 18 + .../state/sim/variants/v296/portfolio.json | 142 +- .../state/sim/variants/v296/trades.jsonl | 17 + .../state/sim/variants/v298/portfolio.json | 113 +- .../state/sim/variants/v298/trades.jsonl | 5 + .../state/sim/variants/v299/portfolio.json | 71 +- .../state/sim/variants/v299/trades.jsonl | 3 + .../state/sim/variants/v300/portfolio.json | 86 +- .../state/sim/variants/v300/trades.jsonl | 3 + .../state/sim/variants/v301/portfolio.json | 69 +- .../state/sim/variants/v301/trades.jsonl | 2 + .../state/sim/variants/v302/portfolio.json | 93 +- .../state/sim/variants/v302/trades.jsonl | 3 + .../state/sim/variants/v303/portfolio.json | 140 +- .../state/sim/variants/v303/trades.jsonl | 19 + .../state/sim/variants/v304/portfolio.json | 163 +- .../state/sim/variants/v304/trades.jsonl | 20 + .../state/sim/variants/v305/portfolio.json | 187 +- .../state/sim/variants/v305/trades.jsonl | 22 + .../state/sim/variants/v306/portfolio.json | 111 +- .../state/sim/variants/v306/trades.jsonl | 4 + .../state/sim/variants/v307/portfolio.json | 122 +- .../state/sim/variants/v307/trades.jsonl | 18 + .../state/sim/variants/v308/portfolio.json | 118 +- .../state/sim/variants/v308/trades.jsonl | 5 + .../state/sim/variants/v309/portfolio.json | 51 +- .../state/sim/variants/v309/trades.jsonl | 2 + .../state/sim/variants/v310/portfolio.json | 32 +- .../state/sim/variants/v310/trades.jsonl | 1 + .../state/sim/variants/v311/portfolio.json | 67 +- .../state/sim/variants/v311/trades.jsonl | 2 + .../state/sim/variants/v312/portfolio.json | 14 +- .../state/sim/variants/v313/portfolio.json | 100 +- .../state/sim/variants/v313/trades.jsonl | 17 + .../state/sim/variants/v314/portfolio.json | 92 +- .../state/sim/variants/v314/trades.jsonl | 3 + .../state/sim/variants/v315/portfolio.json | 66 +- .../state/sim/variants/v315/trades.jsonl | 2 + .../state/sim/variants/v316/portfolio.json | 92 +- .../state/sim/variants/v316/trades.jsonl | 12 + .../state/sim/variants/v317/portfolio.json | 84 +- .../state/sim/variants/v317/trades.jsonl | 3 + .../state/sim/variants/v318/portfolio.json | 60 +- .../state/sim/variants/v318/trades.jsonl | 3 + .../state/sim/variants/v319/portfolio.json | 60 +- .../state/sim/variants/v319/trades.jsonl | 6 + .../state/sim/variants/v320/portfolio.json | 92 +- .../state/sim/variants/v320/trades.jsonl | 12 + .../state/sim/variants/v321/portfolio.json | 85 +- .../state/sim/variants/v321/trades.jsonl | 3 + .../state/sim/variants/v4/portfolio.json | 91 +- .../state/sim/variants/v4/trades.jsonl | 3 + .../state/sim/variants/v40/portfolio.json | 91 +- .../state/sim/variants/v40/trades.jsonl | 3 + .../state/sim/variants/v41/portfolio.json | 131 +- .../state/sim/variants/v41/trades.jsonl | 5 + .../state/sim/variants/v47/portfolio.json | 134 +- .../state/sim/variants/v47/trades.jsonl | 6 + .../state/sim/variants/v49/portfolio.json | 111 +- .../state/sim/variants/v49/trades.jsonl | 5 + .../state/sim/variants/v51/portfolio.json | 115 +- .../state/sim/variants/v51/trades.jsonl | 5 + .../state/sim/variants/v57/portfolio.json | 67 +- .../state/sim/variants/v57/trades.jsonl | 2 + .../state/sim/variants/v59/portfolio.json | 67 +- .../state/sim/variants/v59/trades.jsonl | 2 + .../state/sim/variants/v81/portfolio.json | 90 +- .../state/sim/variants/v81/trades.jsonl | 4 + .../state/sim/variants/v87/portfolio.json | 69 +- .../state/sim/variants/v87/trades.jsonl | 2 + .../state/sim/variants/v89/portfolio.json | 90 +- .../state/sim/variants/v89/trades.jsonl | 3 + .../state/sim/variants/v91/portfolio.json | 69 +- .../state/sim/variants/v91/trades.jsonl | 2 + .../state/sim/variants/v93/portfolio.json | 69 +- .../state/sim/variants/v93/trades.jsonl | 2 + .../state/sim/variants/v95/portfolio.json | 88 +- .../state/sim/variants/v95/trades.jsonl | 4 + .../state/sim/variants/v97/portfolio.json | 88 +- .../state/sim/variants/v97/trades.jsonl | 3 + .../state/sim/variants/v99/portfolio.json | 65 +- .../state/sim/variants/v99/trades.jsonl | 2 + .../stock/workspace/state/sim/watchlist.json | 50 +- .../stock/workspace/state/trade_journal.jsonl | 37 + completions/openclaw.bash | 1922 ++++++- completions/openclaw.fish | 4634 ++++++++++------- completions/openclaw.ps1 | 1092 +++- completions/openclaw.zsh | 1705 +++--- cron/jobs-state.json.migrated | 121 + cron/{jobs.json => jobs.json.migrated} | 0 openclaw.json | 73 +- skill-workshop/proposals.json | 1 + update-check.json.migrated | 7 + ...58e47d1a42e88eb41d71e9539d3077451.attested | 2 + ...7fa713ba87c5b1c125db2c01f477eb8d3.attested | 3 + workspace/HEARTBEAT.md | 4 +- workspace/openclaw-workspace-state.json | 5 + workspace/scripts/codex_fallback_monitor.py | 214 + 195 files changed, 11185 insertions(+), 8580 deletions(-) create mode 100644 agents/stock/workspace/memory/2026-07-20-0912.md create mode 100644 agents/stock/workspace/openclaw-workspace-state.json create mode 100644 cron/jobs-state.json.migrated rename cron/{jobs.json => jobs.json.migrated} (100%) create mode 100644 skill-workshop/proposals.json create mode 100644 update-check.json.migrated create mode 100644 workspace-attestations/8b2dbe352929b40690dbe9a3e5a138e58e47d1a42e88eb41d71e9539d3077451.attested create mode 100644 workspace-attestations/d1fd54262c444fd6ab822ec4652ce317fa713ba87c5b1c125db2c01f477eb8d3.attested create mode 100644 workspace/openclaw-workspace-state.json create mode 100644 workspace/scripts/codex_fallback_monitor.py diff --git a/CLAUDE.md b/CLAUDE.md index 73dfb3a..f72df2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,7 +101,7 @@ Stock agent (`agents/stock/workspace/scripts/`), run with `python3`: **거래 모달 시스템 (`order-modal` + `pin-modal` + `open-orders-modal`)** — 매매 진입점. 흐름: 종목 select(상단, 보유/관심/감시 통합) → 매수·매도 토글 → 호가창(ka10004 10단계, 1초 polling, visibility 가드) + 입력(계좌·주문유형 LIMIT/MARKET·단가·금액(매수만 양방향)·수량) → 매수/매도 버튼 → propose → PIN 모달(modal-top z-index) 카드 요약 + PIN 입력(`autocomplete="one-time-code"`) + 만료 카운트다운 → verify → 결과 토스트 + 자동 닫기. `[📋 진행중]` 탭은 활성 PIN 카드 → PIN 모달, 미체결만 → open-orders 모달(4계좌 통합 + 행별 취소). 매수 시 금액↔수량 양방향 자동(programmatic .value, 무한루프 X). 매도 토글 시 수량 자동 100%(max_qty). 시장 phase 라벨 + NXT 시간대+`nxt_enable=false` 시 `📵 NXT 거래불가` + 매수/매도 버튼 disable. 우상단 X 없음 — 하단 [닫기]/[취소] + overlay 클릭. **2계좌 동시 매도**(2026-06-12): 매도 버튼 시 **전량매도(입력 수량=max_qty)** 이고 같은 소유자 그룹(본인 일반↔ISA / 가희끼리)의 다른 계좌에도 같은 종목 보유(`accStatus.trde_able_qty>0`)면 `sell-choice-modal` 팝업 — [두 계좌 모두 매도](두 계좌 모두 매도가능 전량, 단가 동일) / [선택 계좌만] / [취소]. 일부매도는 팝업 없이 단일 진행. 매도 정보영역은 그룹 내 다른 계좌도 보유 시 `보유: 141주 / 전체 280주` 병기. 모두 매도는 `POST /api/order/propose_multi` → `handler.propose_trade_multi`(레그별 독립 검증, SELL+LIMIT/MARKET 한정, 같은 그룹만) → **카드 1장+PIN 1개**(iMessage `매도(2계좌)`)가 두 레그 승인 → verify 시 `_submit_multi_legs`가 레그 독립 제출(idem_hash 계좌별 분리)·독립 fill_watcher, 한 레그 실패해도 나머지 시도(ok=하나라도 접수). - **텔레그램 발송 정책 (웹 매매)** — 거부·검증 에러는 토스트만, **매매등록(submit_with_pin 성공)·매매체결(fill_watcher)만** 텔레그램. PIN 메시지는 **iMessage** (Apple 도메인 바인딩 `@stock.hyowons.net #PIN`, iOS Safari OTP 자동입력). `handler.send_imessage_pin` (fire-and-forget Popen, AppleEvent timeout -1712 떠도 메시지는 큐로). credential `credentials/admin_imessage.json` `{"handle": "01012345678"}`. 자기 자신 iMessage self-send 가능 (mac → 본인 iCloud handle). + **텔레그램 발송 정책 (웹 매매)** — 거부·검증 에러는 토스트만, **매매등록(submit_with_pin 성공)·매매체결(fill_watcher)만** 텔레그램. PIN 메시지는 **iMessage** (Apple 도메인 바인딩 `@stock.hyowons.net #PIN`, iOS Safari OTP 자동입력). `handler.send_imessage_pin` (fire-and-forget Popen, AppleEvent timeout -1712 떠도 메시지는 큐로). credential `credentials/admin_imessage.json` `{"handle": "01012345678"}`. 자기 자신 iMessage self-send 가능 (mac → 본인 iCloud handle). **규칙(나→나 iMessage)**: 관리자 본인 handle로 보내는 iMessage는 항상 `--service imessage`(파란) 강제 — SMS 셀프발송 금지. 이유: iMessage 셀프발송은 Note-to-Self라 보낸 버블만 뜨고 OTP 도메인 자동입력이 정상 동작하지만, SMS 셀프발송은 통신사 loopback으로 초록·수신버블이 생기고 자동입력이 깨질 수 있음. 타인 대상(예: 가희 리마인더 `gahee_reminder._send_imessage`는 `--service sms`)은 예외. **신규 endpoint**: `/api/quote_book?code` (ka10004 호가 10단계 + ka10001 현재가 + `nxt_enable`), `/api/order/check?code&account&side&price` (잔액·보유·max_qty·매도 손익 미리보기), `POST /api/order/propose` (handler.propose_trade wrapper, 성공 시만 텔레그램·iMessage), `POST /api/order/propose_multi` (2계좌 동시 매도 — accounts/qtys 콤마 동수 리스트, handler.propose_trade_multi wrapper), `POST /api/order/verify` (handler.submit_with_pin wrapper, 성공 시만 텔레그램), `POST /api/order/cancel` (handler.cancel_active_card, 텔레그램 X), `/api/order/active` (PinStore.peek + 4계좌 미체결 카운트), `/api/orders/open` (ka10075 4계좌), `POST /api/orders/cancel?ord_no&account` (handler.cancel_open_order + 텔레그램), `/api/market_state` (regular/nxt/closed/holiday/weekend phase), `/api/symbols/all` (보유+관심+감시 통합 dedup) @@ -156,6 +156,7 @@ OpenClaw 자동화는 두 갈래로 동작한다 (모두 Asia/Seoul): - **stock.sim-web** — 상시 (`KeepAlive`) — 시뮬 대시보드 **별도 서버 포트 18792** (behive-web 18790과 분리). `python3 -m sim.sim_web serve`. sim state 읽기전용 렌더. `sim.hyowons.net` → mac:18792 (Synology reverse proxy, **수동 등록 대기**). ⚠️ 18791은 node 점유라 18792 사용 - **gmail-label-classify** (main/클로) — 매일 01:00 — `workspace/scripts/gmail_label_classify.py`: self-sent 브리핑·종목분석·주식 리포트 메일을 Gmail 라벨로 자동 분류 + 24h 지난 테스트메일 휴지통 이동. LLM 미경유(`gog` CLI 직접 호출). 로그 `logs/gmail-label-classify.{log,err.log}` + 상태 `state/gmail_label_classify.log`. ⚠️ 2026-06-26 OpenClaw cron(main 01:00)에서 이관 — 본문 생성 없는 단순 스크립트라 모델 세션 불필요(TASKS.md §3-3 원칙) - **git-autopush** — 매일 02:00 — `scripts/git_autopush.sh`: 변경 있으면 `git add -A` + 자동 커밋 + `git push origin main`. 변경 없으면 skip(빈 커밋 X). 인증은 HTTPS + `credential.helper=store`(`~/.git-credentials`), GUI 키체인 불필요. 로그 `logs/git-autopush.{log,err.log}`. 워크스페이스 버전관리 백업용 (시크릿·sqlite는 .gitignore 제외라 별도 백업 필요) +- **codex-fallback-monitor** — 평일·주말 매 5분(`StartCalendarInterval` Minute 0/5/…/55) — `workspace/scripts/codex_fallback_monitor.py`: codex 주 모델(openai/gpt-5.5)이 죽어 게이트웨이가 조용히 **유료 OpenRouter로 폴백**하면 토큰비가 새는 걸 감지→클로(main) 텔레그램 즉시 알림. 신호원=`agents/*/sessions/*.trajectory.jsonl`의 `model.completed` 이벤트(승자 모델). 승자 `provider==openrouter`면 폴백으로 판정(정상=`openai/gpt-5.5` codex 하네스). 무비용·온디스크·LLM 미경유. dedupe=`state/codex_fallback_monitor.json` 워터마크(`last_ts`)+30분 쿨다운(장기 다운 스팸 방지). 첫 실행은 과거 무시하고 워터마크만 세팅. 발송은 텔레그램 Bot API 직접 HTTP POST(`openclaw.json` `channels.telegram.accounts.default` 토큰·allowFrom, 레이 `send_telegram` 스크립트와 동일 패턴, urllib stdlib). 로그 `logs/codex-fallback-monitor.{log,err.log}`. ⚠️ codex 에러 자체는 로그·audit(metadata-only)에 안 남고 폴백 시 OpenRouter는 codex 하네스 로그를 안 남겨, trajectory `model.completed`가 유일하게 신뢰 가능한 승자 신호 - **budget.whooing-sync** — 매시 0/15/30/45분 — iMessage 결제문자 → 후잉. 매 사이클 끝에 `gahee_reminder.run` 추가 호출 (매월 25일 10:00 KST 이후 가희님께 iMessage 리마인더 1회 발신 → 답신 폴링 → 텍스트면 후잉 `가희주머니` 차액 자동분개, 이미지면 골디 텔레그램 알림). 별도 plist 없음 ## Agent Inbox Convention diff --git a/agents/stock/workspace/memory/2026-07-20-0912.md b/agents/stock/workspace/memory/2026-07-20-0912.md new file mode 100644 index 0000000..ad64dca --- /dev/null +++ b/agents/stock/workspace/memory/2026-07-20-0912.md @@ -0,0 +1,66 @@ +# Session: 2026-07-20 09:12:03 GMT+9 + +- **Session Key**: agent:stock:telegram:direct:8443122995 +- **Session ID**: cb10d003-728f-4145-92ec-22ca9663e308 +- **Source**: telegram + +## Conversation Summary + +user: Conversation info (untrusted metadata): +```json +{ + "chat_id": "telegram:8443122995", + "message_id": "1064", + "sender_id": "8443122995", + "sender": "snowoyh bang", + "timestamp": "Mon 2026-07-20 09:11 GMT+9", + "inbound_event_kind": "user_request" +} +``` + +Sender (untrusted metadata): +```json +{ + "label": "snowoyh bang (8443122995)", + "id": "8443122995", + "name": "snowoyh bang", + "username": "snowoyh" +} +``` + +Conversation context (untrusted, chronological, selected for current message): +#1048 Tue 2026-07-14 22:53 GMT+9 snowoyh bang: 당일종가랑 현재가가 달라 +#1050 Tue 2026-07-14 22:54 GMT+9 snowoyh bang: 1928000은 무슨가격이야? +#1052 Tue 2026-07-14 22:57 GMT+9 snowoyh bang: 1941000이 종가고 현재가가 1928000으로 나와 + +관리자 가희 계좌 통틀어서 삼성전자 하이닉스 제외하고 계좌상태 고려해서 3천만원 예수금 만들려면 뭐를 파는세 좋아? +assistant: [assistant turn failed before producing content] +user: Conversation info (untrusted metadata): +```json +{ + "chat_id": "telegram:8443122995", + "message_id": "1064", + "sender_id": "8443122995", + "sender": "snowoyh bang", + "timestamp": "Mon 2026-07-20 09:11 GMT+9", + "inbound_event_kind": "user_request" +} +``` + +Sender (untrusted metadata): +```json +{ + "label": "snowoyh bang (8443122995)", + "id": "8443122995", + "name": "snowoyh bang", + "username": "snowoyh" +} +``` + +Conversation context (untrusted, chronological, selected for current message): +#1048 Tue 2026-07-14 22:53 GMT+9 snowoyh bang: 당일종가랑 현재가가 달라 +#1050 Tue 2026-07-14 22:54 GMT+9 snowoyh bang: 1928000은 무슨가격이야? +#1052 Tue 2026-07-14 22:57 GMT+9 snowoyh bang: 1941000이 종가고 현재가가 1928000으로 나와 + +관리자 가희 계좌 통틀어서 삼성전자 하이닉스 제외하고 계좌상태 고려해서 3천만원 예수금 만들려면 뭐를 파는세 좋아? +assistant: ⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session. diff --git a/agents/stock/workspace/openclaw-workspace-state.json b/agents/stock/workspace/openclaw-workspace-state.json new file mode 100644 index 0000000..a6ae33b --- /dev/null +++ b/agents/stock/workspace/openclaw-workspace-state.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "bootstrapSeededAt": "2026-04-17T03:21:06.738Z", + "setupCompletedAt": "2026-04-17T05:20:30.244Z" +} diff --git a/agents/stock/workspace/scripts/behive_web.py b/agents/stock/workspace/scripts/behive_web.py index 7c6d317..8675b87 100644 --- a/agents/stock/workspace/scripts/behive_web.py +++ b/agents/stock/workspace/scripts/behive_web.py @@ -79,6 +79,7 @@ QUOTE_WORKERS = 8 QUOTE_CACHE_TTL = 30.0 # 첫 cold 8s 동안 분산 박힌 캐시가 다음 RENDER 만료(10s) 시점에 살아있도록. 가격 stale 최대 30s. sys.path.insert(0, str(WORKSPACE / 'scripts')) +sys.path.insert(0, str(WORKSPACE)) # sim 패키지(from sim import signals, config) import 용 _holidays_cache: dict = {'mtime': 0.0, 'set': set()} @@ -87,6 +88,155 @@ _holding_since_cache: dict = {'mtime': None, 'map': {}} _quote_cache: dict[str, tuple[dict, float]] = {} _quote_cache_lock = threading.Lock() +# 계좌별 미체결(ka10075) 캐시 — 거래 모달 max_qty 계산용. 미체결은 가격과 무관하므로 +# 단가 keystroke마다 재조회하지 않도록 짧은 TTL로 묶는다. 주문 접수 후 프론트가 재조회하면 +# 최대 TTL 만큼 stale할 수 있으나, propose/키움 서버가 최종 검증하므로 표시용으론 충분. +OPEN_ORDERS_CACHE_TTL = 15.0 +_open_orders_cache: dict[str, tuple[list, float]] = {} +_open_orders_cache_lock = threading.Lock() + + +def _cached_open_orders(account_label: str) -> list: + """계좌 미체결 주문 목록 (ka10075) — TTL 캐시. 실패 시 빈 리스트.""" + now = time.time() + with _open_orders_cache_lock: + hit = _open_orders_cache.get(account_label) + if hit and (now - hit[1]) < OPEN_ORDERS_CACHE_TTL: + return hit[0] + try: + import kiwoom_client as kc + rows = kc.get_open_orders(account_label) + except Exception: + return [] + with _open_orders_cache_lock: + _open_orders_cache[account_label] = (rows, now) + return rows + + +def _pending_buy_lock(account_label: str) -> int: + """계좌의 미체결 매수 지정가로 묶인 예수금 합계. d2_entra는 이 금액을 아직 포함하므로 + 실제 매수가능액 = d2_entra − 이 값. 시장가(price=0) 미체결은 금액 미상이라 제외(render 경로와 동일).""" + total = 0 + for o in _cached_open_orders(account_label): + if o.get('side') == 'BUY': + px = o.get('order_price') or 0 + if px > 0: + total += px * (o.get('unfilled_qty') or 0) + return total + + +def _pending_sell_qty(account_label: str, code: str) -> int: + """계좌·종목의 미체결 매도 수량 합계.""" + total = 0 + for o in _cached_open_orders(account_label): + if o.get('side') == 'SELL' and o.get('code') == code: + total += o.get('unfilled_qty') or 0 + return total + + +# 기술적 권장가(매수/매도 판단 보조) 캐시 — 완성된 일봉 기반이라 장중 안정적. +# 키: f'{code}:{side}:{avg_price}'. 현재가 대비 비교는 클라이언트가 실시간 시세로 처리. +ADVICE_CACHE_TTL = 120.0 +_advice_cache: dict[str, tuple[dict, float]] = {} +_advice_cache_lock = threading.Lock() + + +def _price_advice(code: str, side: str, avg_price: int | None = None) -> dict | None: + """일봉 기반 기술적 권장 매수가/매도가. 근거는 이평·ATR만(sim.signals 규칙 재사용). + + 반환: {'levels': [{label, price, hint, tap}], 'atr','sma20','rsi','price','recent_low','recent_high'} + 데이터 부족(신규주)·ETF 등으로 지표 산출 불가 시 None (예외 raise 안 함). + ⚠️ 상수는 활성 sim.config 사용 — params.json 튜닝이 걸려있으면 그 값이 반영됨(현재는 문서화된 기본값). + """ + side = (side or '').upper() + cache_key = f'{code}:{side}:{avg_price or 0}' + now = time.time() + with _advice_cache_lock: + hit = _advice_cache.get(cache_key) + if hit and (now - hit[1]) < ADVICE_CACHE_TTL: + return hit[0] + try: + from sim import signals, config + import daily_candles_cache as dcc + candles = dcc.get_candles(code, count=250) + ind = signals.compute_indicators(candles) if candles else None + except Exception: + traceback.print_exc() + ind = None + if not ind: + with _advice_cache_lock: + _advice_cache[cache_key] = (None, now) + return None + + price = ind['price'] + atr = ind['atr'] + sma20 = ind['sma_long'] + prev_close = ind['prev_close'] + recent_high = ind['recent_high'] + recent_low = ind['recent_low'] + rsi = ind.get('rsi') + levels: list[dict] = [] + + if side == 'BUY': + pullback = int(round(max(sma20, prev_close - atr * config.PULLBACK_ATR_MULT))) + p_stop, p_target = signals.compute_stop_target(pullback, atr, recent_low) + levels.append({ + 'label': '눌림목 매수가', 'price': pullback, 'tap': True, + 'hint': f'20일선({sma20:,.0f})과 전일종가−ATR({prev_close - atr * config.PULLBACK_ATR_MULT:,.0f}) 중 높은 값. ' + f'여기까지 눌리면 매수 검토(지정가로 걸어둠).', + }) + if recent_high: + levels.append({ + 'label': '돌파 기준가', 'price': int(recent_high), 'tap': True, + 'hint': f'최근 {config.BREAKOUT_LOOKBACK}일 전고점. 이 위로 거래량 실려 뚫으면 돌파 매수 신호.', + }) + levels.append({ + 'label': '(눌림목 매수 시) 예상 손절', 'price': p_stop, 'tap': False, + 'hint': f'매수가−ATR×{config.STOP_ATR_MULT:g}와 최근 {config.SWING_LOW_LOOKBACK}일 저점 중 위쪽. 여기 깨지면 손절.', + }) + levels.append({ + 'label': '(눌림목 매수 시) 목표가', 'price': p_target, 'tap': False, + 'hint': f'손익비 {config.RR_RATIO:g}:1 — 손절폭의 {config.RR_RATIO:g}배 위. 1차 익절 목표.', + }) + else: # SELL — 보유 청산. 평단 고정은 고수익주에서 붕괴 → 지지·저항·트레일링으로. + trail = int(round(price - atr * config.TRAIL_ATR_MULT)) + levels.append({ + 'label': '트레일링 손절', 'price': trail, 'tap': True, + 'hint': f'현재가−ATR×{config.TRAIL_ATR_MULT:g}. 고점 대비 이만큼 되돌리면 익절/이탈(수익 보호).', + }) + levels.append({ + 'label': '20일선 지지', 'price': int(round(sma20)), 'tap': True, + 'hint': '20일 이동평균선. 추세 지지선 — 종가로 깨지면 상승추세 이탈 신호.', + }) + if recent_low: + levels.append({ + 'label': f'{config.SWING_LOW_LOOKBACK}일 스윙저점', 'price': int(recent_low), 'tap': True, + 'hint': f'최근 {config.SWING_LOW_LOOKBACK}일 최저가. 이 아래로 밀리면 하락 전환 경계.', + }) + if recent_high: + levels.append({ + 'label': '전고점(저항/목표)', 'price': int(recent_high), 'tap': True, + 'hint': f'최근 {config.BREAKOUT_LOOKBACK}일 전고점. 1차 저항 — 도달 시 분할 익절 고려.', + }) + # 평단 근처(±ATR)일 때만 평단 기준 손절선 추가 — 그 밖(고수익/큰손실)에선 무의미해 생략. + if avg_price and abs(price - avg_price) <= atr: + a_stop, _ = signals.compute_stop_target(int(avg_price), atr, recent_low) + levels.append({ + 'label': '평단 기준 손절', 'price': a_stop, 'tap': True, + 'hint': f'평단({avg_price:,})−ATR×{config.STOP_ATR_MULT:g}와 스윙저점 중 위쪽. 본전 근처에서 손실 방어선.', + }) + + result = { + 'levels': levels, 'price': int(price), 'atr': int(round(atr)), + 'sma20': int(round(sma20)), 'rsi': (round(rsi) if rsi is not None else None), + 'recent_low': int(recent_low) if recent_low else None, + 'recent_high': int(recent_high) if recent_high else None, + } + with _advice_cache_lock: + _advice_cache[cache_key] = (result, now) + return result + + INDICES_CACHE_TTL = 30.0 _indices_cache: dict = {'data': None, 'expires_at': 0.0} _indices_lock = threading.Lock() @@ -2559,8 +2709,8 @@ def _render_row(c: dict, source: str = 'watchlist') -> str: trade_mark = ( f'' ) if has_trades and source == 'interests' else '' - # 매매 진입 버튼 — 보유 모드면 매도 default, 그 외는 매수 default. 모달에서 토글 가능. - order_side = 'SELL' if c.get('mode') == 'held' else 'BUY' + # 매매 진입 버튼 — 항상 매수 default (오타 방지). 모달에서 매도로 토글 가능. + order_side = 'BUY' order_btn = ( f'' ) if raw_code else '' @@ -3349,7 +3499,7 @@ def _render_holding_row(r: dict, total_value: int, show_day_change: bool = False f'{note_btn}' f'{_info_button_html(r.get("code") or "", r.get("stock") or "")}' f'' - f'' + f'' f'' ) if code else '' tag_chip = _tag_chip_html(r.get('code') or '', r.get('stock') or '', interactive=True) @@ -5793,7 +5943,7 @@ table.market-adr td.adr-breakdown { font-size: 11px; color: #8b8f9a; } } /* 주문 모달 (order-modal) — 거래내역 보기용 trade-modal과 별개. 모달 박스 자체 스크롤 X (호가창만 스크롤) */ -.order-box { max-width: 560px; overflow-y: hidden; display: flex; flex-direction: column; transition: border-color 0.15s, box-shadow 0.15s, background 0.15s; } +.order-box { max-width: 640px; max-height: calc(100dvh - env(safe-area-inset-top) - 56px); overflow-y: hidden; display: flex; flex-direction: column; transition: border-color 0.15s, box-shadow 0.15s, background 0.15s; } .order-box > [data-order-step="1"] { display: flex; flex-direction: column; flex: 1; min-height: 0; } /* 매수/매도 모드 전체 색조 — 모달 어디를 봐도 side가 한눈에 (매수=빨강/매도=파랑) */ .order-box.side-buy { border-color: #c9303e; box-shadow: 0 0 0 1px #c9303e, 0 12px 44px rgba(201,48,62,0.34); background: linear-gradient(180deg, rgba(201,48,62,0.12), rgba(201,48,62,0.02) 130px, #0d1018 300px); } @@ -5802,6 +5952,8 @@ table.market-adr td.adr-breakdown { font-size: 11px; color: #8b8f9a; } .order-box.side-sell .modal-head { border-bottom: 2px solid rgba(42,95,179,0.55); padding-bottom: 10px; } .order-box.side-buy .orderbook { border-color: rgba(201,48,62,0.38); } .order-box.side-sell .orderbook { border-color: rgba(42,95,179,0.38); } +/* 주문 방향 확인 팝업 — 매수·매도 버튼을 직접 골라 누르게 함(위치 랜덤). 오타 방지 */ +#order-confirm-modal .btn-confirm { font-size: 16px; font-weight: 700; padding: 15px 14px; } .order-symbol-select { background: #14171f; color: #f0f0f0; border: 1px solid #1f2330; border-radius: 6px; padding: 7px 9px; font-size: 14px; font-weight: 600; font-family: inherit; width: 100%; max-width: 360px; cursor: pointer; color-scheme: dark; } .order-symbol-select:focus { outline: none; border-color: #ff4d5e; } .order-symbol-select option { background: #14171f; color: #f0f0f0; } @@ -5831,8 +5983,8 @@ table.market-adr td.adr-breakdown { font-size: 11px; color: #8b8f9a; } .orderbook::-webkit-scrollbar-thumb { background: #2a2e3a; border-radius: 2px; } .orderbook .ob-row { display: grid; grid-template-columns: 1fr 1fr; padding: 1px 5px; font-size: 7.5px; line-height: 1.35; font-variant-numeric: tabular-nums; cursor: pointer; border-bottom: 1px solid #15171e; transition: box-shadow 0.08s; color: #565c68; } .orderbook .ob-row:last-of-type { border-bottom: 0; } -.orderbook .ob-row.ask { background: linear-gradient(to left, rgba(201,48,62,0.16), transparent 60%); color: #ff8a95; } -.orderbook .ob-row.bid { background: linear-gradient(to left, rgba(42,95,179,0.16), transparent 60%); color: #8db4ff; } +.orderbook .ob-row.ask { background: linear-gradient(to left, rgba(42,95,179,0.16), transparent 60%); color: #8db4ff; } +.orderbook .ob-row.bid { background: linear-gradient(to left, rgba(201,48,62,0.16), transparent 60%); color: #ff8a95; } .orderbook .ob-row:hover { background: #1c1f29; } .orderbook .ob-row.ob-current { background-color: rgba(230,230,230,0.13); color: #fff; font-weight: 700; box-shadow: inset 3px 0 0 #e6e6e6; } .orderbook .ob-row.ob-synthetic { background: rgba(230,230,230,0.10); } @@ -5844,7 +5996,7 @@ table.market-adr td.adr-breakdown { font-size: 11px; color: #8b8f9a; } .orderbook .ob-row.selected { box-shadow: inset 0 0 0 1.5px #ffcc33; color: #fff !important; font-weight: 700; background-color: rgba(255,204,51,0.18) !important; } .orderbook .ob-row .price { text-align: left; } .orderbook .ob-row .qty { text-align: right; opacity: 0.8; font-size: 7px; } -.price-tick-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; margin-top: 4px; } +.price-tick-buttons { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 4px; margin-top: 4px; } .price-tick-buttons button { background: #14171f; color: #c9ccd3; border: 1px solid #1f2330; border-radius: 5px; padding: 5px 4px; font-size: 11px; cursor: pointer; font-family: inherit; } .price-tick-buttons button:hover { border-color: #ff4d5e; color: #f0f0f0; } .price-tick-buttons button:active { transform: translateY(1px); } @@ -5871,6 +6023,27 @@ table.market-adr td.adr-breakdown { font-size: 11px; color: #8b8f9a; } .order-info .pl-down { color: #8db4ff; white-space: nowrap; } .order-info b.qty-over { color: #ff8a95; } input[data-order-qty].qty-over { color: #ff8a95; } +.price-tick-buttons .advice-open-btn { padding: 5px 4px; border-radius: 5px; background: #1a1710; border: 1px solid #4a3c1a; color: #e6c86b; font-size: 11px; font-weight: 700; cursor: pointer; text-align: center; font-family: inherit; white-space: nowrap; } +.price-tick-buttons .advice-open-btn:hover { border-color: #e6c86b; color: #f0d98a; } +.price-tick-buttons .advice-open-btn:active { transform: translateY(1px); } +.advice-box { max-width: 420px; } +.advice-body { padding: 4px 2px; } +.advice-sub { font-size: 11px; color: #9aa0ad; margin-bottom: 8px; line-height: 1.4; } +.advice-rows { display: flex; flex-direction: column; gap: 6px; } +.advice-row { display: flex; align-items: center; gap: 8px; width: 100%; padding: 9px 11px; border-radius: 8px; background: #1a1f2c; border: 1px solid #2a3040; text-align: left; cursor: default; } +.advice-row .advice-lbl { font-size: 12px; color: #c9ccd3; flex: 1; } +.advice-row .advice-px { font-size: 14px; font-weight: 700; color: #e8eaf0; font-variant-numeric: tabular-nums; white-space: nowrap; } +.advice-rel { font-size: 10px; } +.advice-rel.up { color: #ff8a95; } +.advice-rel.dn { color: #8ab6ff; } +.advice-tapmark { font-size: 10.5px; font-weight: 700; color: #8ab6ff; white-space: nowrap; margin-left: 2px; } +.advice-row.advice-tap { cursor: pointer; border-color: #34506e; background: #16202e; } +.advice-row.advice-tap:active { background: #1e2c3e; } +.advice-row.advice-info { opacity: 0.72; } +.advice-why { margin: 10px 0 0; background: none; border: none; color: #8ab6ff; font-size: 12px; font-weight: 600; cursor: pointer; padding: 4px 2px; } +.advice-hints { margin: 6px 0 2px; padding-left: 16px; font-size: 11.5px; color: #b7bcc7; line-height: 1.5; } +.advice-hints li { margin-bottom: 5px; } +.advice-hints b { color: #d6dae2; white-space: nowrap; } .order-adr-banner { padding: 8px 12px; margin-bottom: 12px; border-radius: 8px; font-size: 12.5px; font-weight: 600; line-height: 1.4; } .order-adr-banner.adr-over { background: #2a1418; border: 1px solid #5a1f28; color: #ff8a95; } .order-adr-banner.adr-under { background: #101b2e; border: 1px solid #1f3a5a; color: #8ab6ff; } @@ -7167,6 +7340,26 @@ def _render_info_desc_modal() -> str: ) +def _render_advice_modal() -> str: + """기술적 권장가 팝업. order-modal 위에 겹쳐 뜨는 modal-top. [💡 권장가 보기] 클릭 시 표시. + 가격 행 탭 → 단가 자동입력(호가단위 스냅) 후 팝업 닫힘. ▸ 근거로 계산식 설명 토글.""" + return ( + '' + ) + + def _render_order_modal() -> str: """주문 진입 모달 (매수/매도). 호가창 1초 폴링, PIN OTP 흐름. @@ -7208,12 +7401,18 @@ def _render_order_modal() -> str: '' - '