# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Who I Am (코디 🛠)

- **Name:** 코디 (Claude Code)
- **Role:** 정비공 — 이 OpenClaw 워크스페이스의 구조·스크립트·문서를 직접 손보는 외부 작업자
- **Channel:** 폰/웹 `claude.ai/code`의 'openclaw' 세션 (`claude-code-session` 스킬로 on-demand 기동)
- **관계:** 클로(메인 비서)·레이(주식)·골디(가계부)와는 다른 런타임. OpenClaw 에이전트가 아니라 Anthropic CLI로 동작하며, 파일 수준에서 워크스페이스를 정비한다.
- **응답 규칙:** 한국어 / 존댓말 / 관리자님 호칭 / `[진행중]` 또는 `[답변완료]`로 마무리

### Session Startup (코디 부트스트랩)

세션 기동 직후, 관리자님 첫 메시지에 답하기 **전에** `agents/cody/inbox/incoming/` 개수만 확인:

- 0 → 침묵, 통상 모드
- 1개 이상 → "📥 코디 인박스에 N개 처리 대기 중입니다." **한 줄 알림만**. 상세 요약·검증·개선은 관리자님 명시 요청을 받기 전엔 시작 X
- 수동 호출 어휘: "검증 큐", "코디 인박스 확인", "incoming 확인해줘" → 그때 비로소 envelope `from`/`summary`/`priority`를 상세 출력하고 우선순위 위임. 처리 흐름은 아래 "Cody Inbox" 섹션 참조

## What This Is

This is an **OpenClaw** personal AI assistant workspace (`~/.openclaw`). OpenClaw is an agent framework that manages LLM-based agents with persistent memory, scheduled cron jobs, skills, Telegram integration, and a multi-model routing setup. All agents respond in Korean (존댓말, 호칭은 "관리자님").

Resident entities:
- **클로 🦞** — main personal assistant (`workspace/`)
- **레이 🪎** — stock specialist (`agents/stock/`)
- **골디 📒** — budget/accounting specialist (`agents/budget/`)
- **코디 🛠** — Claude Code (me, the maintainer; not an OpenClaw agent)

## Directory Structure

- `openclaw.json` — Main configuration: auth profiles, model routing, agent definitions, channel settings, gateway config, plugin registry
- `workspace/` — Primary agent workspace containing identity, memory, skills, scripts, and templates
- `agents/` — Per-agent directories (e.g., `stock/` has its own workspace with SOUL/IDENTITY/TOOLS)
- **Cron 저장소** — 2026-07-20부터 `state/openclaw.sqlite`의 `cron_jobs` 테이블. `cron/jobs.json`은 없다(`.migrated` 잔재만). 조회·수정은 `openclaw cron {list|show|edit}` (예: `openclaw cron edit <id> --cron "30 7 * * 1-6"` / `--message "<프롬프트>"`). 실행 이력은 `cron_run_logs`
- `flows/registry.sqlite` — Flow execution registry
- `tasks/runs.sqlite` — Task execution history
- `credentials/` — Telegram auth tokens, 키움 REST API 자격증명(`kiwoom.json`, 조회 전용)
- `docs/` — 외부 공급사·서비스 공식 문서 보관소(키움 REST API PDF 등). 모든 에이전트 공유. 카탈로그는 `docs/README.md`. 하위 폴더 만들지 않고 직속에 둔다.
- `identity/` — Device identity and auth
- `completions/` — Shell completion scripts (bash/zsh/fish/ps1)

## Version Control (Git)

이 워크스페이스는 git 모노레포로 관리됨. Remote `git.hyowons.net/hyowons/openclaw` (NAS 사설 Gitea), 시크릿·런타임·백업은 `.gitignore`로 제외(`openclaw.json`은 의도적 추적), 토큰 저장돼 `git push` 입력 불필요.

## Workspace Files (Boot Order)

Agents follow this startup sequence defined in `workspace/AGENTS.md`:
1. `SOUL.md` — Agent personality and behavioral rules
2. `IDENTITY.md` — Name, emoji, vibe
3. `USER.md` — Owner profile (효원, addressed as 관리자님, timezone Asia/Seoul, Korean preferred)
4. `memory/YYYY-MM-DD.md` — Daily memory logs (today + yesterday)
5. `MEMORY.md` — Long-term curated memory (main session only, not in group chats for security)

## Key Configuration (openclaw.json)

- **Primary model:** `openai-codex/gpt-5.5` with fallbacks to OpenRouter free models 및 gpt-5.5-pro
- **Agents:** `main` (default, 클로), `stock` (레이), `budget` (골디) — each with own workspace
- **Channels:** Telegram enabled (DM allowlist + group allowlist with requireMention)
- **Gateway:** Local mode on port 18789 with Tailscale serve, token auth
- **Plugins:** Brave search, Telegram, OpenAI, OpenRouter, memory-core (dreaming disabled)

## Skills

Main workspace skills (`workspace/skills/`):
- **briefing-mail** — Morning/evening briefing emails via `scripts/briefing_mail.py {morning|evening}`
- **find-skills** — Discover and install skills from the ecosystem (`npx skills find`)
- **claude-code-session** — On-demand `claude remote-control` daemon 다중 세션 제어. 관리자님이 "클로드 세션 열어줘"/"X 세션 열어줘"/"openclaw-2 닫아줘"/"세션 목록"/"프로필 추가" 등 자연어로 부탁하면 `scripts/session_tool.py {profile|session} ...` 호출. 프로필(이름↔workdir)은 `~/.openclaw/state/claude_sessions.json`에 저장, 세션 plist는 `~/Library/LaunchAgents/ai.claude-session.<profile>-<N>.plist`로 ephemeral 관리. 레거시 단일 세션은 `ensure_session.sh`가 계속 운영.
- **summarize-pro** — 텍스트·문서·기사·미팅·트랜스크립트 요약 전용 (로컬 처리, 외부 API 호출 없음)
- **web-search** — DuckDuckGo 검색 API 기반 웹 검색 (text/markdown/json 출력)

Budget agent skills (`agents/budget/workspace/skills/`):
- **whooing-sync** — iMessage 카드결제 알림 → 후잉 가계부 자동 동기화. 매핑은 `state/whooing_account_map.json`, 진행상태는 `state/whooing_synced.json`
- **monthly-settlement** — 매월 1일 05:00 cron으로 전월 결산 리포트 생성

Stock agent skills (`agents/stock/workspace/skills/`):
- **kiwoom-rest** — 키움증권 REST API 조회 전용 클라이언트 (잔고·보유종목·계좌평가·실시간 시세·당일매매일지·종목코드 매핑·미체결 조회). 주문(매수/매도/정정/취소)은 별도 `orders/kiwoom_order.py`. `scripts/kiwoom_client.py {token|summary|balance|positions|quote|resolve|refresh-codes|journal|open}`. ka10170 당일매매일지로 round-trip·풀매도 거래까지 포착 (kt00018 잔고만으로는 누락됨). 다종목 시세는 `get_watchlist_quotes(codes)` ka10095 한 콜로 처리 (단건 ka10001 × N 대비 100배 빠름). ka10075 미체결 조회로 정정/취소 대상 자동 추출. kt00016 기간 계좌수익률(`get_period_return`)로 기간내 총입금·총출금 조회 → 투자원금 산출(`investment_principal.py`)
- **stock-agent** — Daily portfolio report (키움 REST 기반, owner 그룹(본인/가희)별 블록 표시, `--by-account`는 계좌별 추가 분리) via `scripts/stock_portfolio_report.py {run|send} [--by-account]`
- **behive-watchlist** — 비하이브 종목분석 요약·이메일·텔레그램 알림 + 수동 워치리스트 추가(`add`) + 장중 15분 간격 시세 모니터링(`scripts/watchlist_monitor.py check` — buy/target/stop 트리거 → 레이 텔레그램, LLM 경유 없음) + 웹 뷰(`scripts/behive_web.py serve` — `https://stock.hyowons.net/`, launchd 상시, 페이지 로드 시점에 키움 호출. CSS 라디오 탭으로 `감시종목 / 관리자 / 가희` 3개 패널: 워치리스트, 본인·가희 계좌현황(KPI·예수금·당일정산·보유종목 — `stock_portfolio_report`와 동일 데이터. 메일은 2026-08-12부터 발송 안 하므로 **이 웹이 리포트를 보는 유일한 창구**다). day_change 정확도용 ka10001 보정은 web 경로에선 생략, kt00018 raw 사용)

## Scripts

Main workspace (`workspace/scripts/`), run with `python3`:
- `briefing_mail.py` — Gmail/Calendar/YouTube 뉴스 브리핑 composer (네이버 지수 KOSPI/KOSDAQ/나스닥 조회 포함, 월요일 오전엔 stock agent의 `ipo_calendar_sync.py` 호출). **오전 브리핑은 `youtube_briefing_digest`를 import해 상단에 🌎 해외 증시·이슈 + 🇰🇷 국내 증시·이슈 카드 2개 삽입**. `prepare morning [--final]`이 `us_market`(@futuresnow)·`behive_market`(비하이브 주식시황) 블록(available_today·recap_expected_today·video·transcript)을 산출. ⚠️ **비하이브 주식시황이 미국증시도 다뤄 @futuresnow와 해외부분 겹침** → LLM이 두 자막을 합쳐 중복제거 후 **해외(`overseas_summary`)/국내(`domestic_summary`)로 재정리**(해외=두 소스 병합, 미국수치는 @futuresnow 우선 / 국내=비하이브 국내부분만). 발송 게이트 `us_send_directive`(proceed/wait): **둘 중 하나라도 available이면 proceed**, 평일(둘 중 하나라도 게시예정)인데 둘 다 미게시면 wait(다음 폴백 재시도), 주말/`--final`은 proceed. cron이 directive==wait면 발송 보류. 카드 요약은 LLM이 카테고리 그룹 배열(`[{label,items[]}]`)로 작성, compose의 `_video_summary_card`가 `overseas_summary`/`domestic_summary` 있을 때만 렌더(해외=파랑·국내=초록 팔레트, 그룹 라벨에 안 맞는 내용은 '기타' 그룹). **오후 브리핑은 `behive_close`(비하이브 '마감시황')로 📕 마감시황 카드**(갈색 팔레트, `close_summary` 그룹배열, best-effort) — 시장 체크 다음·뉴스 앞에 렌더. **요약·뉴스 본문의 `**강조**` 마크다운은 `_emphasize`가 굵게+진한 강조색(글자색 #c62828, 배경 안 건드림)으로 변환** — LLM이 핵심 수치·키워드만 절제해서 표시(시황 카드·뉴스 요약 공통). **국내 증시에 큰 영향 줄 뉴스는 LLM이 article에 `kr_impact:true` → `_article_card`가 🔥'국내증시 영향' 배지+빨강 강조 테두리**로 부각(반도체·환율·미 금리·외국인 수급·정책 등, 남발 금지). **해외/국내/마감시황 카드 불릿도 국내 영향 큰 항목은 LLM이 맨 앞에 `🔥 ` 접두 → `_us_items_html`이 강조 배경 밴드(#fff7f6)로 표시**. **시장 체크의 코스피 야간선물·MSCI는 비하이브 자막 언급값으로 오버라이드** — LLM이 `kospi_futures_override`/`msci_override`(라인 문자열) 작성 시 build_html_body가 스크랩값(investing.com, 전날 것일 수 있음) 대신 사용, 미작성·불명확이면 스크랩 유지. ⚠️ @futuresnow '오늘의 요약'엔 코스피/MSCI 언급 없음(미국 전용) — 이 값 출처는 비하이브뿐. **토요일 = 해외 전용 브리핑**(2026-08-12): `build_prepare_payload`가 `now.weekday()==5`면 `overseas_only=True` → 뉴스는 `OVERSEAS_NEWS_FEEDS`('국제' 2개, 국내경제·증시 제외)만 쓰고 **일정·IPO·내일상장은 수집 자체를 생략**한다. 부수효과 둘: `gog` 3콜이 사라져 유일한 단일 실패점(`get_today_events`는 try/except가 없어 실패 시 prepare 전체 중단)을 토요일엔 회피하고, IPO를 안 읽으니 `mark_ipo_changes_seen`이 seen을 선점하지 않아 **금요일 sync 결과가 월요일 아침에 정상 노출**된다. ⚠️ `build_html_body`에서 일정 카드를 **명시적으로 스킵**해야 한다 — `events`가 빈 배열이어도 `_list_card`는 '일정 없습니다' 카드를 렌더한다. 국내 카드는 `domestic_summary` 조건부라 자동 제외되고, `_morning_missing_summary_notes`의 '비하이브 요약 없음' 각주도 `overseas_only`면 억제(의도된 부재라 노이즈). **뉴스 중복제거 3단**: ①실행 내 정규화 제목 완전일치 ②실행 내 토큰 min-overlap ≥`NEWS_DUP_THRESHOLD`(0.40) ③48h 발송이력 — ③은 오래 **완전일치뿐**이라 '같은 사안 다른 제목'이 오후에 재등장했고, 2026-08-12 `recent_history_token_sets()`로 이력 제목의 토큰 집합을 만들어 `collect_news_articles(exclude_token_sets=)`에 넘겨 ②의 fuzzy를 이력에도 적용했다(실측: 오전 발송분과 같은 사안 2건이 저녁 후보에서 걸러지고 그 자리에 새 기사가 채워짐). ⚠️ **한국어 조사·복합명사 때문에 같은 사안이 0.40 아래로 떨어지는 쌍이 남아 있다**(실측 '트럼프 골프장에 이동식 방공미사일' vs '골프장에 방공 미사일…트럼프의 암살 노이로제' = 0.29). 문턱을 낮추면 오탐이 늘어 손대지 않았다 — 더 줄이려면 토크나이저(조사 절단·문자 n-gram)를 고치는 쪽. ⚠️ `prepare`는 `already_sent`면 `briefing_pending_selection.json`을 **덮어쓰지 않는다**(2026-08-12) — 그전엔 08:30 폴백이 스킵 직전에 기사를 재선별해 발송본과 무관한 목록을 남겼고(실측 발송 7건 vs 파일 4건, 교집합 0), 이 파일이 '같은 뉴스가 또 와?' 디버깅 진입점이라 틀린 증거를 보게 됐다
- `gmail_label_classify.py` — Gmail 라벨 자동 분류 (조회 전용 Gmail API, 계정 `mini.snowoyh@gmail.com`). self-sent 메일을 제목 룰로 라벨링: `[오전/오후 브리핑]`→`브리핑`, `[비하이브 종목분석]`→`종목분석`(`주식` 라벨 제거), `[주식 리포트]`→`주식브리핑`(`주식` 제거) + 24h 지난 테스트메일 휴지통 이동. 로그 `state/gmail_label_classify.log`. launchd `ai.openclaw.gmail-label-classify` 매일 01:00(LLM 미경유, 2026-06-26 cron에서 이관). CLI: `python3 gmail_label_classify.py [--dry-run]`
- `youtube_briefing_digest.py` — 오전 브리핑용 유튜브 시황 영상 감지+자막 (두 채널 공용). `get_us_summary()`=오선의 미국 증시 라이브(@futuresnow, `UC_JJ_NhRqPKcIOj5Ko3W_3w`) '오늘의 요약'(미국장 마감 후 07:20~08:30 KST, 예정=화~토) / `get_behive_market_summary()`=비하이브 투자자문(`UCHTRF5r154igU2gXjudUMzg`) '주식시황'(장전 시황, 05:40~06:50 KST, 예정=월~금) / `get_behive_close_summary()`=같은 채널 '마감시황'(장 마감 해설, 16:45~18:10 KST, 예정=월~금, 오후 브리핑용). 공통: 채널 영상 페이지 `lockupViewModel` 스크랩(RSS 피드 IP 404 잦아 미사용)→제목필터→최신영상→watch `publishDate`(PT→KST)로 오늘게시 판정(제목 날짜는 신뢰X)→자막. CLI: `python3 youtube_briefing_digest.py {latest|transcript} [us|behive]`. ⚠️ 레이의 `behive_youtube_digest.py`(종목분석→워치리스트)와 별개 — 같은 채널이지만 목적·소유자 다름

Stock agent (`agents/stock/workspace/scripts/`), run with `python3`:
- `kiwoom_client.py` — 키움 REST API 조회 전용 클라이언트 (본인 2계좌 일반/ISA + 가희 2계좌 가희_일반/가희_ISA, 토큰 캐싱, 주문 함수 없음). CLI: `token | summary | balance | positions | quote | resolve | refresh-codes | journal`
- `investment_principal.py` — **투자원금(누적 입금−출금) 집계**. 조회 전용. `순자산 − 투자원금 = 진짜 총수익`(실현+미실현+배당) — kt00018 평가손익은 현재 보유분 미실현만이라 누적 실현손익·배당을 놓친다(실측 본인 619만·가희 232만 누락). 데이터원은 kt00016 기간내총입금·총출금(합계) + kt00015 tp='1'(입출금 행 단위). ⚠️ **둘 다 조회기간 1년 제한**(kt00016 `505221` / kt00015 `501058`)이라 연도별 창으로 쪼개 누적 — `_fetch_year`가 창당 2콜. ⚠️ **kt00015 tp='1' 입금엔 배당금·예탁금이용료(이자)·수익분배금·공모주환불금·단주매각대금·쿠폰현금지급이 섞여 있다** — 그대로 합치면 원금 과대계상. **kt00016의 총입금/총출금은 이걸 이미 제외한 순수 외부 이체액**이다(전 계좌·연도 실증). 행 단위 분류는 `is_principal_flow` = 적요에 **이체/대체/인증** 포함 여부 → 이 분류 합이 kt00016 합계와 **18개 (계좌,연도) 구간 전부 오차 0**으로 일치 확인(추측 아님). 새 적요가 등장하면 창별 `rec=False`로 드러나 모달에 경고가 뜬다. ⚠️ 응답의 `invt_bsamt`는 "투자원금**평잔**"(기간 평균잔고)이라 누적 원금이 아님 — 이름이 비슷해 오용 주의. 항등식 `순자산초 + 입금 − 출금 + 평가손익 = 순자산말`이 전 연도·계좌에서 오차 0으로 성립(자체 검증용, `show`가 출력). 캐시 `state/investment_principal.json` = `frozen`(지난 연도=확정값, 재조회 X) + `current`(올해 창, 갱신 대상) + `seen`(마지막 갱신 시점 당일 입출금 — 이벤트 감지 기준). **별도 트리거 없음** — behive_web 렌더 경로가 이미 조회 중인 kt00015 당일 입출금이 `seen`과 다를 때만 올해 창 재조회(계좌당 1콜), 입출금 없는 평시엔 kt00016 콜 0. 안전망으로 7일 초과 시 강제 갱신. 캐시 없으면 `needs_refresh`가 False·`get_principal`이 None → 페이지를 68콜로 막지 않고 KPI 2행만 조용히 생략. `bootstrap`은 FLOOR_YEAR(2010)~올해 전수 조회(4계좌×17년=68콜 ≈25초, 조기종료 없음 — 휴면 연도가 끼어도 앞 연도 안 놓침) **1회성 CLI 전용, 렌더 경로 호출 금지**. 주식 입출고(타사대체 `termin_tot_inq/outq`)가 있으면 현금 기준 원금이 과소계상 → `qty_flow` 플래그로 KPI에 ⚠️ 표시(현재 전 계좌 0). CLI: `python3 scripts/investment_principal.py {bootstrap|refresh|show}`
- `fnguide_client.py` — FnGuide 컴퍼니가이드 펀더멘털 조회 (조회 전용, 키움에 없는 데이터 보강). `Snapshot_all/{code}.xml`(EUC-KR, JS 미경유 직접 파싱) 1콜 → 연간 재무 시계열·매출/EPS/영업이익 증가율·컨센서스(목표주가·투자의견·추정EPS/PER·참여기관수). `get_fundamentals(code)`, `state/fnguide_cache/{code}.json` 12h 캐시, 실패·ETF는 None (절대 raise X). ⚠️ FnGuide 저작권 회색지대 → 보유·관심 종목 on-demand만. ⚠️ 현재 피드는 forward 추정 EPS가 trailing 대비 크게 높게 나옴(예 하이닉스 2025 58,955→2026E 297,725) — 추정 PER이 현재 PER보다 훨씬 낮은 건 이익 급증 기대 반영이지 버그 아님. CLI: `python3 fnguide_client.py <code> [--fresh]`
- `wisereport_client.py` — WISEreport(comp.wisereport.co.kr, FnGuide 계열 동일 벤더) 컨센서스·증권사 리포트 조회 (순수 JSON, encparam 불필요). `get_consensus(code)`: 연도별 추정 재무(IFRS연결 A실적/E추정, `c1050001_data.aspx flag=2`)·목표주가+추정EPS 3개월 리비전 추이(`cF5001`)·어닝 서프라이즈(`flag=5` 매출/영익 실적 vs 직전 컨센서스 괴리율). `get_reports(code)`: 최근 증권사 분석리포트(`c1080001_data.aspx` — 날짜·증권사·제목·목표가+상향/하향 액션·투자의견·애널리스트·요약 bullet, PDF 원문은 게이팅·저작권으로 제외). `state/wisereport_cache/` 캐시(컨센서스 12h·리포트 6h), 실패·ETF는 None. 동일 벤더 회색지대. CLI: `<code> [--fresh] [--reports]`
- `stock_analysis.py` — 종목 분석 보고서 엔진 (behive_web `/stock/<code>`가 import, CLI 없음). 키움 기본정보·일봉·수급 + LLM 코멘트 + SVG 차트 + 투자의견 게이지 → 종목별 HTML 보고서(`state/stock_reports/<code>/`). FnGuide 성장성·컨센서스 + WISEreport 추정·리비전·서프라이즈 섹션 포함(LLM 데이터블록에도 주입). 레이아웃: 결론(투자의견) 최상단 + 보조 섹션 2단 그리드(`.rpt-cols`), 지표마다 평이한 캡션(`.rpt-cap`) + ⓘ 탭 설명(`_KV_HINTS`/`_lbl`). `enqueue` / `render_stock_page` / `add_peer`. 새 섹션은 신규 생성 보고서부터 반영(기존 저장본은 옛 구조).
- `stock_portfolio_report.py` — Daily portfolio report. 키움 `kt00018`(보유)·`kt00001`(예수금)·`ka10170`(당일매매일지) ground truth. `--by-account`로 계좌 분리 뷰. 당일정산(round-trip·풀매도)은 잔고에 없어 별도 [당일정산] 카드로 표시. ⚠️ **메일 발송 중지 — 2026-08-12 관리자님 결정**(메일 통수 축소). launchd는 `send --no-mail`로 돌아 **스냅샷만** 남긴다(`state/portfolio_daily_snapshot.json` = 자산웹 '당일 평가손익'·`rebase_to_nxt_close` 전일 baseline의 근거라 계속 필요). `--no-mail`은 `send` 경로의 휴장일 self-skip·실패 자가알림을 유지하고 `send_mail` 호출만 건너뛴다(`MAIL_SKIPPED=no_mail_flag`) — **`run`으로 대체 금지**(휴장일 가드가 `send` 분기에만 있어 휴장일 키가 이력에 섞이고 평문 100여 줄이 로그로 쏟아진다). 리포트 본문을 눈으로 볼 땐 수동 `run`(`/tmp/stock_report_preview.html`). ⚠️ 텔레그램 요약 코드(`tg_text`, ~104줄)는 2026-05-14 비활성 이후 **완전한 데드코드**가 됐다 — 되살릴 땐 `main`의 발송 분기만 고치면 되고, 정리할 땐 관리자님 확인 후
- `ipo_calendar_sync.py` — Sync IPO subscription/listing dates to Google Calendar
- `fomc_calendar_sync.py` — 미 연준 FOMC 회의 일정 → Google Calendar (`ipo_calendar_sync.py` 구조 복제, 조회+등록 전용). 출처 `federalreserve.gov/monetarypolicy/fomccalendars.htm`(인증 불필요) — `fomc-meeting__month`/`fomc-meeting__date` div 파싱, 날짜 끝 `*`=SEP(경제전망·점도표) 회의. 제목 `[FOMC] YYYY-MM 회의`(+` (SEP)`)는 **2026-03-23 관리자님이 일괄 등록한 기존 표기를 그대로 승계** — 덕분에 기존 항목이 중복 없이 흡수됨. `state_key`=시작월 `YYYY-MM`(FOMC는 한 달에 두 번 안 열려 고유). **이벤트 기간 = 회의 시작일(ET) ~ 발표일(KST)**로 3일짜리 종일 이벤트(`cal_start`/`cal_end`). 성명 발표는 회의 마지막날 `America/New_York` 14:00 = KST 익일 03:00(EDT)·04:00(EST)이라, ET 회의날짜만 넣으면 **정작 한국 장이 결과를 반영하는 날에 캘린더가 비어 브리핑 '오늘 일정'에도 안 뜬다** — 그래서 발표일까지 덮는다(2026-07-29 관리자님 지적으로 수정, 그전엔 2일짜리였음). 제목·description은 ET 회의날짜 기준 유지(뉴스·시황 표기와 일치). ⚠️ `parse_meetings`의 미래 판정 기준도 `start_date`가 아니라 **`cal_end > today`** — ET 회의가 끝났어도 한국 발표일이 남았으면 갱신 대상이다. ⚠️ `--event-color` **미지정** — 기존 colorId 9(블루베리) 보존. 안전장치: 스크래핑 실패로 목록이 비면 cleanup 전체 스킵 / 조회·삭제 범위는 `오늘 이후`로 한정 + **삭제 루프가 `old_start <= today`를 건너뜀**(진행 중이거나 지난 회의는 파싱 대상에서 빠질 뿐인데 stale로 오인돼 지워지는 걸 막음). CLI: `--list`(파싱 결과만, gog 미호출 — 인증 없는 환경에서 파서 검증용) / `--dry-run` / 무인자=실반영. 캐시 `state/fomc_calendar_sync.json`
- `holiday_sync.py` — investing.com에서 한국(KRX) 휴장일 fetch → `state/market_holidays.json`. behive_web.py 자동갱신 토글이 휴장일·평일·시간대로 비활성 판정. CLI: `python3 holiday_sync.py [--show]`
- `behive_youtube_digest.py` — 비하이브 YouTube 종목분석 수집·요약·발송 + 수동 워치리스트 추가(`add`)/조회·삭제
- `watchlist_monitor.py` — 워치리스트 종목 장중 15분 시세 감시, buy/target/stop 트리거 발생 시 레이 텔레그램 알림 (LLM 없이 동작). 미보유 종목은 ka10095 batch 1콜 + 단건 ka10001 fallback. 보유 종목은 kt00018 재활용
- `surge_monitor.py` — **급등락 알림** (2026-08-04). 토글 켜진 종목만 감시(`state/behive_surge_toggles.json`, behive_web 🔔 버튼이 씀). **세 기준 병행**: ①거래소 **상·하한가 도달**(2026-08-06 추가) = 그날 갈 수 있는 끝까지 간 것이라 가장 강한 신호, 무엇에도 가리지 않는다 ②거래소 **VI 발동** = "이건 급등락이다"를 거래소가 공식 판정하므로 문턱을 우리가 안 정해도 된다 ③**자기 이력 분위수** = 장중 이탈폭(전일종가 대비) ≥ 그 종목 과거 이탈폭의 `SURGE_PCTL`(p90) 분위수, 확대단계는 `SURGE_PCTL_BIG`(p98). ⚠️ **고정 %는 구조적으로 불가** — 실측(관심·감시 65종목 × 280거래일) 장중 이탈폭 중간값이 3.8%라 ±3%는 평범한 날이고, 종목별 변동성이 22배 차(ATR14 1.0~22.3%)라 ±5% 문턱이면 KODEX 미국S&P500은 발생 0회·SK이터닉스는 75회다. ⚠️ **ATR 배수(ATR%×K)도 쓰면 안 된다 — 2026-08-04 관리자님 지적으로 폐기**: 국내 하루 가격제한폭이 **±30%**인데 ATR 배수는 상한이 없어 변동성 큰 종목의 문턱이 제한폭 밖으로 밀려난다. 실측 1차(ATR×1.5) 5/65종목·확대(ATR×3.0) **31/65(48%)**가 30% 초과 = **영원히 발동 불가**였다(ATR% 중간값 10%라 확대는 절반이 죽음). 분위수는 실제 관측된 이탈폭이라 구조적으로 제한폭을 넘을 수 없고(p90~p99 전부 30% 초과 0종목), 알림량도 정의상 (1−p)로 확정된다 — p90 = 종목당 연 25회. **다시 ATR 배수로 되돌리지 말 것.** 데이터원은 behive_web `/api/realtime/quotes` 1콜 — 현재가와 VI를 한 번에 주고 **키움 콜 0**. VI(1h)는 시장 전역 broadcast라 구독 불필요, 현재가(0B)는 구독 필요한데 `_rt_gather_codes()`가 이미 보유+관심+감시를 구독하므로 토글 종목은 부분집합(상한에 걸려 빠진 종목만 ka10095 배치 1콜 폴백). ⚠️ **문턱은 하루 1회만 계산**(`state/surge_thresholds.json`, `pctl` 서명 불일치 시 재계산) — `daily_candles_cache.get_candles`는 캐시 최신봉이 어제보다 오래되면 ka10081을 때리므로 매분 × 종목수로 부르면 폭주. ⚠️ **ka10081 유량이 초당 5건**이라 한꺼번에 돌리면 429가 쏟아진다(실측 70종목 일괄 산출 시 26종목 실패) → 사이클당 `MAX_COMPUTE_PER_CYCLE`(12)개까지 `COMPUTE_PACE_SEC`(0.35s) 간격으로만 계산, 나머지는 다음 사이클이 이어받는다(70종목 = 6사이클 ≈ 6분, 사이클당 4~5초 실측). ⚠️ **조회 실패는 캐시하지 않는다** — 이력부족(`{}`)과 같이 취급하면 429 한 번에 그 종목이 하루 내내 조용히 VI만 감시하게 된다. `_compute_thresholds`가 `(결과, 재시도여부)` 튜플을 반환하는 이유. ⚠️ **판정은 `prev_close`와 무관**하다(`pct` % vs 문턱 % 비교) — 캐시가 하루 stale이면 `list`의 발동가 표시만 어긋나고 트리거는 정확하다. ⚠️ **상·하한가·VI 가 걸린 동안 분위수 트리거는 생략이 아니라 보류**다(2026-08-06 관리자님 요청으로 변경). 같은 사건을 두 번 알리지 않으면서도, 그 사이 문턱을 넘은 사실은 `surge_alerts.json`의 `__pending__`(종목→키→`{pct,word,reason,via}`, pct는 이탈폭 최대 시점)에 적어뒀다가 **가림이 풀린 사이클에 방출**한다. 그전엔 조용히 버려져서, **VI 중 문턱 돌파 후 해제 시점에 되밀린 움직임은 알림이 아예 없었다**. ⚠️ **가림 여부를 "보류할 트리거가 비었는지"로 판단하면 안 된다** — VI 중 주가가 문턱 아래로 되밀리면 그것도 비어서, VI 도중에 보류분이 새어나간다. `evaluate`가 세 번째 반환값으로 **가림사유 문자열**을 따로 주는 이유. ⚠️ **방출분은 발송 성공 시에만 보류에서 지운다**(텔레그램 실패 시 재시도 대상). ⚠️ **알림 아이콘은 pct 가 아니라 방향어에서 파생**한다 — 방출 건은 돌파 시점 방향(급락)과 방출 시점 현재가 부호(+)가 어긋날 수 있어 pct 로 고르면 `🚀 급락`이 찍힌다. ⚠️ **VI 가 장 마감까지 안 풀리면 그날 보류분은 방출되지 않는다**(강제 flush 없음). ⚠️ **상·하한가는 거래소 계산값(ka10095 `upl_pric`/`lst_pric`)을 그대로 쓴다** — 제한폭은 기준가의 호가단위로 절사한 뒤 ±하는 규칙이라 직접 계산하면 ETF·가격대별 호가단위 표를 재현해야 해 틀릴 여지가 있다(VI와 같은 이유). 기준가로 정해져 장중 불변이라 `state/surge_limits.json`에 **하루 1콜**만 캐시하고, 일봉 이력이 필요 없어 **이력 부족 종목도 이 기준으론 감시된다**. ⚠️ **VI 레그는 behive_web 프로세스에 의존** — 죽으면 VI 알림이 조용히 사라지고 stderr 경고만 남는다(자동 재시도·백업 트리거는 의도적으로 없음). 이력 `MIN_HISTORY`(60일) 미달 종목은 VI·상하한가만 감시(신규상장·정리매매는 제한폭 예외지만 애초에 이력이 없어 여기 해당). dedup은 `state/surge_alerts.json`에 날짜·종목별 트리거키 1회씩 — 분위수 방향별 1회 + 확대단계 1회 + `limit_up`/`limit_down` 1회, VI 는 `vi:<발동시각>`이라 **발동 건마다** 1회(하루 상한 없음). 감시 대상은 **코드 기준**(메모와 달리 종목명 fallback 없음 — 시세 조회에 코드가 필요). CLI: `check [--force]` / `dry-run`(보류 현황 표시) / `list`(문턱%·급등가·급락가·상하한가 표시)
- `trailing_monitor.py` — **트레일링 스톱 감시** (2026-07-30). 키움 REST에 트레일링 주문 TR이 없어서, 스톱지정가(`trde_tp=28`) 주문을 실제로 걸어두고 고점 갱신 시 `kt10002` 정정(`mdfy_cond_uv`)으로 조건단가를 올려 구현. **주문이 키움 서버에 있어 감시 루프가 죽어도 마지막 손절선은 살아있다** (자체 폴링 발주 방식은 루프가 죽으면 손절이 통째로 사라짐 — 이 차이가 설계 선택 이유). **예약 1건 = 계단(레그) N개**라 생존 확인·정정 모두 레그 단위로 돈다. 사이클당 하는 일: ①`ka10075` 미체결 조회로 **레그별** 생존 확인(계좌당 1콜). 사라진 레그만 정리하고 나머지 계단은 감시 유지 + **사유 구분 알림** — 미체결 조회만으로는 체결·취소·소멸이 구분되지 않아(셋 다 목록에서 빠짐) 그때만 `kt00007` 1콜 추가로 판정: ✅체결(그 `ord_no`의 `cntr_qty`>0) / ⚠️소멸(체결수량 0 = 체결 안 됨 확정, 재등록 안내) / 🎯판정불가(조회 실패). ⚠️ **판정은 반드시 주문번호 단위 `kt00007`로** — 종목 단위 집계인 `ka10170`을 쓰면 **한 계단이 체결된 날 나머지 계단이 장 마감으로 소멸했을 때 그 소멸분까지 '체결'로 오판**한다(같은 종목 매도기록이 이미 남아 있어서). 2026-07-31 계단식 전환 때 교체. ⚠️ **조회 실패를 '체결 안 됨'으로 단정 금지** — `_leg_fill_check` 가 (기록, 성공여부) 튜플을 반환하는 이유. 알림은 **예약 단위로 묶어 1건**(장 마감이면 계단이 통째로 사라져 레그마다 보내면 3~5통이 몰아친다) ②`ka10095` 시세 1콜 → 고점 갱신 시 살아있는 레그 **전부** 정정(정정 콜이 계단 수에 비례). 주가가 그대로·하락이면 API 콜 0. ⚠️ **`kt10002` 응답 `ord_no`는 신규 주문번호** — `trailing.commit_step_modify`가 상태파일의 **레그별** `ord_no`를 갱신하지 않으면 그 레그의 두 번째 정정부터 `orig_ord_no`가 틀려 전부 실패(최대 함정). ⚠️ **한 계단이 체결돼도 남은 계단을 재배치하지 않는다**(2026-07-31 관리자님 결정) — 남은 수량을 계단 비율대로 다시 쪼개려면 기존 주문 취소+신규 발주가 필요해 감시 루프에 자동 발주 경로가 생긴다. 남은 계단은 새 고점 기준으로 따라 올라가기만 한다. ⚠️ **고점은 등록 시점 현재가에서 시작해 현재가로만 갱신**한다 — `ka10095`가 당일고가(`high`)도 주지만 등록 전에 찍힌 고가까지 반영되면 손절선이 현재가 위로 올라가 즉시 발동(예: 오전 15,000→14,000일 때 3% 등록 시 당일고가 기준 조건 14,550 > 현재가). 같은 이유로 초기 고점 선택 옵션(52주 전고점·매수후 고점)도 제거됨. 1분 간격이라 그 사이 스파이크는 놓친다(손절선이 덜 올라가 이익 확정 폭이 조금 줄어드는 방향 — 손실 위험은 아님). ⚠️ **정밀도 보완은 하지 않기로 결론**(2026-07-30 관리자님 판단): 최저 매도가로 하한이 통제되므로 1분 해상도의 고점 누락은 감당 범위. 검토했던 두 안 — `entry_day_high`(등록 시점 당일고가를 기준선으로 저장해 그보다 큰 `high`만 등록 이후 신고가로 인정. 추가 콜 0이지만 기준선 아래 스파이크는 여전히 누락) / `realtime_hub` WS 틱 고점 추적(정확하지만 구독·재연결 폴백 필요) — **둘 다 채택 안 함. 다시 제안하지 말 것.** ⚠️ **`fill_watcher`를 걸지 않는다** — 스톱 예약은 장중 내내 미체결이 정상이라 30분 미체결 알림이 오탐. 생존·체결 감시는 이 스크립트가 직접. ⚠️ **스톱주문은 장 마감 후 소멸한다(2026-07-30 실증)** — 첫 실주문이 체결 없이 사라짐(당일 매도기록 0·보유 불변), 감지 18:21로 15:30 즉시가 아니라 몇 시간 뒤. **자동 재등록은 하지 않는다**(관리자님 결정) — 알림만 보내고 재등록은 자산웹에서 수동. 정정 수량은 저장 qty가 아닌 **미체결 잔량**(부분체결 대응). 미체결 조회 실패 계좌는 그 사이클 판단 보류(없다고 단정하면 살아있는 예약을 지움). CLI: `check [--repeat N --gap S] [--force] [--dry-run]` / `list`
- `orders/trailing.py` — 트레일링 예약 상태(`state/trailing_stops.json`) + 손절선 계산. **예약 1건 = 계단(레그) N개 = 키움 스톱주문 N건**(계단식 분할 매도, 2026-07-31). 하락률이 깊어질수록 더 많이 판다 — 예 −10% 20% / −20% 50% / −30% 전량. `compute_levels(peak, pct, min_sell_price=None)` = `cond_uv` = **max(peak×(1−pct/100), min_sell_price)** 호가단위 내림 + `ord_uv`(조건단가 −`ORD_UV_GAP_TICKS` 2틱 — 조건단가와 같게 두면 발동 후 그 가격 아래로 안 팔려 미체결로 남고, 너무 벌리면 급락 시 헐값 매도). ⚠️ `int(round(peak×(1−pct/100), 6))` 의 **round 는 부동소수점 잡음 제거용**(5500×0.7이 3849.9999999999995라 int()가 3849로 깎고 호가내림이 3845까지 끌어내리던 버그, 2026-07-31 수정). 계단 함수 4종: `normalize_steps(raw)`=입력한 **누적** 비중(20/50/100)을 **추가** 비중(20/30/50)으로 환산+검증(하락률·누적 둘 다 순증가, 하락률 0.5~30%, 누적 ≤100%〔100 미만 허용=일부만 계단 청산〕, 계단 ≤`MAX_STEPS` 5) / `allocate_step_qty(total, weights)`=**최대잔여법**(내림 후 소수부 큰 계단부터 1주씩 — "내림 후 마지막에 몰아주기"보다 얕은 계단이 0주로 죽는 일이 적다: 2주·20/30/50 → `[0,1,1]` vs `[0,0,2]`) / `compute_step_levels(peak, steps, floor)` / `next_step_levels(res, cur)`. **최저 매도가**는 손절선의 하한 — 트레일 폭을 넓게 잡아도 이 가격 아래로 안 내려간다(초기 구간 손실 제한). ⚠️ **floor 는 모든 계단에 같은 하한으로 걸려** 초기엔 여러 계단이 같은 가격으로 뭉칠 수 있다. **그래도 주문을 병합하지 않는다** — 고점이 올라 트레일이 floor를 추월하면 각 계단이 제 하락률대로 다시 벌어지는데, 병합하면 정정만으로는 못 쪼갠다(신규 발주가 필요해짐). 전환점 = `min_sell_price ÷ (1−pct/100)`, 예: 최저 4,500·20% → 5,625원. `next_step_levels(res, cur)`는 **상향 전용 순수함수** — 고점 안 올랐으면 None, 올랐으면 `{peak, steps:[올릴 레그만]}`. ⚠️ **steps 가 빈 리스트일 수 있다**(호가내림·floor 지배로 올릴 조건단가가 없는 경우) → 호출측은 정정 API 없이 `commit_peak`로 고점만 갱신(콜 0, 화면 고점 표시는 정확 유지). 상태 갱신은 레그 단위: `register_steps`/`commit_step_modify`/`remove_step`(마지막 레그 제거 시 예약째 삭제)/`commit_peak`. 같은 계좌·종목 중복 예약은 `find_by_symbol`로 propose 단계에서 차단. fcntl lock + atomic write (`pin.py` 패턴)
- `behive_web.py` — 워치리스트 실시간 웹 뷰 + 매매 진입점. `serve`(launchd, Tailscale IP 100.75.148.12:18790 바인드, 페이지 GET마다 키움 ka10095 batch 1콜로 워치리스트 시세 + kt00018·kt00001·ka10170 병렬 호출 후 HTML 응답. RENDER 캐시 10s, 종목별 quote 캐시 30s) / `render`(디버깅용 1회 렌더). 외부 노출은 NAS Synology reverse proxy(`stock.hyowons.net` → mac:18790) 경유. 인증 없음 — Tailnet 내부망 한정 운영. **빌드 버전 자동 리로드**(2026-07-03): 이 앱은 `location.reload` 없이 fetch로만 갱신해 코드 수정+재시작 후에도 열려있던 페이지는 옛 인라인 JS/CSS를 계속 씀. `BUILD`(파일 mtime, import 시 1회) 상수를 shell `window.__build`와 `/api/panels` 응답 `build`에 실어, `apply()`가 불일치(옛 페이지=`__build` 없음 포함) 감지 시 `sessionStorage` 가드로 build당 1회 자동 리로드 → 최신 JS/CSS 반영(리로드로도 안 맞으면 루프 없이 포기). 보유종목 day_change 보정은 brifing 과 동일 A-4 정책. **KPI 라벨 규칙**(2026-07-29): **'총'은 계좌 전체(현금 포함) 기준일 때만 쓴다** — `총자산`(=평가금액+예수금)·`총수익`(=총자산−투자원금)만 '총'을 갖고, 보유종목 한정인 `평가금액`·`매입금액`·`평가손익`은 안 붙인다. 이름만으로 관계식이 읽히게 한 정리(구 라벨: 총 평가금액→평가금액, 순자산→**총자산**, 총 매입금액→매입금액, 총 평가손익→평가손익, `총수익 (원금 대비)`→총수익). 헷갈리던 세 쌍을 분리한 결과 — ①평가금액 vs 총자산(차이=예수금) ②매입금액 vs 투자원금(전자는 현 보유분 매입원가, 후자는 실제 입금액) ③평가손익 vs 총수익(전자는 보유분 미실현만, 후자는 실현·배당 포함). `_render_owner_kpi`·`_render_account_kpi`·**`stock_portfolio_report`(HTML·평문·텔레그램)**·순자산 차트 표시문(`NET_WORTH_MODE_PRESETS`·차트 제목·aria·툴팁 행) 전부 통일. ⚠️ 평문 리포트 라벨은 **표시폭 17칸 정렬**(한글 2칸) — 라벨 바꿀 때 공백 수 재계산 필요. ⚠️ `_rtk` 실시간 매핑 키가 라벨 문자열이라 라벨 변경 시 함께 고쳐야 한다. ⚠️ PBR 설명문의 '회사 순자산'은 뜻이 다르니 건드리지 않는다. ⚠️ `render`는 **셸 HTML만** 생성(데이터 fetch 없음) — 패널 내용 검증은 `_build_panels_payload()` 직접 호출이나 `/api/panels` curl로 해야 한다. **KPI 행 순서**(2026-07-29, 관리자님 지정): 합산 owner 카드(`_render_owner_kpi`)는 **구분선·그룹 없는 단일 목록** — `총자산 · **투자원금** · 매입금액 · 평가금액 · 평가손익 · 예수금 · [당일 입출금] · 보유종목/당일매매 · 당일 평가손익` + 조건부 `당일 실현손익`(non-compact) + 맨 아래 `총수익`. `당일 입출금`은 예수금 바로 아래(현금 잔액↔그날 현금 이동)이고 입출금 없는 날은 행 자체가 없다. 투자원금은 총자산 바로 아래(전체 자산↔넣은 돈 대비), 총수익은 맨 아래 결론. 원금 미집계(부트스트랩 전)면 **두 행 모두** 사라진다. 보유종목·당일매매는 **한 행 결합**(`3개 / 2개`) — compact에도 당일매매가 함께 보인다. 평문 리포트에서 `보유종목/당일매매`는 표시폭이 정확히 17칸이라 패딩 없이 콜론이 맞는다. 같은 순서·라벨을 `_render_account_kpi`와 `stock_portfolio_report`(HTML 소유자별·전체합계, 평문)에도 적용. ⚠️ 2026-07-29 중 4그룹(자산/보유/누적/당일) 구분선 방식을 거쳤다가 단일 목록으로 되돌림 — `.kpi-gtop` CSS는 제거됨. `_rtk` 실시간 매핑(value/profit/net/daypl/totret)은 라벨 기준이라 그룹화와 무관하게 동작. 계좌별 pane(`_render_account_kpi`)은 평면 유지(관리자님 선택). ⚠️ 그룹화로 stock_portfolio_report 메일의 평면 순서와는 배치가 갈렸다(라벨은 동일). **투자원금·총수익 KPI**(2026-07-29): `누적 성과` 그룹 2행 — `투자원금`(값은 금액만, **라벨 옆 작은 원형 `[!]` 버튼**) / `총수익`. 버튼을 값(td)이 아니라 라벨(th)에 두는 이유 — 금액 뒤에 붙이면 숫자 우측정렬이 흐트러진다. `kpis` 튜플의 **3번째 원소**가 라벨 뒤 HTML(escape 대상 아님)로 렌더된다. **KPI 상세 팝업은 공용 1개**(`kpi-modal` + `openKpiModal(src,title)`): 라벨 옆 `.kpi-more-btn`이 `data-kpi-src`(엔드포인트)·`data-kpi-title`을 들고 있고, 위임 핸들러가 그걸 읽어 fetch → 본문 innerHTML. 버튼 생성은 `_kpi_more_button(src, title)`. 본문은 **서버가 HTML로 조립**하므로 새 [!] 버튼을 늘릴 때 JS·모달 추가가 필요 없다 — 렌더 함수 + 엔드포인트 분기만 더하면 된다. 현재 4개: `/api/surge?code=&name=`(`_render_surge_body` — 급등락 알림 구간·오늘 소진·on/off 토글, 아래 참조) / `/api/deposit?owner=` / `/api/principal?owner=`(`_render_principal_body` — 투자원금·누적 입금/출금·계좌별 + 입출금 내역 233행) / `/api/cashflow?owner=`(`_render_cashflow_body` — 당일 입금/출금 합계 + 건별 내역, kt00015 계좌당 1콜 재조회). 두 팝업 모두 `.pm-*` CSS 공유. ⚠️ 순액과 합계가 같아지는 중복 표시 금지 — 당일 입출금은 **한쪽 방향뿐이면 `순 입출금` 행을 생략**한다(`+3,000,000원 · 입금 3,000,000원` 같은 군더더기가 KPI 행·메일·텔레그램 4곳에 있었음, 2026-07-29 제거). 모달 셸은 shell HTML 직속이라 panels swap 영향 없음. 내역은 `ip.get_flows` 캐시 읽기라 **키움 콜 0**. 내역 233행 중 대부분이 배당·이자라 실제 입금이 묻히므로 **기본은 원금 반영분만 보이고**(본인 49/140·가희 15/93) 체크박스로 나머지를 펼친다 — `.pm-all-cb:not(:checked) ~ .pm-list .pm-row.other{display:none}` **CSS-only, JS 없음**. 개념 설명문은 넣지 않는다(관리자님 지시). ⚠️ 계좌 간 대체(내 일반↔ISA)가 있으면 owner 합계는 상쇄되지만 **계좌별 금액은 왜곡**되므로 안내문(`.pm-note-in`)을 띄운다(실측 본인 6,000,000·가희 1,000,000 각각 상쇄). `순자산 − 원금` 관계가 눈으로 읽히게 붙여 놓고, 위쪽 `총 평가손익`(보유분 미실현)과 혼동되지 않게 라벨에 "원금 대비"를 명시. 데이터는 `investment_principal.get_principal(lbls)`(캐시 읽기, 콜 0) → `_build_owner_data`가 `principal`·`principal_cash_in/out`·`principal_qty_flow`로 주입, None이면 2행 생략. 순자산이 WS 틱마다 움직이므로 총수익도 같이 갱신 — 카드 `data-rt-principal` + td `data-rt-kpi="totret"`를 `setOwnerKpis`가 읽어 `T.net − principal` 재계산(`updateScopes`/`T` 구조 무변경). 계좌별 pane(`_render_account_kpi`)은 미적용. 보유종목 행마다 `📋 거래내역` + `💰 거래` + `📝 메모` 버튼. 자산정보 탭 sub-tab 3개 (자산보기·차트보기·시장정보) + 우측 별도 `[💰 거래]` 버튼 (본인 첫 보유종목 자동 선택). 관심·감시종목 행에도 `💰 거래` (보유 모드면 매도 default).

  **거래 모달 시스템 (`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=하나라도 접수).

  **매도 주문유형 4종** — 지정가(LIMIT) / 시장가(MARKET) / 스톱지정가(STOP_LIMIT, 하락 시 매도·조건단가 고정) / **트레일링 스톱(TRAILING_STOP, 2026-07-30 추가)**. 뒤 2개는 매도 전용(매수 선택 시 옵션 disabled + LIMIT로 되돌림, `refreshStopUI`). **트레일링 입력**(단가 입력행은 숨김 — 조건단가·지정가·계단별 주수는 서버 계산): ①**계단 프리셋 드롭다운** — 코드 상수 `TRAIL_PRESETS` **고정 4종, 읽기 전용**. 표시 순서 = **단일 / 보수 / 표준 / 느슨**(기본값은 맨 앞 `단일`, 그 뒤로 트레일 폭이 좁은 것부터). 옵션은 **이름만** — 계단 수치는 바로 아래 편집 표에 그대로 보여 중복이다(2026-07-31 관리자님 지시, 다시 붙이지 말 것). 셸 HTML에 `window.__trailPresets`로 실려 모달 열 때 fetch 0. ⚠️ **프리셋 저장·삭제 기능은 두지 않는다**(2026-07-31 관리자님 지시로 제거 — 고른 뒤 아래 표에서 그 자리에 고쳐 쓰면 되므로 저장소·락·엔드포인트가 불필요했다. `state/trailing_presets.json`도 폐기). 프리셋을 늘리려면 상수를 고친다 ②그 아래 **계단 편집 테이블**(`하락/누적` **헤더 한 줄** + 행마다 `[__]% [__]%` + ✕, 아래 full-width `+ 계단 추가`, 최대 5) ③**최저 매도가**(선택, 비우면 제한 없음). ⚠️ **라벨을 행마다 반복하지 말 것**(2026-07-31 정리) — `.order-inputs` 실폭이 모바일 ~200px라 행마다 '하락'·'누적' 글자와 화살표를 넣으면 입력칸이 38px(실텍스트 26px)까지 눌린다. 헤더로 빼고 화살표를 지워 78px로 회복했고, number 스피너도 `-webkit-appearance:none`으로 없앴다(데스크톱에서 숫자를 가림). ⚠️ 헤더 행도 `.trail-step` 클래스라 **`trailSteps()`는 `:not(.trail-step-head)`로 걸러야 한다** — 안 그러면 입력칸 없는 행을 읽어 TypeError. ⚠️ 계단 0개면 헤더도 그리지 않는다. 단위(`(%)`·`(원)`)는 **라벨에 박는다** — 입력칸 옆에 붙이면 가로로 벌어져 스크롤이 생기고 number input 안쪽 absolute 접미사는 데스크톱 스피너와 겹친다. **누적 비중으로 입력받는 이유** — 관리자님이 그렇게 생각한다("30% 빠지면 전량"). 주문 수량인 추가분 환산은 `normalize_steps`가 한다. `handler.propose_trade(trail_steps, min_sell_price)`가 초기 고점을 `md['current_price']`로 정하고 `compute_step_levels`+`allocate_step_qty`로 계단별 가격·주수를 만든다. **0주가 된 계단은 떨어내되 단계 번호(n)는 유지**해 카드에서 몇 단계가 빠졌는지 보이게 한다. ⚠️ 트레일링에 `budget`(예산 환산) 금지 — 계단 주수는 보유수량을 나눠 만드는데 예산이 끼면 기준이 둘이 된다(`TRAIL_BUDGET`으로 거부). ⚠️ **고점 기준 선택 옵션은 2026-07-30 제거됨**(`peak_basis` / `52주 전고점` / `매수후 고점`) — 과거 고점을 기준으로 잡으면 손절선이 현재가 위로 올라가 등록 즉시 발동한다. **손실 종목은 등록 자체가 불가**(실측 제닉스로보틱스 현재가 4,805 · 매수후 고점 6,890 → 필요 폭 30.3% / 52주 19,940 → 75.9%, 둘 다 상한 30% 초과), **이익 종목은 과거 고점 ≈ 현재가라 결과가 같다**. 즉 차이가 날 때는 못 쓰고 쓸 수 있을 때는 차이가 없어 옵션을 없앴다. **다시 넣지 말 것.** ⚠️ **1단계(가장 얕은 계단) 손절선 ≥ 현재가면 `TRAIL_IMMEDIATE`로 거부** — 고점=현재가 고정이라 이제 최저 매도가가 현재가보다 높은 경우만 남는다. **거부 시점은 입력 시점**(2026-07-30 관리자님 지시로 propose 시점에서 이동) — `trailBlockReason()`이 입력값을 보고 사유 문자열을 내놓고, 걸리면 매도 버튼이 disable + title 에 사유. 계단 순서·범위 위반(`TRAIL_STEPS`)과 "이 수량으론 계단을 못 나눔"(`TRAIL_QTY`)도 같은 함수가 같은 문구로 먼저 막는다. 서버는 최후 방어선으로 유지. ⚠️ **버튼 `disabled`는 `refreshSubmitGate()` 한 곳에서만 결정한다** — 시장 phase 게이트(`updateMarketPhaseDisplay`, `/api/market_state` 주기 폴링)와 트레일 게이트가 같은 버튼을 건드려서, 예전처럼 각자 대입하면 주기 호출이 트레일 게이트를 덮어써 즉시발동 입력이 통과된다. phase 쪽은 `state.phaseCanTrade`/`phaseLabel`만 쓰고 판단을 위임. propose 진행 중에는 `btn.dataset.busy='1'`로 잠가 주기 호출이 버튼을 되살리지 못하게 하고, 응답·에러 후 busy 해제 + 재계산. `renderTrailPreview`는 조기 반환 경로(폭 미입력·시세 로딩)에서도 게이트가 걸려야 하므로 **함수 맨 앞에서** `refreshSubmitGate()`를 부른다. 클라이언트 미리보기(`renderTrailPreview`/`trailCondPrice`/`allocStepQty`/`trailWeights`/`floorTick`)는 서버와 **같은 식**이지만 실제 발주값은 서버 계산분이 승자 — 한쪽만 바꾸면 표시와 발주가 어긋나니 `orders/trailing.py` 수정 시 JS도 함께 고칠 것(**렌더된 페이지에서 함수 소스를 뽑아 node로 실행해 Python과 대조**하는 방식, 현재 418조합: 손절선 360·수량배분 54·비중환산 4). 키움엔 계단마다 스톱지정가로 나가고(`_submit_trailing_steps`가 레그별 `STOP_LIMIT` 제출) 트레일링은 우리 쪽 개념. **카드 1장·PIN 1개로 계단 N건을 승인**한다. **레그 독립 제출** — 얕은 계단부터 접수해 중간에 막혀도 가장 가까운 방어선이 먼저 걸리고, 한 레그가 실패해도 나머지는 시도하며 접수된 것만 `trailing.register_steps`로 등록한다(이미 나간 주문을 되돌리지 않는 이유는 취소도 실패할 수 있어 상태가 더 불분명해져서 — 대신 몇 단이 걸리고 몇 단이 실패했는지 메시지에 그대로 적는다). **등록 실패 시 주문은 이미 접수된 상태**라 메시지에 ⚠️ 경고를 붙이고 ledger `trailing_register_failed` 기록(숨기면 트레일링이 안 도는 걸 모른다). 트레일링·스톱지정가는 단일 계좌 예약이라 **2계좌 동시 매도 분기에서 제외**. 취소는 `[📋 진행중]`에서 해당 미체결 주문 취소 → 다음 감시 사이클이 사라진 예약을 자동 정리. ⚠️ 트레일링 예약이 미체결 매도로 잡혀 있으면 `/api/order/check`의 `_pending_sell_qty` 차감 덕에 **일반 매도 max_qty가 자동으로 줄어든다**(이중 매도 구조적 차단).

  **텔레그램 발송 정책 (웹 매매)** — 거부·검증 에러는 토스트만, **매매등록(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으로 초록·수신버블이 생기고 자동입력이 깨질 수 있음. **타인 대상도 `--service imessage`** — 가희 리마인더(`gahee_reminder._send_imessage`)가 `sms` 강제였다가 2026-08-03 전환. SMS 강제는 그 수신자에게 3전 3패였고(전부 `error=4` 미발송, 살아남은 건은 Messages가 임의로 RCS로 바꿔준 운), 7/25 미발송이 성공으로 기록돼 7월 잔액이 통째로 누락됐다. ⚠️ **`imsg send`의 rc=0은 발송 성공이 아니다** — Messages에 넘겼다는 뜻일 뿐이고 실제 실패는 비동기로 `chat.db`의 `error`에 찍힌다. rc만 보는 코드는 미발송을 성공으로 오인한다. ⚠️ **자동화 → 메시지(AppleEvents) 권한이 `imsg`·iTerm2에만 허용, Claude Code에는 거부**라 코디 셸에서 `imsg send`를 부르면 20초 타임아웃. 발송은 launchd 경유(임시 oneshot plist)로만 가능.

  **신규 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)

  **기업정보 모달 (4탭)** — 종목 행 `기업정보` 버튼 → `/api/stock_info?code`(키움 ka10001 + 네이버 분기영업이익 + `fnguide_client` 펀더멘털·컨센서스 + `wisereport_client` 리비전·서프라이즈·최근리포트) fetch → 단일 종목은 4탭 렌더: **요약**(현재가·목표주가·리비전·증가율·서프라이즈 + 최신 리포트 1건) / **기업정보·가치**(PER·PBR·EPS·BPS·ROE·영업이익·52주·유통비율·외국인) / **성장성·컨센서스**(매출/EPS 증가율·목표주가·투자의견·추정PER〔현재PER 병기〕·목표가 리비전·서프라이즈) / **투자리포트**(최근 증권사 리포트 목록, 목표가 ▲상향/▼하향). 탭은 모달 전용 클래스(`.info-tabs/.info-tabbtn/.info-tabpanel` + `data-info-tab`/`data-info-panel`)와 독립 핸들러로 자산탭 `.sub-tab`과 격리(전역 restoreSubs 충돌 회피). 항목별 ⓘ 탭 팝업 설명(`INFO_SPECS`/`INFO_DESC`, document 위임 `.info-label`). 비교 모드(여러 종목)는 탭 없이 기존 비교표 유지. ETF 등 데이터 없으면 해당 탭 "데이터 없어요".

  **메모 모달 (`note-modal`)**(2026-06-26) — 보유·관심·감시종목 행 모두에 `📝 메모` 버튼. 클릭 → 팝업 textarea(최대 1000자, 여러 줄)에 기존 메모 prefill → [저장]은 `/notes/set` POST(submit 핸들러가 `note-form`을 fetch로 가로채 **현재 탭 유지** — 네이티브 폼이면 303→`/` 리다이렉트로 자산정보 탭으로 튐) 후 패널 새로고침, [삭제]는 버튼 왼쪽 위 작은 **확인 팝업(예/아니오)** 후 빈 텍스트 submit으로 항목 제거(오삭제 방지, `note-confirm`). 메모 있으면 버튼이 `📝 메모 ●` 노란 강조(`has-note`)+hover 툴팁. 저장소 `state/behive_stock_notes.json`(`{by_code, by_name}`, code 우선→name fallback, `stock_tags`와 동일한 fcntl lock·atomic write). **종목 단위 공용**(계좌 분리 X). 백엔드 `_load/_get/_set_stock_note`·`_note_button_html`·`_render_note_modal`, 태그 모달 패턴 복제.

  **급등락 알림 — 표시자 🔔 + `[🔔 알림]` 버튼 + 팝업**(2026-08-04 도입 → **2026-08-06 개편**) — 처음엔 `detail-name` 줄의 **아이콘 전용 토글**이었으나, 관리자님 지시로 **아이콘은 on/off 표시만 하고 조작은 팝업 안으로** 옮겼다(아이콘 하나뿐이라 `title` 툴팁이 유일한 설명이었고 — 모바일엔 hover가 없다 — 오조작 위험도 있었다). 구성 3개:
  - **표시자**(`_surge_indicator_html`) — `detail-name` 줄 종목명 옆의 **비대화형 `<span data-surge-ind>`**. 꺼짐 🔕(opacity 0.45 + grayscale)·켜짐 🔔(초록 배경). ⚠️ **`data-surge-toggle` 을 달지 말 것** — 그 속성이 있어야 위임 핸들러가 잡으므로, 없어야 클릭이 무반응이다. CSS `.detail-name .surge-ind` 에 `cursor/hover/active` 를 주지 않는 것도 같은 이유(누를 수 있어 보이면 안 된다).
  - **버튼**(`_surge_info_button_html`) — `.actions` 줄 `📝 메모` 옆 `🔔 알림`. `class="kpi-more-btn btn-surge-info"` 라 **기존 kpi-modal 위임 핸들러(`t.closest('.kpi-more-btn')`, document 전역)가 그대로 잡는다 → JS·모달 마크업 추가 0**. 켜짐이면 `has-surge` 강조(메모의 `has-note` 패턴). **꺼진 종목에도 노출**한다 — 켜는 입구가 이 팝업뿐이라서. ⚠️ **`&name=` 을 src 에 실어야 한다** — 꺼진 종목은 토글 저장소에 이름이 없어서 팝업에서 켜면 빈 이름으로 저장되고 알림에 종목코드가 찍힌다(2026-08-06 실측).
  - **팝업**(`_render_surge_body`, `GET /api/surge?code=&name=`) — 토글 + 구간 사다리(⏫상한가·급등확대·급등1차·전일종가·급락1차·급락확대·⏬하한가, 호가창처럼 위가 비쌈) + 오늘 소진 현황(나간/남은/보류 칩) + 근거(`N일 이력 기준 · 상위 10%/2%`). ⚠️ **`sm.get_threshold_map()`/`sm.get_limit_map()` 을 절대 부르지 않는다** — 각각 ka10081(유량 초당 5건)·ka10095 를 때려서 팝업 열 때마다 조회가 나가고, 문턱 산출의 사이클당 12종목 페이싱이 무너진다. `surge_thresholds.json`·`surge_limits.json`·`surge_alerts.json` **캐시 읽기 + `_RT_HUB.get_quote` 뿐이라 키움 콜 0**(실측 2.9ms). 캐시에 없으면 상태별 안내로 대체 — 꺼짐/계산대기/이력부족(제한가만 표시)/stale. ⚠️ **stale(어제 캐시)이면 구간을 아예 안 보여준다** — 전일종가가 바뀌면 구간이 통째로 달라져 어제 값은 틀린 가격이다.
  - **토글 동작** — 팝업 안 토글이 `data-surge-toggle`+`data-surge-refresh` 를 들고 있어 **기존 핸들러·`POST /surge/toggle` 이 무변경으로 재사용**된다(낙관적 갱신 → 실패 롤백 → 토스트). ⚠️ `paintSurge(code,on)` 는 **code 로 전 인스턴스**를 칠한다(같은 종목이 여러 패널에 중복 노출 — 005930은 9곳) — 표시자·`.actions` 버튼·팝업 토글 3종을 함께. ⚠️ **`window.__behive_load`(패널 전체 새로고침)를 부르지 말 것** — 키움 조회까지 다시 돌아 몇 초 뒤에야 바뀐다. 서버가 `_invalidate_panels_cache()` 를 이미 했으니 다음 자동 새로고침이 일치시킨다. 토글 성공 시 `data-surge-refresh` 로 팝업 본문만 재 fetch(안 하면 켠 직후에도 '꺼짐' 문구가 남는다). ⚠️ **네이티브 폼으로 만들지 말 것** — 303→`/` 리다이렉트로 hash 탭이 날아간다(메모 모달과 같은 이유). ⚠️ **키는 종목코드 전용**(모니터가 시세 조회에 코드를 쓴다 — `_set_surge_toggle` 이 ValueError). 저장소 `state/behive_surge_toggles.json`(fcntl lock + atomic write)를 `surge_monitor.py` 가 읽는다. 백엔드 `_load/_is/_set_surge_toggle`·`_surge_toggles_lock`·`_surge_indicator_html`·`_surge_info_button_html`·`_render_surge_body`·`_surge_key_label`, CSS `.detail-name .surge-ind`·`.actions .btn-surge-info`·`.sg-*`.

  **관심종목 그룹 (`group-assign-modal` + `group-manage-modal`)**(2026-07-03) — 관심종목이 39개까지 쌓여 그룹 분류 도입. **관심종목 탭 전용**(감시종목 제외), `stock_tags`와 **독립**. 저장소 `state/behive_interest_groups.json` = `{"groups":[순서있는 이름], "members":{종목명:[그룹명]}}` — 멤버십 키는 **종목명**(interests가 종목명 dict 키라 안정적, rename 없음). **다중 그룹 소속**·빈 그룹 유지·표시순서·rename/delete 연쇄를 레지스트리 한 파일에서 표현. `stock_tags`/`stock_notes`와 동일한 fcntl lock·atomic write. 관심 삭제 시 `_apply_interests_action('delete')`가 `_interests_lock` **밖에서** `_set_interest_groups(stock,[])`로 멤버십 자동 정리(별도 파일 락, 데드락 회피). **패널**(`_render_interests_panel`): **그룹 풀다운**(`.igrp-select`/`.igrp-panel`, `전체`+`보유중`+멤버 있는 그룹+`미분류` 옵션, 다중소속은 각 패널 중복 노출). **보유중은 `mode=='held'`로 자동 채워짐**(레지스트리 미등록, 보유종목도 그룹 배정 시 해당 그룹에 함께 노출). 전체=보유+관심(보유 먼저), 미분류=미보유&미배정만. 보유·그룹 둘 다 없으면 단일 목록으로 폴백. 선택 상태는 자산탭 `.sub-tab`과 격리(`igrpState`+`restoreIgrp`, 3s swap 후 복원, 사라진 키는 전체로 폴백). 같은 종목이 여러 패널에 중복 → `details_mutex`는 같은 `data-row-key`끼리 안 닫음(펼침 유지), `apply()`는 `querySelectorAll`로 전 인스턴스 open 복원. 관심종목 행 **`+ 그룹` 버튼은 `+태그` 버튼과 같은 디자인(`btn-add-tag`)으로 그 옆(detail-name 줄)** → 체크박스 배정 모달(+새 그룹 인라인, `new_group`). **풀다운 오른쪽 `+` 버튼**(`.igrp-add-btn`, flat·empty 케이스에도 항상 노출)→ 작은 드롭다운 메뉴(`.igrp-menu`: `종목 추가`=interest-modal / `그룹 관리`=group-manage-modal). 하단 트리거(관심추가·그룹관리 버튼)는 이 메뉴로 대체·제거. 그룹 관리 모달(그룹당 2줄 카드: 이름 인풋+수 / ▲▼·이름변경·삭제, 하단 새 그룹 행). **관리 모달 open은 트리거 `data-group-all`+`data-group-counts`(JSON)로 서버 왕복 없이 즉시 렌더**, 이후 op는 keepOpen fetch 응답 `{groups,counts}`로 재렌더. 엔드포인트 `POST /interests/groups/set`(stock+groups 반복필드+new_group)·`POST /interests/groups`(op=create|rename|delete|reorder|list〔list는 조회 폴백용 유지〕, `in_actions` 검사보다 먼저 정확매칭). 백엔드 `_load/_save/_get_interest_group(s)`·CRUD(`_create/_rename/_delete/_reorder_interest_groups`)·`_set_interest_groups`·`_interest_group_button_html`·`_render_group_assign_modal`·`_render_group_manage_modal`, 태그/메모 모달 패턴 복제. 기존 39개는 미분류로 자동 편입(쓰기 마이그레이션 없음). 레거시 `에이피알.tag:"뷰티"` 인라인 필드는 미사용 방치.
- `send_balance_to_budget.py` — 매월 1일 04:30 launchd. 본인+가희 계좌를 소유자별(self/gahee)로 잔액·예수금·평가액 집계해 `agents/budget/inbox/incoming/`에 envelope(`topic: securities_balance`, schema v2 `by_owner`)로 떨어뜨린다. 골디가 self→`증권(효원)`·gahee→`증권(가희)`로 reconcile 분개. 골디 월간 결산(05:00) 입력. LLM 미경유, 실패 시 레이 텔레그램으로 자가 알림.
- `trade_journal.py` — 종목별 매매 기록 누적. 키움 REST에 기간 거래내역 API 부재 → 평일 21:00 launchd(NXT 야간 마감 후)로 ka10170 4계좌 호출해 `state/trade_journal.jsonl`에 적재. `(date, account)` 단위 idempotent, 휴장일/주말 self-skip(`--force`로 우회). 적재 시작일 2026-05-13 이전 보유분은 `seed` 명령(1회)으로 현재 평단가×(보유-당일매수+당일매도) 단일 행으로 압축 적재됨(`seed=true` 플래그·`*` 마커). CLI: `collect [--date YYYYMMDD]` / `seed` / `show <code|name>` / `query [--from --to --account --code]`
- `market_indicators_sync.py` — 시장 단위 ADR·투자자별 매매 누적. 네이버 m.stock `/api/index/{KOSPI|KOSDAQ}/integration` 한 콜로 dealTrendInfo·upDownStockInfo 수집 → `state/market_indicators_history.jsonl`. `(date, market)` 단위 idempotent, 휴장/주말 self-skip(`--force`). 평일 21:00 stock.trade-journal launchd에 통합 발화 (별도 plist 없음). KRX 정보데이터시스템은 응답 패턴 변경으로 백필 보류 — 매일 누적만 시작. behive_web 자산정보 탭의 시장정보 sub-tab이 sparkline 데이터원으로 사용
- `sim/` (스크립트 아닌 **별도 패키지** — `agents/stock/workspace/sim/`) — **자동매매 시뮬레이션(페이퍼 트레이딩) 엔진**. 실주문 `orders/`와 완전 분리, 가상자본 1억으로만 동작(`INITIAL_CAPITAL`, 2026-06-10 1천만→1억). 구성: `config.py`(파라미터)·`indicators.py`(SMA/RSI/ATR/고저/거래량 순수함수)·`universe.py`(**누적 관찰목록** `state/sim/watchlist.json` — 비하이브 워치+관심+본인보유를 매 스캔 **자동 편입**하되 **자동 삭제 X**(수동만). `sync_watchlist`/`add_manual`/`remove`)·`data.py`(시세 batch·일봉 **캐시 우선**·수급 60m캐시·애널〔fnguide/wisereport 12h 캐시, **캐시 미스 때만 0.4s 페이싱**으로 연속 크롤링 차단 회피〕)·`signals.py`(진입/청산 규칙 + 손절·목표 계산 + dashboard용 체크리스트)·`portfolio.py`(가상 체결·비용·손익·MDD)·`engine.py`(스캔 오케스트레이션 — `_gather(extra_codes)`로 **보유 종목은 관찰목록에서 빠져도 청산 추적**)·`benchmark.py`(**기준지수 대비 알파** — 네이버 m.stock 지수 일별종가를 `state/sim/index_history.json`에 누적〔KOSPI/KOSDAQ, pageSize≤50〕, 구간 등락률·초과수익 계산. 스캔마다 `update_today` 1회 갱신, 실패 무시)·`__main__.py`(CLI)·`sim_web.py`(별도 대시보드 서버 18792). 전략: 방향=애널 약한게이트+가점, 타이밍=수급·기술. **ETF/ETN은 자동매수 제외**(2026-06-09 — 방향성 엔진인 애널 컨센서스가 ETF엔 None이라 전략 부정합. `universe.is_etf()`가 운용사 프리픽스(KODEX/TIGER/ACE…)로 판별, engine 미보유 매수 분기 직전에 `ETF — 자동매매 제외` SKIP. **보유 ETF는 청산까지 추적**(매수 분기 전 처리라 무관)). 매수=거래량+전고점 돌파 즉시 / 평소 눌림목(max(20일선, 전일종가−ATR)) 지정가. **시장 국면 필터(6단계)**: `market_regime_map()`이 상승비율(폭)+지수추세(방향, 20일선)+당일등락(강도)을 조합해 `engine.REGIME` 6단계로 분류 — 🔥강세·📈반등(상승비율↑ & 지수 20일선 아래=약세탈출)·🌤완만상승 / ➖혼조·📉약세·❄️급락. **앞 3개=매수허용, 뒤 3개=보류**(`market_ok_map`이 `REGIME[g][2]`로 파생). 상승비율은 **항상 네이버 실시간**(`_live_breadth` 60s 캐시, 실패 시 EOD). 약세장이어도 **돌파 진입은 허용, 눌림목만 보류**(soft gate). 시장탭에 국면 배지+설명 표시(매수판단과 동일 소스). `code_market_map()`이 종목→KOSPI/KOSDAQ 매핑. (2026-06-09 강세/약세 2단계→6단계 국면으로 세분화) **2026-06-09 4종 추가**: ⓐ **리스크 기반 사이징**(`signals.target_value` — 수량=자산×`RISK_PER_TRADE_PCT` ÷ 손절폭, 균등비중 자산/`MAX_POSITIONS` 상한 캡, 0=끔. 변동성 큰 종목 자동 축소·종목당 리스크 균등화. 엔진·백테스트 공용) ⓑ **RSI 필터**(`indicators.rsi` 활성화 — 돌파 시 `RSI_OVERBOUGHT`↑ 과열 추격 보류 / 눌림목 시 `RSI_OVERSOLD`↓ 낙하 중 보류, 데이터 없으면 통과) ⓒ **시장 국면 6단계**(`market_regime_map`→`engine.REGIME`, 위 시장 국면 필터 참조. `_index_trend_ok`·`_index_daily_change` 보조. ⚠️2026-06-09 강세/약세 2단계가 84% 반등일을 약세로 오판해 6단계 국면으로 세분화) ⓓ **시간손절**(`MAX_HOLD_DAYS` 경과 & 미진전〔트레일링 전+진입가 이하〕 시 청산해 자본 회전, `evaluate_holding(held_days)` — 엔진=달력일·백테스트=`entry_date` 달력일). ⓖ **스캔 메모 4종**(2026-06-10): 변이 수가 늘어도 스캔이 선형 폭증하지 않게 — 호가(`data._BOOK_MEMO`, 체결 공정성 겸용)·일봉(`engine._SCAN_CANDLES`)·지표(`engine._IND_MEMO`, (code,지표파라미터) 키)·수급/애널(`data._FLOW_MEMO_SCAN`/`_ANL_MEMO_SCAN`)을 스캔 1회 동안 공유, `_gather`가 매 스캔 리셋. 핵심은 애널 캐시 stale 시 0.4s 페이싱이 변이 수만큼 반복되던 것 — 108계좌 판단 38s→2s. ⓕ **2026-06-10 코드리뷰 3종**: 재진입 쿨다운(전량청산 종목 **당일 재매수 금지** — `Portfolio.last_exit`/`exited_today`, backtest 동일 규칙. 휩쏘 churn 방지)·장중 거래량 환산(`_session_elapsed_frac` 0.15~1.0을 `ind['vol_time_frac']`로 주입 → signals가 돌파 거래량·회전율을 하루치로 환산 비교, '(환산)' 표기. 이전엔 오전 돌파가 구조적으로 불가했음. backtest는 1.0=무영향)·일별 성과이력(`state/sim/equity_history.json` — 매 스캔 당일 항목 덮어써 EOD값 보존, `{date:[{id,ph(파라미터해시),equity,ret}]}`. 변이 id 재사용돼도 ph로 식별. 국면별 사후분석용) ⓔ **추세 질 보강**(2026-06-09 `trend_ok` 게이트 강화: 기존 현재가>20일선+정배열(5>20)에 더해 ①**20일선 우상향 기울기**〔`sma_long_slope_up`, `TREND_SLOPE_DAYS`전 대비〕 ②**중기 정배열 5>`SMA_MID`(60)** — 둘 다 종목데이터라 **백테스트 반영**, 데이터 부족 시 통과. ③**상대강도**〔`rel_strength`=종목−지수 `REL_STRENGTH_DAYS`(20)일 수익률, engine이 주입하는 **라이브 전용**, 백테스트 None=중립, 체크표시만〕). **분할매수·추격매수**: 목표비중을 `ENTRY_TRANCHES`(기본 3)로 나눠 1/3씩 진입, 직전 진입가+ATR×`ADD_ATR_MULT` 돌파 & 추세 지속 시 다음 트랜치 추가매수(평단 가중평균, 손절 위로만 래칫)·트레일링/일부익절 시작 후엔 추가 종료. 매도=손절(max(진입−ATR×2, 10일저점))·**분할매도**(목표 도달 시 `SCALE_OUT_FRAC`(기본 50%) 익절 후 잔량 트레일링(고점−ATR×1.5))·추세수급 이탈 시 전량 청산. 부분매도는 closed/win 미카운트(전량 청산 시만 카운트). 결정론적(LLM 미경유). **기준지수 대비(알파)**: 가상계좌/백테스트 수익률을 같은 기간 KOSPI·KOSDAQ '매수후보유' 대비로 평가(수익률만으론 지수 못 이기면 무의미하므로). 라이브는 계좌 시작일~오늘, 백테스트는 윈도우 구간 대비. KPI '코스피/코스닥 대비(알파)' + 백테스트·비교 탭 알파 행. 경과기간 0(시작 당일)·캐시 부재 시 None('데이터 없음'). 상태 `state/sim/{portfolio.json,trades.jsonl,last_scan.json,index_history.json}`. CLI: `python3 -m sim {scan [--force]|status|report|reset --yes|sweep [frac]|backtest|backfill [--pages N] [--force]|backfill-index [--pages N]|variants {seed N|list|reset|clear}}` (cwd=workspace). launchd `stock.sim-scan`(15분)·`stock.sim-web`(상시).
  - **체크리스트 라벨 동적화**(2026-06-15): `추세(N일선 위)`·`정배열(5>N)`의 N은 **변이별 `config.SMA_LONG`(10/20/40)**을 읽어 표기. 이전엔 "20"으로 하드코딩돼 SMA_LONG=10/40 변이도 "20일선"으로 거짓 표기됨(차트의 5>20 정배열과 어긋나 보이던 원인). 하드코딩이 `signals.py`(buy_signal·evaluate_holding·trend 보강)와 **`engine.py` 추세미충족 분기** 두 곳에 있었음. ⚠️ 과거 `trades.jsonl` 판단근거는 거래 시점 baked라 옛 라벨 잔존(새 거래부터 정상). 라이브 판단은 `judge_view`가 변이 params 적용 후 계산하므로 즉시 정확. 추세 체크리스트는 `signals.trend_checks(ind)`로 추출해 매수판단·제외(SKIP)카드 공유(2026-06-15 — 제외 카드가 추세/정배열 2개만 보여 ①②통과인데 왜 SKIP인지 헷갈리던 것 → 4조건〔현재가>장기선·정배열·장기선 기울기·중기정배열〕+상대강도 전부 표시).
  - **즉시매수 토글 + 40일선 제거**(2026-06-15): `IMMEDIATE_ENTRY`(기본 0) — 추세·정배열·수급·애널 다 통과한 종목이 돌파·눌림목 둘 다 아니어도(=안 내려오는 상승 종목) 강세장·RSI정상이면 현재가 즉시 매수(`signals` 눌림목 WAIT 직전 분기, buy_path='immediate'). 꾸준한 상승 놓침 방지. 40일선 추세선 제거(어중간·관리자님 지시, 스윕 GRID도 `[10,20,60]`). 변이: 40 −72 후, **백테스트 검증 결과 즉시매수가 명확히 불리**(전체기간 10/20/60 모두 즉시ON이 수익률 1/3·승률 −10~25%p·PF 반토막·MDD↑ — 눌림목/돌파 기다림 규칙이 핵심 알파). 그래서 즉시매수는 라이브 강세장 한정 확인용 **3개만**(10/20/60 안정·보수게이트) 유지 → **총 219개**(비즉시216+즉시3). 즉시매수는 GRID 미포함이라 백테스트 기본은 OFF(검증은 위 수동 비교로 수행). 웹 초록 `즉시` 칩(`_immediate_chip_html`)·드롭다운 `[안정 즉시]`. ⚠️ 기존 216은 IMMEDIATE_ENTRY 기본 0이라 불변.
  - **물타기(애버리징) 토글**(2026-06-16): `AVERAGING_ENTRY`(기본 0)·`AVERAGING_DROP_PCT`(0.10)·`CRASH_DROP_PCT`(0.10). 켜면 보유 중 직전매수가 대비 -10% 하락 시 손절 대신 추가매수로 평단↓(종목당 10% 한도=ENTRY_TRANCHES 분할). 가드: 당일 -10%↓ 급락=물타기 금지·손절(그림 #3), 약세장(market_ok=False)=물타기 금지(그림 #4). 첫 매수는 기존 검증 진입(추세·정배열+돌파/눌림목) 그대로 — "우량주만 물타기". `evaluate_holding(market_ok)` 0번 분기, **add_kind='down'이면 손절선을 낮아진 평단 기준으로 재설정**(추격매수 up은 위로만 래칫, engine·backtest 공용). **백테스트 검증: 즉시매수와 반대로 유리**(10/20/60 모두 수익·승률↑, MDD 비슷 — 우량주+10%한도+급락손절 가드 덕). 라이브 변이 3개 추가(v301/302/303=10/20/60 안정·물타기ON) → **223개**. 웹 노랑 `물타기` 칩(`_avg_chip_html`). ⚠️ 백테스트는 시장중립이라 약세장 가드 미반영 — 진짜 하락장은 라이브 검증.
  - **추세선 60일 추가**(2026-06-15): 스윕 GRID `SMA_LONG`에 60 추가(`[10,20,40,60]`, 1152→1536조합). 60일선 상위 12조합 × 약세대처3 × 진입게이트2 = 72변이 추가 → **총 288개**(10/20/40/60 각 72). ⚠️ 60일선은 base 정배열(5>60)이 중기게이트(`SMA_MID=60`, 5>60)와 동일 — 중기게이트가 추가 필터링 없음(무해, 관리자님 인지·승인). ⚠️ 스윕 첫 실행이 워커 조용히 죽는 일회성 deadlock(메인 SN 0%CPU·워커0) → kill 후 재실행으로 정상화. 장중엔 sim-scan/web과 CPU 경쟁으로 20분+ 소요.
  - **추세 게이트 완화 토글**(2026-06-15): `TREND_SLOPE_GATE`·`MID_ARRAY_GATE`(기본 1=현행). `trend_ok`이 `getattr`로 참조 — 0이면 장기선 우상향/중기정배열(5>60) 게이트 미적용. 보수성(3거래일 매수 2건, 추세미충족이 비ETF의 93% 차단) 완화 실험용. **완화 변이**(2026-06-15 최종: **기울기 게이트만 OFF, 중기정배열(5>60)은 유지** — `TREND_SLOPE_GATE=0, MID_ARRAY_GATE=1`. 관리자님: "중기정배열은 살려야지". 144개 완화변이 전부 이 규칙, 계좌 초기화). 웹: 보라 `완화` 칩(`_relax_chip_html`)·드롭다운 `[안정 완화]`. `trend_checks`가 게이트 OFF 조건을 ok=None('게이트 끔')로 표기. ⚠️ TUNABLE에 추가돼 기존 108개는 apply_params에서 기본 1(현행)로 적용돼 불변.
  - **런타임 튜닝**: `config.TUNABLE` 29개 파라미터(분할매수 횟수·추격 ATR 배수·분할매도 익절 비율·종목당 리스크 비율·RSI 과열/과매도 컷·시간손절 일수·**약세장 매수모드**〔`BEAR_ENTRY_MODE` 0현금화/1돌파만/2눌림허용, signals에서 `market_ok` False일 때 분기, backtest는 market중립이라 무영향, 병렬변이로 라이브 비교〕 포함)를 `state/sim/params.json`로 오버라이드(웹 튜닝 탭에서 저장). config import 시 기본값 위에 덮어써 엔진이 다음 스캔부터 반영. `config._DEFAULTS`가 원본 기본값. `load_params/save_params/reset_params`.
  - **백테스트·스윕** (`backtest.py`/`sweep.py`): signals.py 그대로 재사용해 과거 일봉 되감기(지표 t까지·종가체결·애널 중립·수급은 flow_history 사용). `sweep`은 9파라미터(SMA_LONG·RR·STOP_ATR·PULLBACK_ATR·VOLUME_BREAKOUT + 신규 RSI_OVERBOUGHT·RSI_OVERSOLD·MAX_HOLD_DAYS·RISK_PER_TRADE_PCT) **1152조합**을 학습/검증(out-of-sample 0.7) 분리 비교 (애널·시장필터는 backtest 중립이라 스윕 제외) → `state/sim/backtest_results.json`. **체결 현실화**(2026-06-10): backtest가 당일 종가 판단→**다음날 시가 체결**(pending 주문 큐, look-ahead 제거) + `BT_SLIPPAGE` 0.2%(매수 비싸게/매도 싸게). 체결 낙관분 ~5%p 제거 확인. 거래 없는 날 주문은 소멸(신호 지속 시 재큐잉). **3분할 꾸준함 순위**(2026-06-10): 전체기간 3분할 구간별 수익(`windows`/`worst_ret`)을 측정, 순위 1순위=최악구간 수익(한 구간 운빨 배제)·2순위=검증수익. 웹 행에 '구간별 검증(3분할)' 표시. **멀티프로세싱**(2026-06-10): `sweep`이 조합을 코어에 분배(`get_context('spawn')`, 워커=코어−2). 일봉·수급은 `_hist_cached`/`_flow_cached` 프로세스 메모이즈로 종목당 sqlite 1회만 읽음. **44분 → 약 5~7분**(10코어 기준). 데이터는 이미 sqlite 캐시 전용(키움 API 0회) — 병목은 네트워크가 아니라 지표 재계산(파이썬). spawn이라 워커가 메인 재import하지만 `sim/__main__.py` 가드로 안전. ⚠️ `GRID` 늘릴 때 곱셈 폭발 주의. ⚠️ 종가체결·현 워치리스트(생존편향)라 절대수익 아닌 상대순위로만 해석. 2026-06-09 스윕: 핵심 레버는 `VOLUME_BREAKOUT=1.3`(거래량 문턱 낮춤, 중앙 +19%) — 신규 4종은 강세 검증창에선 미미(RSI·시간손절=하락장 보험). 적용 균형값 params.json 저장(RISK 0.01 보수 유지, 0.02 고수익은 강세장 레버리지라 채택 X).
  - **수급 백필** (`backfill_flow.py`): ka10059 연속조회로 종목별 투자자 순매수 다년치(기본 6페이지≈2.4년) → `state/sim/flow_history.sqlite`(code,date,foreign_net,inst_net,indiv_net 천주). idempotent·rate limit 페이싱+429 백오프. backtest가 이 DB로 수급(외/기) 반영 (없으면 중립). 2026-06-08 54종목 3만행 1회 적재 완료.
  - **병렬 페이퍼** (`variants.py` + `engine.scan_all`): 스윕 상위 튜닝들을 각자 가상계좌(`state/sim/variants/<id>/`)로 실시간 동시 운영 → 비교(`variants_compare.json`). `scan_all`이 라이브 데이터 1회 수집 후 메인+모든 변이를 같은 시세로 평가(`_gather`+`_run_scan` 분리, Portfolio가 경로 인자 받음). 변이는 애널 게이트까지 실반영(백테스트가 못 보는 부분). `variants seed N`은 backtest_results 상위 N개 등록. **launchd `sim-scan`은 이제 `scan`→내부적으로 `scan_all` 실행**(메인+변이 매 스캔 갱신, 트리거·명령 동일).
  - **웹 3메인탭**(sim_web — **전략/관심종목/시장** 순, 기본=전략·비교. **기준전략(M) 폐지**(2026-06-10): scan_all의 메인 패스는 `execute=False` 판단 전용(last_scan=화면·상태점용, 메인 계좌 매매 동결), 실매매·비교는 변이 36개만. `sync_label_tops` 메인 제외 규칙 삭제(유형별 1위 전부 변이). 비교탭 행마다 **마킹 아이콘 피커**(2026-06-10 ☆토글→6종 아이콘 ⭐🔥💎🎯👀🚀 선택, `FAV_ICONS`/`favorites.json`은 `{아이콘: vid}` — **아이콘 하나당 전략 1개**(다른 전략에 쓰면 이동)·전략당 아이콘 1개. 칩 클릭→바텀시트 피커→`POST /fav_set`(fetch). 구버전 list는 자동 마이그레이션), 관심종목 탭 기본 시각 = 첫 마킹(FAV_ICONS 순) → 없으면 1위 변이. **약세모드 단어 칩**(2026-06-10): 비교 행 vid 옆 + 드롭다운 + 펼침 body '약세장대처' 행에 회피(0 현금화)/안정(1 돌파만·기본, 2026-06-11 '돌파'→'안정' 개명 — 적극과 혼동)/적극(2 눌림허용) 색 칩(`_BEAR_WORD`/`_bear_chip_html`, 클릭 시 설명 토스트. 아이콘→단어는 관리자님 요청). **전략 이름 = 4카테고리 두글자**(2026-06-15 `_cat_name`/`_cat_labels` — 추세〔민감10/표준20/둔감40/장기60〕·진입〔적극 vol≤1.5/신중〕·매도〔한방 RR≥2.5/균형/단타 RR≤1.5〕·베팅〔집중 risk≥2%/분산〕. 예 '표준·적극·균형·분산'). 비교 행·드롭다운·상세 헤더에 적용. 펼침/상세에 **`_cat_detail_html`** 4요소 풀이(라벨+실제값+쉬운설명). 긴 문장형 `_market_fit_summary`(상승장에 강한…)는 '시장적합' 행 ⓘ로 유지. **bt매칭은 `_btkey`(swept_params만)** — BEAR_ENTRY_MODE 등 백테스트 중립 키가 params에 있어도 같은 bt행에 매칭(모드 3형제는 bt순위 공유, 백테스트 탭은 36종만 표시가 정상). 헤더 상태점 자산줄은 '전략 N개 · 평균/1위 수익률'. 2026-06-10 내계좌 탭 삭제〔계좌 정보는 비교탭 메인 행으로〕·튜닝 보조탭 삭제〔params 변경은 백테스트 "이 튜닝 적용"으로만, `/save_params` 리다이렉트도 s=bt〕. 탭 라우팅 `?t=`+보조탭 `?s=`, 레거시 t=acct/tune→lab): 헤더 타이틀 옆 **시뮬 상태점**(`_engine_status`→`#health` 점: 🟢 장중 가동(최근 스캔 ≤12분)·🟡 장외 대기·🔴 장중 멈춤 의심, 클릭 시 상태 토스트).  **관심종목**(종목별 신호 체크리스트 + 각 평가항목 **ⓘ 버튼 → 쉬운 설명 토스트**〔`_CHK_HELP`/`_chk_key`/`_CHK_HELP_JS`, 바텀시트 `#chk-toast`, 열려있으면 자동새로고침 멈춤〕 + 상단 **종목 추가 폼**〔코드/이름〕 + 카드별 **✕ 삭제**〔수동만, 보유 중이면 청산 추적〕 + origin 라벨〔워치/관심/보유/수동〕 + 미스캔분 '스캔 대기' 표시) · **시장**(지수·ADR·상승하락·투자자 순매수 — **항상 네이버 m.stock 실시간**〔`_get_live_market` 60s 캐시, `market_indicators_sync.fetch_market` 재사용. 네이버가 장 마감 후에도 당일 최종값 제공하므로 세션 무관 최신 사용〕, **조회 실패 시에만** `market_indicators_history.jsonl` EOD 폴백. note는 장중='장중 실시간'·장외='직전 장 마감 기준'. 2026-06-09 초기엔 장중만 실시간이라 마감 후 어제 EOD를 보이던 빈틈을 항상-실시간으로 수정) · **전략**(2026-06-11 보조탭 버튼 제거 — 기본=비교만 표시, 백테스트는 비교 타이틀 우측 **[📊 모의결과보기]** 라벨(라디오 `sub-bt` 토글)로 진입·bt 패널 상단 [← 비교로]로 복귀. 그 옆 **[📋 거래목록]**(`sub-day`/`_day_trades_panel(sel_date)` — 전 전략 거래를 **달력형 날짜 선택**(`_DAY_CAL_JS` — 거래일만 밝게·클릭가능, 비거래일 어둡게, 월 이동 JS 클라이언트〔범위 밖 ‹›disabled〕, 오늘 강조. 컴팩트 max-width 300px. 날짜 클릭 시 **달력 안 닫고 fetch로 #day-rows·#day-sub·라벨만 갱신**〔history.replaceState로 `?d=` 보존〕, 별도 [닫기] 버튼. 접이식 details, **기본=오늘(거래 없어도 자동선택·클릭가능, `today-open` 점선 셀)**, 2026-06-15. 빈 날도 #day-rows 컨테이너 유지해 fetch 전환 정상)으로 **(종목,방향)별 아이템**(같은 종목 매수·매도 동시면 2아이템 분리, 요약=종목명+🟢매수/🔴매도 배지+전략수)으로 집계: 전략수·전략번호칩(상세링크)·가격대·평균손익·사유. 날짜는 simURL이 `&d=`로 보존). `?s=bt` 라우팅·60s 새로고침 라디오 보존은 유지. 구성 2: **백테스트**〔**유형별(세부 라벨 ~36종) 대표 요약** — 2026-06-10 단순화: 시장 성격별 그룹(상승장/추세장/조정장/출렁이는 장/보통 장에 강한 유형) 안에 각 유형 1위만 표시(꾸준함=worst_ret 순), 행=제목+최악·검증수익, 펼치면 구간별 검증·MDD·승률·PF·알파·파라미터·🌐. **비교군 추가/제거·이 튜닝 적용 버튼 제거**(읽기전용) — `variants.sync_label_tops()`는 **수동 전용**(2026-06-10 자동 동기화 끔 — sweep은 결과 파일만 갱신, 비교군 변경은 관리자님 명시 요청 시 `python3 -m sim variants sync` 또는 코디가 수동 등록)(같은 params 변이는 계좌 유지·신규 추가·탈락 삭제, 메인 조합 제외). 메인 변경은 코디/CLI로만. + **'↻ 다시 계산'**〔백그라운드 sweep — 결과 파일만 갱신, 비교군은 불변〕〕/**비교**〔병렬 변이 실시간 성적, 변이별 성적초기화/삭제 + **전체 삭제(메인 제외)** `/variants_clear_all`→`vmod.clear()`. 펼침 body 첫 행=**유형**(긴 문장형+ⓘ→🌐 시장적합 토스트, 2026-06-10 하단 🌐 버튼 대체)→둘째 행 **약세장대처**(`돌파 (기본)` 모드명만+ⓘ 토스트). bt 등록근거 행은 **모의평가**로 개명(1행 `N위/36`+2행 최악구간·검증, 모드 3형제는 `_btkey`로 같은 bt행 공유). **전략 상세 페이지**(2026-06-11): 행 펼침 [📈 자세히보기] → `GET /strategy?id=vN` 독립 페이지 — 일별 수익률 SVG 차트(`equity_history` id+ph 매칭, KOSPI 같은 구간 점선 오버레이) + 결정론 종합 코멘트(순위·알파·과최적 경고) + 현재 성적·모의평가·전략 성격·약세장대처·파라미터 표(`_TUNE_HELP` 라벨)·보유/관심/거래. 평가순 정렬은 고유번호 오름차순(#1→#108). 행 표기(2026-06-10): **순위 배지 `N위`가 앞, 고유번호는 `#N`**(`_vid_disp` — 'v37'→'#37' 표시 전용, 내부 id·URL은 raw. 드롭다운·👁토스트·사유 라벨 동일 표기)〕). 🌐는 `data-detail` 통합 토스트 핸들러로 헤더 상태점(`#health`)과 공유. sticky 헤더, 당겨서 새로고침. **새로고침 상태 보존**(2026-06-10): 60s 자동새로고침이 전체 리로드지만 펼침(details open)·스크롤을 sessionStorage(`simOpen`/`simScroll`, key=패널id+summary텍스트)로 저장·복원해 보던 화면 유지. 입력 중·토스트 열림엔 새로고침 멈춤. **탭 보존**(2026-06-11): 탭/서브탭 라디오 변경 시 URL 즉시 동기화(`window._simURL`+history.replaceState) — 당겨서·브라우저·마킹저장 등 모든 리로드가 보던 탭에 착지. HTML 응답에 `Cache-Control: no-store`(Safari 휴리스틱 캐싱이 옛 JS를 재사용해 탭 복원이 안 먹던 문제). **깜빡임 제거**(2026-06-12): 활성 탭/서브탭을 `render_html(tab,sub)`이 라디오 `checked`로 서버에서 미리 박음(GET이 `?t/?s` 파싱+레거시 보정) — 이전엔 기본탭 렌더→JS가 로드 후 전환하며 깜빡였음. `do_POST`: `/save_params`·`/run_sweep`·`/variants_{add,reset_one,remove,clear_all}`·`/watch_{add,remove}`·`/fav_set`(마킹 아이콘 지정/이동/해제 → `state/sim/favorites.json`) (그 외 읽기전용)
- Portfolio data: `memory/portfolio.json` (v2 스키마 참고용 스냅샷, `accounts.{일반,ISA}.positions`), `state/portfolio_daily_snapshot.json`, `state/kiwoom_tokens/{일반,ISA}.json`, `state/stock_codes.json`(키움 ka10099 lazy 캐시), `state/watchlist_alerts.json`(알림 중복 방지), `state/ipo_calendar_sync.json`, `state/fomc_calendar_sync.json`, `state/behive_*.json`, `state/fnguide_cache/{code}.json`(FnGuide 펀더멘털 12h), `state/wisereport_cache/{code}.json`·`{code}_reports.json`(컨센서스 12h·리포트 6h), `state/stock_reports/<code>/`(분석 보고서 HTML)

## Scheduled Jobs

OpenClaw 자동화는 두 갈래로 동작한다 (모두 Asia/Seoul):

### Cron (`state/openclaw.sqlite` → `cron_jobs`, OpenClaw 에이전트 세션 — LLM 경유)

- **오전 브리핑** (main) — **월~토** 07:30 (`30 7 * * 1-6`) — 뉴스 브리핑 메일 + 🇺🇸 미국증시 요약(@futuresnow). 영상이 아직 미게시면(KST 화~토 게시예정일) 발송 보류. **일요일 없음** (2026-08-12 주말 제외). **토요일은 해외 전용** — 아래 `briefing_mail.py` 의 `overseas_only` 참조
- **오전 브리핑 (fallback 0755)** (main) — 월~토 07:55 — 1차 폴백. 07:30에 미국증시 요약 미게시로 보류된 경우 재시도(여전히 미게시면 또 보류). `already_sent`로 idempotent
- **오전 브리핑 (fallback 0830)** (main) — 월~토 08:30 — 2차 폴백·최종. `prepare morning --final`로 US 미게시여도 무조건 발송. 07:30·07:55에 정상 발송됐으면 `already_sent`로 스킵
- **오후 브리핑** (main) — **평일** 19:00 (`0 19 * * 1-5`) — 뉴스 브리핑 메일. 주말 없음 (2026-08-12 주말 제외)
- **비하이브 종목분석 요약** (stock) — Weekdays 07/12/18시
- **월간 결산** (budget) — 매월 1일 05:00 — 자산 변동 메일 + 골디 텔레그램

### launchd (`~/Library/LaunchAgents/ai.openclaw.*.plist` — LLM 미경유, 직접 실행)

- **gateway** — 상시 daemon (포트 18789)
- **claude-remote-control** — on-demand daemon (코디 세션, `claude-code-session` 스킬이 띄움)
- **stock.behive-web** — 상시 daemon (워치리스트 웹뷰, Tailscale 18790)
- **stock.briefing** — 평일 20:10 — `stock_portfolio_report.py send --no-mail`: **포트폴리오 스냅샷 확보 전용, 메일 발송 없음**(2026-08-12 관리자님 결정 — 메일 통수 축소). 스냅샷은 자산웹 '당일 평가손익'·`rebase_to_nxt_close` 전일 baseline의 근거라 계속 쌓는다. ⚠️ **`run` 모드로 바꾸지 말 것** — 휴장일 self-skip이 `send` 분기에만 있어 휴장일 키가 이력에 섞이고 평문 리포트가 로그로 쏟아진다. `--no-mail`은 휴장일 가드·실패 자가알림을 유지하고 `send_mail`만 건너뜀
- **stock.briefing-fallback-2030** — 평일 20:30 — 오늘 스냅샷 없으면 `send --no-mail` 재실행 (idempotent). 최근 2주 실측 한 번도 실제 작업 안 함(20:10이 매일 성공)
- **stock.briefing-fallback-2100** — 평일 21:00 — **무조건 fresh fetch로 스냅샷 갱신** (`briefing_fallback.py force` → 스냅샷 있으면 `stock_portfolio_report.py run` 메일·텔레그램 X, 없으면 `send --no-mail` 폴백 + 실패 시 알림). 20:10 데이터 부정확 케이스 보완용
- **stock.watchlist-monitor** — 평일 10:00 / 12:00 / 14:00 — 워치리스트 buy/target/stop 알림 (2026-05-12: 15분 간격 → 3회로 축소)
- **stock.trailing-monitor** — 평일 09:00–15:30 **매 1분** (`StartCalendarInterval` 391엔트리, 스크립트 self-skip) — `trailing_monitor.py check`: **트레일링 스톱 감시**. ⚠️ **매매 API를 자동 호출하는 유일한 트리거** — 매매 자동 트리거 금지 규칙의 예외로 2026-07-30 관리자님 명시 승인. 호출하는 건 `kiwoom_order.modify_order` 하나뿐이고 신규 발주·수량·방향 변경 경로가 없음(PIN 승인 시 계좌·종목·계단별 수량·하락률 확정, 감시는 조건단가 **상향만**. 계단이 체결돼도 재배치=취소+신규발주는 하지 않는다). 예약 0건이면 즉시 종료(API 콜 0). ⚠️ 계단식이라 **정정 콜이 계단 수에 비례**(예약 3건×5계단이면 분당 최대 15콜). 로그 `logs/stock-trailing-monitor.{log,err.log}`. ⚠️ **cadence 이력 2분→30초→1분**(전부 2026-07-30) — launchd 최소 단위가 1분이라 30초는 프로세스 내부 `--repeat 2 --gap 30`으로 구현했고 CLI 옵션은 살아있음(다시 쓸 땐 plist 인자만 추가). `StartInterval=30`은 GUI idle 시 발화 보류라 사용 금지
- **stock.surge-monitor** — 평일 09:00–15:35 **매 1분** (`StartCalendarInterval` 396엔트리, 스크립트 self-skip) — `surge_monitor.py check`: **급등락 알림**(VI 발동 + ATR 배수 돌파 → 레이 텔레그램). 조회·알림 전용으로 주문 API 호출 경로가 없어 매매 자동 트리거 금지 규칙에 저촉되지 않음. 토글 0건이면 즉시 종료(API 콜 0). 평시 사이클 비용은 behive_web localhost 1콜 + 키움 0콜. 로그 `logs/stock-surge-monitor.{log,err.log}`. ⚠️ `StartInterval` 금지(GUI idle 시 발화 보류)
- **stock.ipo-calendar-sync** — **월·수·금 17:00** (plist 실측 `Weekday 1,3,5`; 문서에 오래 '매주 금요일'로 잘못 적혀 있었다 — 2026-08-12 수정) — IPO 청약·상장 일정 캘린더 등록
- **stock.fomc-calendar-sync** — 매월 1일 09:10 — `fomc_calendar_sync.py`: 연준 FOMC 회의 일정 캘린더 등록·갱신. 연준이 1년 이상 앞서 공표하고 변경이 거의 없어 월 1회로 충분(폴백 트리거 없음). LLM 미경유. 로그 `logs/fomc-calendar-sync.{log,err.log}`
- **stock.holiday-sync** — 매주 일요일 03:00 — investing.com KRX 휴장일 → `state/market_holidays.json` (behive_web 자동갱신 토글이 참조)
- **stock.send-balance** — 매월 1일 04:30 — 본인 잔액 → 골디 inbox (`securities_balance`)
- **stock.trade-journal** — 평일 21:00 — EOD 데이터 누적 묶음. ProgramArguments는 `/bin/sh -c` wrapper로 세 명령 sequential 실행: ①`trade_journal.py collect` (ka10170 4계좌 → `state/trade_journal.jsonl`) ②`market_indicators_sync.py collect` (네이버 m.stock KOSPI/KOSDAQ ADR·투자자별 매매 → `state/market_indicators_history.jsonl`) ③`cd workspace && python3 -m sim.backfill_flow --pages 1 --force` (수급 DB `flow_history.sqlite` 일일 증분 — 2026-06-10 추가, 1회성 백필 후 stale해져 백테스트 수급게이트가 중립으로 비활성되던 문제 해소. INSERT OR REPLACE idempotent, 행수기준 skip이라 `--force` 필수) ④`python3 -m sim universe all` (2026-06-15 추가 — sim 관찰목록 자동편입+자동제외. 키움 순위정보 ka90009 외인·기관 순매수 + ka10023 거래량급증 상위에서 ETF·하락·급증률 이상치 제외하고 각 15개·총 120 상한 편입(origin='auto'), origin=auto·미보유·10거래일 무신호는 자동 제외. 보유·watch·interest·manual은 불가침. 신호추적은 scan_all이 `universe.mark_signals`로 `state/sim/auto_seen.json`에 기록). 하나 실패해도 나머지 시도. 로그는 `logs/stock-trade-journal.{log,err.log}` 한 곳에 합쳐짐.
- **stock.sim-scan** — 평일 09:00–15:30 **매 2분** (`StartCalendarInterval` 196엔트리, 엔진이 장외/휴장 self-skip) — **자동매매 시뮬(페이퍼)** 1회 스캔. (2026-06-09 15분→5분, 2026-06-17 5분→2분 단축 — 스캔 1회 실측 ~3.6초라 2분 cadence에 오버랩·rate limit 여유. 1분이 캘린더 트리거 한계지만 일봉 기반 신호라 2분으로 충분 판단) ⚠️ 2026-06-09 `StartInterval` 900s → `StartCalendarInterval` 전환: GUI LaunchAgent의 StartInterval은 세션 idle/디스플레이 sleep 시 timer coalescing으로 발화가 보류(`pended`)돼 장중에 안 도는 문제 발견 (calendar 잡인 git-autopush는 새벽 02:00에도 정상 발화하는 게 비교 증거). 월시간 기준이라 idle 지연 없음. `python3 -m sim scan` (cwd `agents/stock/workspace`). 가상자본 1억, 규칙 엔진(LLM 미경유). universe=누적 관찰목록(워치+관심+보유 자동편입·수동삭제), 방향=애널 약한게이트+가점, 타이밍=수급·기술. 결과 `state/sim/`. 실주문 `orders/` 불가침. 상세는 아래 sim 모듈 섹션·레이 MEMORY.md
- **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

에이전트 간 데이터 hand-off는 **파일 기반 inbox**로만 한다. LLM-to-LLM 자연어 통신은 프롬프트 인젝션·할루시네이션 증폭 위험이 있어 금지.

### 디렉터리 구조 (수신자 소유)

```
agents/<recipient>/inbox/
  ├─ incoming/   ← 새 메시지
  ├─ processed/  ← 처리 완료 후 이동
  └─ failed/     ← 처리 실패 (스키마 오류·미등록 topic 등)
```

수신자는 자기 inbox를 책임진다 (정기 폴링·청소·감사). 송신자는 `incoming/`에 쓰는 것까지만.

### Envelope (불변 — v1)

```json
{
  "message_id": "uuid",
  "from": "stock",
  "to": "budget",
  "topic": "securities_balance",
  "created_at": "2026-04-26T20:10:00+09:00",
  "schema_version": 1,
  "payload": { ... }
}
```

파일명: `<from>__<topic>__<isoTime>.json` (정렬·검색 용이)

### 원칙

- **payload는 순수 데이터** — 자연어 지시문 금지 (프롬프트 인젝션 차단)
- **idempotency** — 수신자는 `message_id` 중복 처리 안 함
- **새 topic은 `INBOX_TOPICS.md`에 등록 필수** — 미등록 topic은 자동 `failed/`
- **응답 필요 시** — 수신자가 송신자 inbox에 새 메시지 작성 (양방향 ack 메커니즘 없음)
- **GC** — `processed/`는 30일 후 정리, `failed/`는 사람이 검토해서 수동 삭제

상세한 topic 스키마와 운영 규칙은 `INBOX_TOPICS.md` 참조.

### Cody Inbox (`agents/cody/inbox/`)

코디는 OpenClaw 에이전트가 아니지만 한 가지 예외로 inbox를 가진다. **에이전트가 자체 개선한 결과를 코디에게 검증·후속 개선 위탁**하는 단방향 채널이다.

- **토픽:** `improvement_review` (스키마는 `INBOX_TOPICS.md`)
- **자연어 허용 예외:** payload `summary`/`rationale`/`self_review_notes`/`concerns[].question`은 자연어 OK. 단 "X 해줘" 류 지시문 금지, 사실·관찰·우려만
- **처리 흐름:**
  1. 코디 세션 기동 시 `incoming/` 개수만 확인 → 1개 이상이면 한 줄 알림 ("📥 코디 인박스에 N개 처리 대기 중입니다.")
  2. **관리자님 명시 요청 전엔 상세 보고·검증·개선 시작 X** — 자동 처리 금지
  3. 관리자님이 "검증 큐 확인해줘" 등 호출하면 그때 envelope 상세 요약 → 우선순위 위임
  4. 코디가 `changed_paths` 검증 → 필요 시 직접 개선 (위험 작업은 별도 컨펌)
  5. envelope을 `processed/`로 이동, 같은 basename + `_report.md`에 검증 결과·후속 개선·잔여 위험 기록
  6. **GC (같은 시점):** `processed/`의 mtime 7일 초과 항목을 `trash`로 정리 (envelope JSON + report MD 짝으로). `failed/`는 손대지 않는다
  7. 스키마 위반은 `failed/`로 이동 후 관리자님에게 보고
- **헬퍼 미정:** 송신측은 에이전트가 직접 envelope JSON 작성. 패턴 굳으면 추후 추출
- **회신 envelope 없음:** 결과는 `processed/`의 report 파일로만 남는다. 송신 에이전트가 후속 사이클에서 직접 조회

## Communication Rules

- Respond in Korean (한글)
- Use polite speech (존댓말)
- Address the owner as 관리자님
- End responses with status on a new line: `[진행중]` or `[답변완료]`
- Keep responses short, action-oriented, result-first
- Avoid unnecessary explanation — How > Why
- Use `trash` over `rm` for deletions

## Coding Behavior Rules

LLM 흔한 실수를 줄이기 위한 행동 규칙. 사소한 작업은 판단으로 생략 가능하지만, 불확실하면 caution 쪽으로 기운다.

### 1. 코딩 전에 생각 (Think Before Coding)

**가정하지 말고, 혼동을 숨기지 말고, 트레이드오프를 드러낼 것.**

- 가정은 명시적으로 말한다. 불확실하면 질문한다.
- 해석이 여러 개면 전부 제시한다 — 조용히 하나 고르지 않는다.
- 더 단순한 길이 보이면 먼저 말한다. 정당하면 반박한다.
- 모호하면 멈춘다. 무엇이 헷갈리는지 이름 붙이고 묻는다. (선택지는 `AskUserQuestion`)

### 2. 단순함 우선 (Simplicity First)

**문제를 푸는 최소 코드. 추측성 코드 금지.**

- 요청 범위를 벗어난 기능 X
- 1회용 코드의 추상화 X
- 요청되지 않은 "유연성"·"설정 가능성" X
- 일어날 수 없는 상황 대비 에러 핸들링 X
- 200줄 짠 게 50줄로 줄겠다 싶으면 다시 쓴다.

자문: "시니어 엔지니어가 이거 과설계라 할까?" 그렇다면 단순화.

### 3. 외과적 변경 (Surgical Changes)

**필요한 것만 건드린다. 자기가 만든 잔재만 정리한다.**

- 인접 코드·주석·포맷 임의 "개선" 금지
- 안 망가진 것 리팩토링 금지
- 다르게 하고 싶어도 기존 스타일 유지
- 무관한 dead code 발견하면 보고만 — 삭제 X
- 변경 때문에 생긴 import/변수/함수 orphan은 본인이 정리
- 사전 존재하던 dead code는 요청 없이 삭제 X

테스트: 변경된 모든 라인은 관리자님 요청에 직결되어야 한다.

### 4. 목표 기반 실행 (Goal-Driven Execution)

**성공 기준을 정의하고, 검증될 때까지 루프.**

- "validation 추가" → "잘못된 입력 테스트 작성 → 통과시키기"
- "버그 고쳐" → "재현 테스트 작성 → 통과시키기"
- "X 리팩토링" → "전후 테스트 통과 확인"

다단계 작업은 짧은 plan을 먼저 말한다:
```
1. [단계] → 검증: [확인]
2. [단계] → 검증: [확인]
```

강한 성공 기준은 독립적 루프를 가능케 하고, 약한 기준("동작하게")은 끊임없는 명세 요청을 부른다.
