feat: 리밸런싱 엔진 V1 + GAS 버그 수정 (2026-06-13)

주요 변경:
- tools/build_rebalance_engine_v1.py: REBALANCE_ENGINE_V1 신규
  * account_snapshot 직접 합산(_build_snap_position_map) → 소수주 분리 행 병합
  * 레짐 소스 macro.REGIME_PRELIM 최우선 (GAS 와 동일)
- src/gas_adapter_parts/gdf_06_rebalance.gs: runRebalanceSheet_() 신규
  * Logger.log / getSpreadsheet_() 로 run_all 연동 수정
- src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs
  * _mergePositionRecord_(): 소수주 중복 행 합산 신규
  * parseInt → parseFloat (qty, availQty)
- src/gas_adapter_parts/gdf_01_price_metrics.gs
  * 미보유 종목 SELL_READY → WATCH_EXIT_SIGNAL
- spec/41_release_dag.yaml: build_rebalance_sheet 노드 추가 (step_count 63)
- spec/51_formula_lifecycle_registry.yaml: REBALANCE_ENGINE_V1 등록

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 13:20:14 +09:00
commit ee3e799de1
1474 changed files with 176087 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
# HTS 캡처 파싱 프롬프트
HTS 캡처 이미지가 제공되면 이 프롬프트를 **분석보다 먼저** 실행한다.
출력 순서: ① 화면판별 → ② 검증 → ③ 충돌감지·캡처우선 선언 → ④ TSV붙여넣기 → ⑤ account_snapshot 상태갱신 → ⑥ 현금요약 → ⑦ 인라인재산출 → ⑧ 다음단계
---
## STEP 1 — 화면 종류 판별
| 화면 | 판별 기준 | 사용 가능 여부 |
|------|---------|-------------|
| **보유종목 화면** | 종목명·수량·평단·평가손익이 있음 | ✅ 잔고 원장으로 사용 |
| **현금/예수금 화면** | 예수금·D+2·주문가능금액이 있음 | ✅ 현금 원장으로 사용 |
| **자동투자/적립식 화면** | 월 납입금액·약정일이 있음 | ❌ 잔고 원장 사용 금지 (AS006) |
| **미체결 화면** | 주문종목·미체결수량이 있음 | ✅ open_order_amount 참조용 |
화면 판별 결과를 첫 줄에 명시한다. 예: `[보유종목 화면 확인] 일반계좌`
---
## STEP 2 — 검증 (출력 전 필수)
| 규칙 ID | 확인 내용 | 실패 시 |
|---------|---------|--------|
| AS001 | 숫자 앞의 라벨 확인 (수량: X주, 평단: X원) | parse_status = CAPTURE_READ_FAILED |
| AS002 | holding_quantity 정수 확인 (소수 불가) | CAPTURE_READ_FAILED |
| AS003 | market_value ≈ holding_quantity × current_price (오차 1% 이내) | DATA_CONFLICT 표시 |
| AS004 | 현금 필드 0 이상 | CAPTURE_READ_FAILED |
| AS006 | 자동투자 화면 여부 확인 | CAPTURE_PROVIDED_BUT_NOT_HOLDINGS |
검증 결과를 한 줄로 출력: `[검증] AS001 OK | AS002 OK | AS003 OK (오차 0.3%) | parse_status = CAPTURE_READ_OK`
---
## STEP 2b — 캡처 vs JSON/account_snapshot 충돌 감지 (캡처 우선)
**출력 조건**: `GatherTradingData.json` 또는 account_snapshot 데이터가 이번 대화에 업로드되어 있을 때
기존 시장 데이터(JSON `data.data_feed`의 Account_Holding_Qty·Account_Avg_Cost)와 캡처값을 종목별로 비교한다.
```
[충돌 감지 결과]
ticker | 항목 | JSON/GAS값 | 캡처(실시간) | 차이 | 우선권
000660 | holding_quantity | 80주 | 100주 | +20주 | ★캡처우선
000660 | average_cost | 175,000원 | 170,000원 | -5,000원 | ★캡처우선
012450 | holding_quantity | 20주 | 20주 | 없음 | 일치
```
**캡처 우선 규칙**:
- 캡처값 ≠ GAS값 → **캡처가 정답**. 이후 모든 분석은 캡처값 기준으로 진행.
- GAS값은 account_snapshot 붙여넣기 전 상태의 오래된 값임.
- JSON에 Account_Parse_Status=MANUAL_ENTRY/STALE_MANUAL/""인 종목도 캡처가 있으면 **캡처우선**.
- 충돌 없으면: `[충돌 없음] 모든 종목 일치`
**주의**: 충돌 종목에 대해 이후 분석에서 GAS의 Profit_Pct·Weight_Pct·Sell_Qty 등은 재산출 대상.
재산출 방법은 STEP 7 참조.
---
## STEP 3 — account_snapshot 탭 붙여넣기 표
**출력 조건**: 보유종목 또는 현금 화면이 CAPTURE_READ_OK일 때
**붙여넣기 방법**: Google Sheets → `account_snapshot` 탭 → **A3 셀 선택** → Ctrl+V
TSV(탭구분) 형식으로 출력한다. 각 행은 account_snapshot 탭의 1개 데이터 행이다.
현금 전용 행은 ticker·name·holding_quantity 이하 종목 컬럼을 빈칸으로 두고 현금 컬럼만 채운다.
```
[account_snapshot 탭 — A3 셀에 붙여넣기]
captured_at account account_type ticker name holding_quantity available_quantity average_cost total_cost current_price market_value profit_loss return_pct immediate_cash settlement_cash_d2 available_cash open_order_amount monthly_contribution_limit monthly_contribution_used parse_status user_confirmed stop_price highest_price_since_entry entry_date entry_stage position_type last_updated
```
**컬럼 순서 고정 — 27컬럼** (변경 금지):
`captured_at | account | account_type | ticker | name | holding_quantity | available_quantity | average_cost | total_cost | current_price | market_value | profit_loss | return_pct | immediate_cash | settlement_cash_d2 | available_cash | open_order_amount | monthly_contribution_limit | monthly_contribution_used | parse_status | user_confirmed | stop_price | highest_price_since_entry | entry_date | entry_stage | position_type | last_updated`
**작성 규칙**:
- `captured_at`: 첫 번째 컬럼. `YYYY-MM-DD HH:MM` 형식. 화면 시각 기준. 없으면 당일 날짜.
- `ticker`: 6자리 문자열 (예: `000660`). 이름만 보이면 종목코드로 변환. 불확실하면 빈칸.
- `holding_quantity`: 정수만. 소수점 불가.
- `available_quantity`: 매도가능수량 (HTS 화면에 표시되는 경우). 없으면 빈칸.
- `average_cost` / `current_price` / `market_value` / `total_cost`: 원 단위 정수 (콤마 없이).
- `total_cost`: 매입금액 = holding_quantity × average_cost. HTS에서 직접 읽으면 그 값 사용.
- `profit_loss`: 평가손익 (원). HTS 화면 값 직접 사용. 없으면 빈칸.
- `return_pct`: 수익률 %. 예: `7.5` (% 기호 제외). 없으면 빈칸.
- 현금 필드: 보유종목 화면에서 현금이 보이지 않으면 빈칸 (0으로 채우지 않음).
- `parse_status`: 검증 통과 시 `CAPTURE_READ_OK`, 실패 시 `CAPTURE_READ_FAILED`.
- `user_confirmed`: 항상 `Y` (붙여넣기 행위 자체가 사용자 확인임).
- 선택 포지션 상태 6컬럼(`stop_price`~`last_updated`)은 캡처에서 확인되지 않으면 빈칸. 기존 값을 보존해야 하면 STEP 4에서 해당 셀만 수정.
**출력 예시** (27컬럼 — 탭으로 구분):
```
[account_snapshot 탭 — A3 셀에 붙여넣기]
2026-05-18 09:30 일반계좌 일반계좌 000660 SK하이닉스 100 100 170000 17000000 181900 18190000 1190000 7.0 3500000 4200000 3500000 0 CAPTURE_READ_OK Y
2026-05-18 09:30 일반계좌 일반계좌 012450 한화에어로스페이스 20 20 980000 19600000 1216000 24320000 4720000 24.1 0 CAPTURE_READ_OK Y
2026-05-18 09:30 일반계좌 일반계좌 현금 3500000 4200000 3500000 0 CAPTURE_READ_OK Y
```
---
## STEP 4 — account_snapshot 선택 포지션 상태 갱신 표
**출력 조건**: stop_price, highest_price_since_entry, entry_stage, position_type, last_updated 갱신이 필요한 경우
**붙여넣기 방법**: 아래 표를 참고해 `account_snapshot` 탭에서 ticker 행을 찾아 해당 선택 열만 수정.
보유수량·평단은 STEP 3의 account_snapshot TSV가 담당한다. `positions` 탭은 사용하지 않는다.
```
[account_snapshot 탭 — 해당 ticker 행의 아래 선택 열을 수동 수정]
ticker stop_price highest_price_since_entry entry_stage position_type last_updated
```
**작성 규칙**:
- `stop_price`가 비어 있으면 GAS가 ATR 기반으로 추정한다. 수동 입력을 강제하지 않는다.
- `last_updated` = 오늘 날짜 `YYYY-MM-DD`.
- `position_type``core` 또는 `satellite`. 비어 있으면 GAS가 `satellite`로 간주한다.
---
## STEP 5 — 현금 요약 (1줄)
```
[현금 요약] 즉시현금: X원 | D+2현금: X원 | 주문가능: X원 | 미체결: X원
→ settings 탭 settlement_cash_d2_krw에 X원 입력 필요
```
D+2현금을 settings 탭에 수동 입력해야 GAS가 매수 가능 금액 계산에 사용한다.
---
## STEP 6 — 다음 단계 안내 (항상 출력)
```
[다음 단계]
1. account_snapshot 탭 A3에 위 표 붙여넣기
2. 필요한 경우 account_snapshot 탭에서 ticker별 stop_price·highest_price_since_entry·last_updated 선택 열만 수정
3. settings 탭 settlement_cash_d2_krw = [D+2현금 값] 입력
4. GAS runDataFeed() 재실행 → xlsx 원본 갱신 확인
5. `npm run convert-data-json` 실행 후 갱신된 `GatherTradingData.json` 업로드 후 분석 요청
```
---
## STEP 7 — 충돌 종목 인라인 재산출 (캡처 우선 적용)
**출력 조건**: STEP 2b에서 충돌 종목이 1개 이상 발견된 경우
GAS 재실행 전이라도 캡처값으로 핵심 지표를 직접 재산출해서 분석에 사용한다.
JSON/GAS의 해당 종목 계좌 값은 **무시**하고 아래 재산출값으로 교체한다.
### 재산출 항목
| 항목 | 재산출 공식 | 필요 입력 |
|------|-----------|---------|
| `Profit_Pct` | `(현재가 - 캡처평단) / 캡처평단 × 100` | 캡처평단, JSON Close |
| `Unrealized_PnL` | `(현재가 - 캡처평단) × 캡처수량` | 캡처수량·평단, JSON Close |
| `Weight_Pct` | `(현재가 × 캡처수량) / 총자산 × 100` | 캡처수량, JSON Close, 총자산 |
| `Sell_Qty` | `캡처수량 × Sell_Ratio_Pct / 100` (반올림) | 캡처수량, GAS Sell_Ratio_Pct |
| `매도가능수량` | `캡처 available_quantity` 있으면 상한 적용 | 캡처 available_quantity |
### 출력 형식
```
[캡처 우선 재산출] — GAS 재실행 전 임시값
ticker | 항목 | GAS값(무효) | 캡처재산출값 | 비고
000660 | Profit_Pct | +4.2% | +6.9% | 평단 변경(175000→170000)
000660 | Unrealized_PnL| 3,360,000원 | 1,190,000원 | 수량 변경(80→100)
000660 | Weight_Pct | 7.8% | 9.7% | 수량 변경
000660 | Sell_Qty | (미산출) | 25주 | TRIM_25(25%) × 100주
```
**재산출 불가 항목** (GAS 의존): SS001_Grade, RW_Partial, Flow_Credit, Final_Action 등 시장데이터 기반 점수.
이 항목들은 JSON 시장데이터 값을 그대로 사용하되, 수량·손익 계산만 캡처값으로 교체한다.
---
## 오류 처리
| 상황 | 출력 |
|------|------|
| 이미지 흐림·잘림으로 숫자 판독 불가 | 해당 필드 빈칸 + `parse_status=CAPTURE_READ_FAILED` |
| 종목코드 불확실 | ticker 빈칸 + name만 기재 + 경고 메모 |
| AS003 오차 > 1% | DATA_CONFLICT 표시 후 계속 기재 (사용자 확인 후 사용) |
| 자동투자 화면 | `parse_status=CAPTURE_PROVIDED_BUT_NOT_HOLDINGS` + 보유수량 기재 금지 |
| JSON 미업로드 | STEP 2b 생략, STEP 7 생략. 캡처값 단독으로 진행. |