From 2d515dd6695d4eaa7e7f624668e40c847b4a6370 Mon Sep 17 00:00:00 2001
From: xavis
Date: Fri, 12 Jun 2026 16:09:32 +0900
Subject: [PATCH] =?UTF-8?q?feat(spot):=202=EB=8B=A8=EA=B3=84=20=EC=9D=B8?=
=?UTF-8?q?=EA=B3=BC=20=EA=B8=B0=EB=B2=95=20=EB=B6=84=EC=84=9D=20=ED=8C=8C?=
=?UTF-8?q?=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EB=A7=88=EB=AC=B4?=
=?UTF-8?q?=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
common/spot/futures 경로 정비, 캔들 데이터 모듈 복원, MTF 규칙 자동 저장 및 2단계 설계·최종 정리 문서를 반영해 3단계 착수 기반을 확정한다.
Co-authored-by: Cursor
---
.env.example | 25 +-
.gitignore | 17 +-
README.md | 539 +++++++++----------
data/spot/mtf/mtf_rules_v3.json | 285 ++++++++++
docs/spot/2_analysis/stage2_design_guide.md | 315 +++++++++++
docs/spot/2_analysis/stage2_final_summary.md | 290 ++++++++++
scripts/0_ground_truth_futures.py | 64 ++-
scripts/2_render_best_technique_chart.py | 126 +++++
scripts/2_run_causal_sim.py | 27 +
scripts/2_run_mtf_analysis.py | 5 +
src/deepcoin/config.py | 14 +-
src/deepcoin/data/__init__.py | 1 +
src/deepcoin/data/candle_loader.py | 44 ++
src/deepcoin/data/candle_store.py | 191 +++++++
src/deepcoin/data/downloader.py | 185 +++++++
src/deepcoin/data/intervals.py | 89 +++
src/deepcoin/evaluation/causal_sim.py | 85 ++-
src/deepcoin/ground_truth/chart.py | 106 +++-
18 files changed, 2073 insertions(+), 335 deletions(-)
create mode 100644 data/spot/mtf/mtf_rules_v3.json
create mode 100644 docs/spot/2_analysis/stage2_design_guide.md
create mode 100644 docs/spot/2_analysis/stage2_final_summary.md
create mode 100644 scripts/2_render_best_technique_chart.py
create mode 100644 src/deepcoin/data/__init__.py
create mode 100644 src/deepcoin/data/candle_loader.py
create mode 100644 src/deepcoin/data/candle_store.py
create mode 100644 src/deepcoin/data/downloader.py
create mode 100644 src/deepcoin/data/intervals.py
diff --git a/.env.example b/.env.example
index 4fa8bcd..623b1e0 100644
--- a/.env.example
+++ b/.env.example
@@ -16,8 +16,8 @@ COIN_TELEGRAM_CHAT_ID=
SYMBOL=BTC
COIN_NAME=비트코인
-# --- 공유: 캔들 DB (현물·선물 유일한 공유 리소스) ---
-DB_PATH=coins.db
+# --- 공통: 캔들 DB (현물·선물 공유) ---
+DB_PATH=data/common/coins.db
DOWNLOAD_DAYS=3650
DOWNLOAD_INTERVALS=1,3,5,10,15,30,60,240,1440,10080,43200
API_REQUEST_SLEEP_SEC=0.35
@@ -25,7 +25,7 @@ API_REQUEST_RETRIES=3
# --- 0단계: GT 타점 (현물·선물 공통 파라미터) ---
GT_INTERVAL_MIN=3
-GT_LOOKBACK_DAYS=3447
+GT_LOOKBACK_DAYS=3650
GT_INITIAL_CASH_KRW=200000
GT_TRADING_FEE_RATE=0.0005
GT_ZIGZAG_REVERSAL_PCT=5.0
@@ -46,17 +46,17 @@ GT_ALIGN_TOLERANCE_BARS=480
GROUND_TRUTH_FILE=data/spot/ground_truth/ground_truth_trades_v3.json
GROUND_TRUTH_V1_FILE=data/spot/ground_truth/ground_truth_trades_v1.json
GROUND_TRUTH_V2_FILE=data/spot/ground_truth/ground_truth_trades_v2.json
-GROUND_TRUTH_CHART_V1_FILE=docs/0_ground_truth/spot/ground_truth_chart_v1.html
-GROUND_TRUTH_CHART_V2_FILE=docs/0_ground_truth/spot/ground_truth_chart_v2.html
-GROUND_TRUTH_CHART_V3_FILE=docs/0_ground_truth/spot/ground_truth_chart_v3.html
+GROUND_TRUTH_CHART_V1_FILE=docs/spot/0_ground_truth/ground_truth_chart_v1.html
+GROUND_TRUTH_CHART_V2_FILE=docs/spot/0_ground_truth/ground_truth_chart_v2.html
+GROUND_TRUTH_CHART_V3_FILE=docs/spot/0_ground_truth/ground_truth_chart_v3.html
-# --- 0단계: 선물 GT (0단계 공통, 차트·JSON 분리) ---
+# --- 0단계: 선물 GT ---
GROUND_TRUTH_FUTURES_FILE=data/futures/ground_truth/ground_truth_trades_v3.json
GROUND_TRUTH_FUTURES_V1_FILE=data/futures/ground_truth/ground_truth_trades_v1.json
GROUND_TRUTH_FUTURES_V2_FILE=data/futures/ground_truth/ground_truth_trades_v2.json
-GROUND_TRUTH_FUTURES_CHART_V1_FILE=docs/0_ground_truth/futures/ground_truth_chart_v1.html
-GROUND_TRUTH_FUTURES_CHART_V2_FILE=docs/0_ground_truth/futures/ground_truth_chart_v2.html
-GROUND_TRUTH_FUTURES_CHART_V3_FILE=docs/0_ground_truth/futures/ground_truth_chart_v3.html
+GROUND_TRUTH_FUTURES_CHART_V1_FILE=docs/futures/0_ground_truth/ground_truth_chart_v1.html
+GROUND_TRUTH_FUTURES_CHART_V2_FILE=docs/futures/0_ground_truth/ground_truth_chart_v2.html
+GROUND_TRUTH_FUTURES_CHART_V3_FILE=docs/futures/0_ground_truth/ground_truth_chart_v3.html
# --- 현물 1단계: GT sim ---
GROUND_TRUTH_CHART_SIM_V1_FILE=docs/spot/1_simulation/ground_truth_chart_sim_v1.html
@@ -74,3 +74,8 @@ MTF_REPORT_HTML=docs/spot/2_analysis/mtf_correlation_report.html
MTF_RULES_JSON=data/spot/mtf/mtf_rules_v3.json
CAUSAL_SIM_REPORT_JSON=docs/spot/2_analysis/causal_sim_report.json
CAUSAL_SIM_REPORT_HTML=docs/spot/2_analysis/causal_sim_report.html
+
+# 폴더 구조: data|docs / {common, spot, futures}
+# common — coins.db 등 공유 리소스
+# spot — 현물 GT·기법·분석·운영
+# futures — 선물 GT·분석·운영
diff --git a/.gitignore b/.gitignore
index b1bc460..b4b7b35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,9 +3,20 @@
.DS_Store
.idea
-# Runtime data & generated artifacts (scripts로 재생성)
-data/
-docs/
+# Runtime data & generated artifacts (scripts로 재생성) — 저장소 루트만
+/data/**
+!/data/spot/
+/data/spot/**
+!/data/spot/mtf/
+/data/spot/mtf/**
+!data/spot/mtf/mtf_rules_v3.json
+/docs/**
+!/docs/spot/
+/docs/spot/**
+!/docs/spot/2_analysis/
+/docs/spot/2_analysis/**
+!docs/spot/2_analysis/stage2_design_guide.md
+!docs/spot/2_analysis/stage2_final_summary.md
logs/
*.db
diff --git a/README.md b/README.md
index 88ce9b0..c5b3a6f 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,14 @@
# DeepCoin
-빗썸 KRW 마켓 암호화폐 캔들 데이터 수집 및 현물·선물 매매 전략 파이프라인.
+빗썸 KRW 마켓 암호화폐 캔들 데이터 수집 및 **현물**·**선물** 매매 전략 파이프라인.
+
+기본 전략 축: **3분봉 현물**, 최근 **10년** 데이터. `data/`·`docs/`는 **공통(common)** · **현물(spot)** · **선물(futures)** 세 유형으로 구분합니다.
## 주요 기능
-- 빗썸 Public API(v1) 기반 분·일·주·월봉 캔들 수집
-- SQLite(`coins.db`) 저장 — 테이블명 `{SYMBOL}_{인터벌코드}` (예: `BTC_60`, `BTC_10080`)
-- 2017-01-01~ 역방향 페이지네이션 수집 (기본 3650일·10년, **1분봉 포함**)
-- Ground Truth 기반 현물·선물 벤치마크 및 인과 기법 분석
+- 빗썸 Public API(v1) 기반 분·일·주·월봉 캔들 수집 (1분봉 포함)
+- SQLite 캔들 DB — 현물·선물 **공통** (`data/common/coins.db`)
+- Ground Truth(GT) 기반 현물·선물 벤치마크·인과 기법 분석·(예정) 실거래 운영
## 요구사항
@@ -18,297 +19,285 @@
```bash
cd DeepCoin
-conda activate ncue
+conda activate ncue # 또는 xavis
pip install -r requirements.txt
-cp .env.example .env # API 키 등 입력
+cp .env.example .env
```
+`.env` 권장값 (현물 3분봉·10년):
+
+```env
+SYMBOL=BTC
+DB_PATH=data/common/coins.db
+DOWNLOAD_DAYS=3650
+GT_INTERVAL_MIN=3
+GT_LOOKBACK_DAYS=3650
+```
+
+---
+
+## 폴더 구조 (공통 · 현물 · 선물)
+
+`data/`와 `docs/` 최상위는 동일하게 **common / spot / futures** 세 갈래입니다.
+
+```text
+DeepCoin/
+├── src/deepcoin/ # 소스 코드
+├── scripts/ # 파이프라인 스크립트
+│
+├── data/
+│ ├── common/ # 공통 — 현물·선물 공유 리소스
+│ │ └── coins.db # 캔들 OHLCV (유일한 공유 DB)
+│ ├── spot/ # 현물 전용 데이터
+│ │ ├── ground_truth/ # 0단계 GT JSON
+│ │ ├── techniques/ # 2단계 기법 결과
+│ │ └── mtf/ # 2단계 MTF 규칙
+│ └── futures/ # 선물 전용 데이터
+│ ├── ground_truth/ # 0단계 선물 GT JSON
+│ ├── techniques/ # (예정) 2단계
+│ └── mtf/ # (예정) 2단계
+│
+└── docs/
+ ├── common/ # 공통 문서 (예정)
+ ├── spot/ # 현물 리포트·차트
+ │ ├── 0_ground_truth/ # 0단계 GT 차트
+ │ ├── 1_simulation/ # 1단계 sim 차트
+ │ ├── 2_analysis/ # 2단계 분석 리포트
+ │ └── 3_operations/ # 3단계 운영 (예정)
+ └── futures/ # 선물 리포트·차트
+ ├── 0_ground_truth/ # 0단계 선물 GT 차트
+ ├── 1_simulation/ # (예정) 1단계
+ ├── 2_analysis/ # (예정) 2단계
+ └── 3_operations/ # (예정) 3단계
+```
+
+### 유형별 역할
+
+| 유형 | `data/` | `docs/` | 설명 |
+|------|---------|---------|------|
+| **common** | `coins.db` | (예정) | 현물·선물이 공유하는 캔들 DB |
+| **spot** | GT·기법·MTF JSON | 단계별 HTML·리포트 | 현물 파이프라인 산출물 |
+| **futures** | 선물 GT JSON | 단계별 HTML·리포트 | 선물 파이프라인 산출물 |
+
+테이블명: `{SYMBOL}_{인터벌}` (예: `BTC_3`, `BTC_1440`)
+
+---
+
+## 현물 파이프라인 전체 순서
+
+```mermaid
+flowchart TD
+ A[common: 캔들 수집] --> B[spot 0단계: GT 타점]
+ B --> C[spot 1단계: GT sim]
+ C --> D[spot 2단계: 인과 기법]
+ D --> E[spot 3단계: 실거래 운영]
+ B --> F[futures 0단계: 선물 GT]
+```
+
+| 순서 | 단계 | 유형 | 스크립트 | 산출물 |
+|------|------|------|----------|--------|
+| 0 | **사전** | common | `00_download.py` | `data/common/coins.db` |
+| 1 | **0단계** | spot | `0_ground_truth.py` | `data/spot/ground_truth/`, `docs/spot/0_ground_truth/` |
+| 2 | **1단계** | spot | `1_ground_truth_sim.py` | `docs/spot/1_simulation/` |
+| 3 | **2단계** | spot | `2_run_*.py`, `2_run_stage2_all.sh` | `data/spot/techniques/`, `docs/spot/2_analysis/` |
+| 4 | **3단계** | spot | (예정) | `docs/spot/3_operations/` |
+| — | **0단계** | futures | `0_ground_truth_futures.py` | `data/futures/ground_truth/`, `docs/futures/0_ground_truth/` |
+
+### 권장 실행 명령 (현물 + 선물 0단계)
+
+```bash
+conda activate ncue
+export PYTHONPATH=src
+
+# ── common: 캔들 수집 ─────────────────────────────────────────
+python scripts/00_download.py # 증분 갱신
+python scripts/00_download.py --full # 최초 1회·재구축
+
+# ── spot 0단계: GT 타점 (3분봉·10년) ──────────────────────────
+python scripts/0_ground_truth.py --interval 3 --days 3650 --tier all
+
+# ── spot 1단계: GT sim (최근 3년) ───────────────────────────
+python scripts/1_ground_truth_sim.py --tier all
+
+# ── spot 2단계: 인과 기법 (일괄) ──────────────────────────────
+bash scripts/2_run_stage2_all.sh
+
+# ── futures 0단계: 선물 GT (현물 GT 기반) ───────────────────
+python scripts/0_ground_truth_futures.py --tier all
+
+# ── spot 3단계: 실거래 운영 (구현 예정) ───────────────────────
+# python scripts/3_execute_live.py
+```
+
+---
+
+## 단계별 상세
+
+### common — 캔들 수집 (사전)
+
+| 항목 | 내용 |
+|------|------|
+| DB 경로 | `data/common/coins.db` (`DB_PATH`) |
+| 기본 동작 | DB 최신 시각 이후 증분 갱신 |
+| 전체 재수집 | `--full` |
+| 1분봉만 풀 다운 | `00_download_candles.py --full --days 3650 --intervals 1` |
+
+### spot 0단계 — GT 타점
+
+사후 최적 매매 타점. **실거래 불가**, 이후 단계의 정답지(기준선).
+
+| 티어 | 포함 신호 |
+|------|-----------|
+| v1 | 스윙 B/S |
+| v2 | + 눌림목 B* |
+| v3 | + 돌파 B^ + 다이버전스 Bd/Sd |
+
+| 산출물 | 경로 |
+|--------|------|
+| JSON | `data/spot/ground_truth/ground_truth_trades_v{1,2,3}.json` |
+| 차트 | `docs/spot/0_ground_truth/ground_truth_chart_v{1,2,3}.html` |
+
+### spot 1단계 — GT sim (벤치마크)
+
+GT 타점 완벽 추종 시 수익 상한선. 최근 3년·초기 20만 원.
+
+| 산출물 | 경로 |
+|--------|------|
+| sim 차트 | `docs/spot/1_simulation/ground_truth_chart_sim_v{1,2,3}.html` |
+
+### spot 2단계 — 인과 기법 분석
+
+설계·목적·MTF 역할 등 상세: [`docs/spot/2_analysis/stage2_design_guide.md`](docs/spot/2_analysis/stage2_design_guide.md)
+
+| 순서 | 스크립트 | 산출물 |
+|------|----------|--------|
+| 2-1 | `2_run_techniques.py` | `data/spot/techniques/`, `docs/spot/2_analysis/comparison_report.html` |
+| 2-2 | `2_run_causal_sim.py` | `docs/spot/2_analysis/causal_sim_report.html` |
+| 2-3 | `2_run_signal_type_align.py` | `docs/spot/2_analysis/signal_type_report.html` |
+| 2-4 | `2_run_mtf_analysis.py` | `data/spot/mtf/mtf_rules_v3.json`, `docs/spot/2_analysis/mtf_correlation_report.html` |
+
+### spot 3단계 — 실거래 운영 (예정)
+
+2단계 검증 전략(`composite_v3` + MTF)을 빗썸 현물 API에 연결.
+
+| 항목 | 내용 |
+|------|------|
+| 캔들 동기화 | `00_download.py` 증분 갱신 |
+| 산출물 | `docs/spot/3_operations/` (예정) |
+
+### futures 0단계 — 선물 GT
+
+현물 GT를 롱·숏 4색 마커로 변환.
+
+| 현물 GT | 선물 마커 | 의미 |
+|---------|-----------|------|
+| buy | L↑ / S↑ | 롱 진입 / 숏 청산 |
+| sell | L↓ / S↓ | 롱 청산 / 숏 진입 |
+
+| 산출물 | 경로 |
+|--------|------|
+| JSON | `data/futures/ground_truth/ground_truth_trades_v{1,2,3}.json` |
+| 차트 | `docs/futures/0_ground_truth/ground_truth_chart_v{1,2,3}.html` |
+
+선물 1~3단계는 `docs/futures/{1_simulation,2_analysis,3_operations}/` (예정).
+
+---
+
## 환경 변수
| 변수 | 설명 | 기본값 |
|------|------|--------|
+| `DB_PATH` | 공통 캔들 DB | `data/common/coins.db` |
| `SYMBOL` | 코인 심볼 | `BTC` |
-| `COIN_NAME` | 코인 이름 | `비트코인` |
-| `DB_PATH` | SQLite 경로 | `coins.db` |
-| `DOWNLOAD_DAYS` | 전체 수집·차트 일수 (10년) | `3650` |
-| `DOWNLOAD_INTERVALS` | 인터벌 코드 목록 (`1`=1분봉 포함) | `1,3,5,10,15,30,60,240,1440,10080,43200` |
-| `BITHUMB_API_CANDLE_COUNT` | 요청당 캔들 수 (최대 200) | `200` |
-| `API_REQUEST_SLEEP_SEC` | API 호출 간격(초) | `0.35` |
+| `DOWNLOAD_DAYS` | 수집·차트 일수 | `3650` |
+| `GT_INTERVAL_MIN` | GT·기법 기준 인터벌(분) | `3` |
+| `GT_LOOKBACK_DAYS` | GT 타점 기간(일) | `3650` |
+| `GT_SIM_LOOKBACK_DAYS` | sim 거래 기간(일) | `1095` |
+| `GT_INITIAL_CASH_KRW` | sim 초기 자본(원) | `200000` |
-인터벌 코드: 분봉은 분 단위 숫자, 일봉=`1440`, 주봉=`10080`, 월봉=`43200`
+### 경로 변수 요약
-캔들 조회는 Public API이므로 API 키 없이도 동작합니다.
+| 용도 | 변수 예시 | 기본 경로 |
+|------|-----------|-----------|
+| spot GT JSON | `GROUND_TRUTH_FILE` | `data/spot/ground_truth/...` |
+| spot GT 차트 | `GROUND_TRUTH_CHART_V3_FILE` | `docs/spot/0_ground_truth/...` |
+| spot sim 차트 | `GROUND_TRUTH_CHART_SIM_V3_FILE` | `docs/spot/1_simulation/...` |
+| spot 2단계 | `TECHNIQUES_DIR` | `data/spot/techniques/` |
+| futures GT JSON | `GROUND_TRUTH_FUTURES_FILE` | `data/futures/ground_truth/...` |
+| futures GT 차트 | `GROUND_TRUTH_FUTURES_CHART_V3_FILE` | `docs/futures/0_ground_truth/...` |
+
+전체 목록: `.env.example`
+
+인터벌 코드: 분봉=분 단위 숫자, 일봉=`1440`, 주봉=`10080`, 월봉=`43200`
---
-## 파이프라인
+## 현물 2단계 인과 기법 (39종)
-**0단계**는 현물·선물 공통입니다. 이후 현물·선물은 각각 1~3단계로 독립 진행합니다.
+`src/deepcoin/techniques/` — 단일 33 + 복합 6, 미래 데이터 미사용.
-| 구분 | 단계 | 내용 | 산출물 | 스크립트 |
-|------|------|------|--------|----------|
-| 사전 | — | 캔들 수집 | `coins.db` | `00_download.py` |
-
-### 0단계 — GT 타점 (현물·선물 공통)
-
-| 시장 | 산출물 | 스크립트 |
-|------|--------|----------|
-| 현물 | `docs/0_ground_truth/spot/` | `0_ground_truth.py` |
-| 선물 | `docs/0_ground_truth/futures/` | `0_ground_truth_futures.py` |
-
-### 현물 단계
-
-| 단계 | 내용 | 산출물 | 스크립트 |
-|------|------|--------|----------|
-| **현물 1단계** | GT sim (벤치마크) 현물 | `docs/spot/1_simulation/` | `1_ground_truth_sim.py` |
-| **현물 2단계** | 인과 기법 분석 (현물) | `docs/spot/2_analysis/` | `2_run_techniques.py` 등 |
-| **현물 3단계** | 현물 실거래 운영 | (예정) | (예정) |
-
-### 선물 단계
-
-| 단계 | 내용 | 산출물 | 스크립트 |
-|------|------|--------|----------|
-| **선물 1단계** | GT sim (벤치마크) 선물 | (예정) | (예정) |
-| **선물 2단계** | 인과 기법 분석 (선물) | (예정) | (예정) |
-| **선물 3단계** | 선물 실거래 운영 | (예정) | (예정) |
-
-### 단계별 상세 설명
-
-#### 사전 — 캔들 수집
-
-빗썸 Public API로 분·일·주·월봉(1분봉 포함) OHLCV를 수집해 `coins.db`에 저장합니다. 이후 모든 단계는 이 DB만 참조하며, 현물·선물이 DB를 공유합니다.
-
-- 기본 동작: DB 최신 시각 이후 **증분 갱신** (`00_download.py`)
-- 전체 재수집: `--full` 옵션 (최초 1회·DB 재구축 시)
-- 테이블명: `{SYMBOL}_{인터벌코드}` (예: `BTC_3`, `BTC_1440`)
-
-#### 0단계 — GT 타점 (현물·선물 공통)
-
-**Ground Truth(GT)** 는 사후적으로 “이상적인 매매 타점”을 정의한 기준 데이터입니다. 미래 캔들을 참조해 ZigZag 스윙·눌림목·돌파·다이버전스 타점을 찾으므로 **실거래에 직접 쓸 수 없고**, 이후 단계(벤치마크·기법 분석)의 **정답지(기준선)** 역할을 합니다.
-
-| 티어 | 포함 신호 | 설명 |
-|------|-----------|------|
-| **v1** | 스윙 B/S | ZigZag 스윙 저점 매수·고점 매도만 |
-| **v2** | + 눌림목 B* | v1 + 상승 레그 중 눌림목 매수 |
-| **v3** | + 돌파 B^ + 다이버전스 Bd/Sd | v2 + 돌파 매수 + RSI 다이버전스 매수·매도 |
-
-v3 신호 유형 6종:
-
-| `signal_type` | 라벨 | 의미 |
-|---------------|------|------|
-| `swing_low` | B | ZigZag 스윙 저점 매수 |
-| `pullback` | B* | 상승 추세 눌림목 매수 |
-| `breakout` | B^ | 횡보·레인지 상단 돌파 매수 |
-| `div_bull` | Bd | RSI 상승 다이버전스 매수 |
-| `swing_high` | S | ZigZag 스윙 고점 매도 |
-| `div_bear` | Sd | RSI 하락 다이버전스 매도 |
-
-**현물 0단계** (`0_ground_truth.py`): 3분봉·2017년~ 구간에서 GT를 생성하고, 매수/매도 마커가 표시된 인터랙티브 차트를 만듭니다.
-
-- 데이터: `data/spot/ground_truth/ground_truth_trades_v{1,2,3}.json`
-- 차트: `docs/0_ground_truth/spot/ground_truth_chart_v{1,2,3}.html`
-
-**선물 0단계** (`0_ground_truth_futures.py`): 동일한 현물 GT 신호를 선물 롱·숏 4색 마커로 변환합니다. 가격·시각은 현물 GT와 동일하며, 해석만 선물 관점으로 매핑합니다.
-
-| 현물 GT | 선물 마커 | 의미 |
-|---------|-----------|------|
-| buy (B·B*·B^·Bd) | **L↑** | 롱 진입 (상방 매수) |
-| buy | **S↑** | 숏 청산 (하방 매도) |
-| sell (S·Sd) | **L↓** | 롱 청산 (상방 매도) |
-| sell | **S↓** | 숏 진입 (하방 매수) |
-
-- 데이터: `data/futures/ground_truth/ground_truth_trades_v{1,2,3}.json`
-- 차트: `docs/0_ground_truth/futures/ground_truth_chart_v{1,2,3}.html`
-
-#### 현물 1단계 — GT sim (벤치마크) 현물
-
-0단계 GT 타점을 **그대로** 따라 매매했을 때의 수익을 재현하는 **현물 벤치마크**입니다. “GT를 완벽히 따랐다면 얼마를 벌 수 있었는가”를 측정하며, 이후 인과 기법·실거래 전략이 비교해야 할 **상한선(천장)** 입니다.
-
-- 기간: 최근 **3년** (`GT_SIM_LOOKBACK_DAYS=1095`)
-- 초기 자본: **20만 원**
-- 매수 규칙: 총평가 구간별 현금 비율 상한 — 1억↑ 10% · 10억↑ 5% · 100억↑ 1%
-- 수수료: 편도 0.05% (`GT_TRADING_FEE_RATE`)
-- 산출물: `docs/spot/1_simulation/ground_truth_chart_sim_v{1,2,3}.html` (누적 수익 곡선·체결 마커 포함)
-
-#### 현물 2단계 — 인과 기법 분석 (현물)
-
-0단계 GT v3 타점을 **미래 데이터 없이** 재현할 수 있는 인과(causal) 매매 기법 39종을 실행하고, GT와 얼마나 맞는지 정량 평가합니다. 목표는 “실시간으로 쓸 수 있는 기법 조합이 GT 타점을 얼마나 포착하는가”를 파악하는 것입니다.
-
-| 스크립트 | 분석 내용 | 산출물 |
-|----------|-----------|--------|
-| `2_run_techniques.py` | 39종 기법 전체 실행 + GT 정합 비교 | `comparison_report.html`, `data/spot/techniques/*.json` |
-| `2_run_signal_type_align.py` | B/B*/B^/Bd/S/Sd 유형별 recall | `signal_type_report.html` |
-| `2_run_mtf_analysis.py` | GT v3 × 10개 TF 피처 상관 (Cohen's d) | `mtf_correlation_report.html`, `data/spot/mtf/mtf_rules_v3.json` |
-
-평가 지표:
-
-- **recall**: GT 신호 대비 기법 신호가 허용 오차(`GT_ALIGN_TOLERANCE_BARS`) 내에 맞춘 비율
-- **score**: recall·정밀도·타점 거리를 종합한 정합 점수
-- **MTF 상관**: 일·주·월 등 상위 TF 지표가 GT 유형별로 어떤 패턴을 보이는지 통계 분석
-
-#### 현물 3단계 — 현물 실거래 운영 (예정)
-
-현물 2단계에서 검증된 인과 기법(또는 `composite_v3` 등 통합 기법)을 빗썸 현물 API에 연결해 **실제 주문·체결·리스크 관리**를 수행하는 단계입니다.
-
-- 실시간 캔들 수신 및 인과 신호 생성
-- 주문 실행·포지션(보유 코인)·손절/익절 규칙
-- 1단계 GT sim 벤치마크 대비 실거래 성과 모니터링
-- 텔레그램 등 알림·로그·장애 복구
-
-#### 선물 1단계 — GT sim (벤치마크) 선물 (예정)
-
-선물 0단계 GT 타점(L↑/L↓/S↓/S↑)을 그대로 따라 롱·숏 포지션을 운용했을 때의 수익을 재현하는 **선물 벤치마크**입니다. 현물 1단계와 동일한 개념이나, 롱·숏 양방향·레버리지·청산 규칙 등 선물 고유 파라미터를 반영합니다.
-
-- 0단계 선물 GT 4색 마커 기준 체결 시뮬레이션
-- 현물 1단계와 동일한 초기 자본·기간·수수료 체계 (선물 수수료·슬리피지 별도 정의 예정)
-- 산출물 예정: `docs/futures/1_simulation/`
-
-#### 선물 2단계 — 인과 기법 분석 (선물) (예정)
-
-현물 2단계와 동일한 인과 기법 39종(또는 선물 특화 변형)을 선물 GT v3 타점과 정합 평가합니다. 롱·숏 각각의 recall, MTF 필터, 신호 유형별 성능을 선물 관점에서 분석합니다.
-
-- 선물 GT 6종 신호 ↔ 인과 기법 정합 리포트
-- 선물 MTF 상관 분석 및 규칙 추출
-- 산출물 예정: `docs/futures/2_analysis/`, `data/futures/techniques/`, `data/futures/mtf/`
-
-#### 선물 3단계 — 선물 실거래 운영 (예정)
-
-선물 2단계에서 검증된 전략을 실제 선물 거래 API에 연결해 롱·숏 포지션을 자동 운용하는 단계입니다.
-
-- 실시간 인과 신호 → 롱/숏 진입·청산
-- 레버리지·증거금·청산가 모니터링
-- 선물 1단계 sim 벤치마크 대비 실거래 성과 추적
-
-### 실행 순서
-
-0단계(GT 타점)를 먼저 만든 뒤, 현물 1단계 sim → 현물 2단계 인과 분석 순으로 진행합니다. 선물·현물 3단계는 각 시장의 1·2단계 완료 후 구현 예정입니다.
-
-```bash
-# 사전: 전체 인터벌 증분 갱신 (1분~월봉, DB 최신 이후만)
-python scripts/00_download.py
-
-# 사전: 전체 인터벌 풀 다운 (최초 1회 또는 DB 재구축)
-python scripts/00_download.py --full
-
-# 1분봉만 풀 다운 (수 시간 소요)
-python scripts/00_download_candles.py --full --days 3650 --intervals 1
-
-# 0단계: Ground Truth 타점 생성 (v1/v2/v3)
-python scripts/0_ground_truth.py
-
-# 0단계: 선물 GT 타점 차트 (롱·숏 4색, 타점만)
-python scripts/0_ground_truth_futures.py
-
-# 현물 1단계: GT sim (최근 3년 · 20만원)
-python scripts/1_ground_truth_sim.py --tier all
-
-# 현물 2단계: 인과 기법 GT 정합 비교
-python scripts/2_run_techniques.py
-python scripts/2_run_signal_type_align.py --from-cache
-python scripts/2_run_mtf_analysis.py
-```
-
-### 현물 2단계 인과 기법 목록 (39종)
-
-`src/deepcoin/techniques/` — 단일 33 + 복합 6, **미래 데이터 미사용**. 카탈로그: `data/spot/techniques_catalog.json`
-
-| ID | 기법 | 유형 | 설명 |
-|----|------|------|------|
-| `zigzag_causal` | 인과 ZigZag | 스윙 B/S | 되돌림 % 확정 시 스윙 저점 매수·고점 매도 (GT ZigZag 인과 버전) |
-| `minor_swing` | 소형 스윙 하이브리드 | 하이브리드 | 소형 ZigZag(2.5%) + 국소 극값 — GT 중간 눌림목 보완 |
-| `local_extrema` | 국소 극값 | 스윙 B/S | 국소 저점 매수·고점 매도 (눌림목 유형 포착) |
-| `pivot_swing` | 피벗 스윙 | 스윙 B/S | 피벗 저점 매수·고점 매도 (스윙 B/S) |
-| `fractal_swing` | 프랙탈 스윙 | 스윙 B/S | Williams 프랙탈 스윙 저점 매수·고점 매도 |
-| `swing_failure` | 스윙 실패 | 스윙 B/S | 스윙 고저점 돌파 실패(페일드 브레이크아웃) 반전 신호 |
-| `donchian` | 돈치안 채널 | 스윙 B/S | 돈치안(40) 채널 하단 매수·상단 매도 |
-| `ema_pullback` | EMA 눌림목 | 눌림목 B* | EMA(20/60) 눌림목 반등 매수·되돌림 매도 (B*) |
-| `fib_pullback` | 피보나치 눌림목 | 눌림목 B* | 피보나치 38.2~61.8% 되돌림 구간 매수·매도 (B*) |
-| `support_bounce` | 지지·저항 반등 | 눌림목 B* | N봉 지지·저항 터치 후 반전 (B*) |
-| `keltner_breakout` | Keltner 돌파 | 돌파 B^ | Keltner 채널 상·하단 돌파 (B^) |
-| `range_breakout` | 레인지 돌파 | 돌파 B^ | N봉 레인지 상·하단 돌파 (B^) |
-| `volume_breakout` | 거래량 돌파 | 돌파 B^ | 거래량 스파이크 + 레인지 돌파 (B^) |
-| `bb_squeeze_breakout` | BB 스퀴즈 돌파 | 돌파 B^ | 볼린저 밴드 스퀴즈 후 돌파 (B^) |
-| `rsi_divergence` | RSI 다이버전스 | 다이버전스 Bd/Sd | RSI 상승(Bd)·하락(Sd) 다이버전스 |
-| `macd_divergence` | MACD 다이버전스 | 다이버전스 Bd/Sd | MACD 히스토그램 상승(Bd)·하락(Sd) 다이버전스 |
-| `obv_divergence` | OBV 다이버전스 | 다이버전스 Bd/Sd | OBV 상승(Bd)·하락(Sd) 다이버전스 |
-| `bb_reversal` | 볼린저 역추세 | 지표 | BB(20,2) 하단 매수·상단 매도 + EMA 추세 필터 |
-| `ma_cross` | EMA 크로스 | 지표 | EMA(20/60) 골든크로스 매수·데드크로스 매도 |
-| `rsi_swing` | RSI 스윙 | 지표 | RSI(14) 과매도 반등 매수·과매수 하락 매도 |
-| `macd_cross` | MACD 크로스 | 지표 | MACD(12,26,9) 시그널선 골든·데드 크로스 |
-| `supertrend` | Supertrend | 추세 | Supertrend 상승·하락 전환 신호 |
-| `adx_trend` | ADX 추세 | 추세 | ADX(14) 강세 + DI 크로스 추세 신호 |
-| `ichimoku_trend` | 일목 추세 | 추세 | 일목 전환선·기준선 크로스 추세 신호 |
-| `parabolic_sar` | Parabolic SAR | 추세 | Parabolic SAR 추세 전환 신호 |
-| `stochastic_cross` | Stochastic 크로스 | 모멘텀 | Stochastic(14,3) %K/%D 크로스 |
-| `cci_extreme` | CCI 극값 | 모멘텀 | CCI(20) 과매도·과매수 반전 |
-| `roc_reversal` | ROC 반전 | 모멘텀 | ROC(12) 극값 반전 신호 |
-| `keltner_reversal` | Keltner 역추세 | 변동성 | Keltner 채널 하단 매수·상단 매도 |
-| `atr_channel` | ATR 채널 | 변동성 | EMA(20) ± ATR(14)×2 채널 반전 |
-| `pivot_points` | 피벗 포인트 | 구조 | 롤링 피벗 S1/R1 반전 |
-| `support_resistance` | 구조적 지지·저항 | 구조 | 스윙 피벗 기반 S/R 반전 |
-| `volume_spike` | 거래량 스파이크 | 거래량 | 거래량 스파이크 후 반전 (클라이맥스) |
-| `composite_swing` | 스윙 복합 | 복합 | 스윙 저점·고점 전담 기법 가중 투표 (B/S) |
-| `composite_pullback` | 눌림목 복합 | 복합 | 눌림목·역추세 기법 가중 투표 (B*) |
-| `composite_breakout` | 돌파 복합 | 복합 | 돌파·모멘텀 기법 가중 투표 (B^) |
-| `composite_divergence` | 다이버전스 복합 | 복합 | RSI/MACD/OBV 다이버전스 가중 투표 (Bd/Sd) |
-| `composite_v3` | v3 통합 스코어링 | 복합 | v3 GT 6종 신호 유형별 핵심 기법 가중 투표 + EMA(60) 추세 필터 |
-| `composite_full` | 전체 통합 복합 | 복합 | 전체 인과 기법 가중 투표 + EMA 추세 필터 |
+| ID | 기법 | 유형 |
+|----|------|------|
+| `zigzag_causal` | 인과 ZigZag | 스윙 B/S |
+| `minor_swing` | 소형 스윙 하이브리드 | 하이브리드 |
+| `local_extrema` | 국소 극값 | 스윙 B/S |
+| `pivot_swing` | 피벗 스윙 | 스윙 B/S |
+| `fractal_swing` | 프랙탈 스윙 | 스윙 B/S |
+| `swing_failure` | 스윙 실패 | 스윙 B/S |
+| `donchian` | 돈치안 채널 | 스윙 B/S |
+| `ema_pullback` | EMA 눌림목 | 눌림목 B* |
+| `fib_pullback` | 피보나치 눌림목 | 눌림목 B* |
+| `support_bounce` | 지지·저항 반등 | 눌림목 B* |
+| `keltner_breakout` | Keltner 돌파 | 돌파 B^ |
+| `range_breakout` | 레인지 돌파 | 돌파 B^ |
+| `volume_breakout` | 거래량 돌파 | 돌파 B^ |
+| `bb_squeeze_breakout` | BB 스퀴즈 돌파 | 돌파 B^ |
+| `rsi_divergence` | RSI 다이버전스 | Bd/Sd |
+| `macd_divergence` | MACD 다이버전스 | Bd/Sd |
+| `obv_divergence` | OBV 다이버전스 | Bd/Sd |
+| `bb_reversal` | 볼린저 역추세 | 지표 |
+| `ma_cross` | EMA 크로스 | 지표 |
+| `rsi_swing` | RSI 스윙 | 지표 |
+| `macd_cross` | MACD 크로스 | 지표 |
+| `supertrend` | Supertrend | 추세 |
+| `adx_trend` | ADX 추세 | 추세 |
+| `ichimoku_trend` | 일목 추세 | 추세 |
+| `parabolic_sar` | Parabolic SAR | 추세 |
+| `stochastic_cross` | Stochastic 크로스 | 모멘텀 |
+| `cci_extreme` | CCI 극값 | 모멘텀 |
+| `roc_reversal` | ROC 반전 | 모멘텀 |
+| `keltner_reversal` | Keltner 역추세 | 변동성 |
+| `atr_channel` | ATR 채널 | 변동성 |
+| `pivot_points` | 피벗 포인트 | 구조 |
+| `support_resistance` | 구조적 지지·저항 | 구조 |
+| `volume_spike` | 거래량 스파이크 | 거래량 |
+| `composite_swing` | 스윙 복합 | 복합 |
+| `composite_pullback` | 눌림목 복합 | 복합 |
+| `composite_breakout` | 돌파 복합 | 복합 |
+| `composite_divergence` | 다이버전스 복합 | 복합 |
+| `composite_v3` | v3 통합 스코어링 | 복합 |
+| `composite_full` | 전체 통합 복합 | 복합 |
---
-## 디렉터리 구조
+## 구현 현황
-```text
-DeepCoin/
-├── src/deepcoin/
-│ ├── data/ # 캔들 수집·로드
-│ ├── ground_truth/ # 0·1단계 GT·차트
-│ ├── techniques/ # 현물 2단계 인과 기법 (39종)
-│ ├── evaluation/ # 현물 2단계 GT 정합 평가
-│ └── mtf/ # 현물 2단계 MTF 분석
-├── scripts/ # 접두사: 00=사전, 0=0단계, 1=현물1, 2=현물2
-│ ├── 00_download.py # 사전 (증분/풀다운 래퍼)
-│ ├── 00_download_candles.py # 사전 (캔들 수집 본체)
-│ ├── 0_ground_truth.py # 0단계 현물 GT
-│ ├── 0_ground_truth_futures.py # 0단계 선물 GT 차트
-│ ├── 1_ground_truth_sim.py # 현물 1단계 sim
-│ ├── 2_run_techniques.py # 현물 2단계
-│ ├── 2_run_signal_type_align.py
-│ └── 2_run_mtf_analysis.py
-├── coins.db # 공유 캔들 DB (현물·선물 유일한 공유 리소스)
-├── data/
-│ ├── spot/ # 현물 전용
-│ │ ├── ground_truth/ # GT JSON v1~v3
-│ │ ├── techniques/ # 2단계 기법 실행 결과
-│ │ └── mtf/ # MTF 규칙
-│ └── futures/ # 선물 (0단계 GT만, 1~3단계 추후)
-│ └── ground_truth/
-├── docs/
-│ ├── 0_ground_truth/ # 0단계 (현물·선물 공통)
-│ │ ├── spot/
-│ │ └── futures/
-│ └── spot/ # 현물 1~2단계
-│ ├── 1_simulation/
-│ └── 2_analysis/
-```
+| 유형 | 단계 | 상태 |
+|------|------|------|
+| common | 사전 (캔들) | 구현됨 |
+| spot | 0~2단계 | 구현됨 |
+| spot | 3단계 (운영) | 예정 |
+| futures | 0단계 | 구현됨 |
+| futures | 1~3단계 | 예정 |
-현물·선물은 `coins.db`만 공유합니다. 현재 구현 범위는 **0단계(공통) + 현물 1·2단계**이며, 선물 1~3단계·현물 3단계는 추후 구현 예정입니다.
+---
## 변경 이력
-- 2026-06-11: `docs/0_ground_truth/{spot,futures}/`로 0단계 차트를 docs 직하로 이동
-- 2026-06-11: `scripts/` 접두사를 파이프라인 단계와 일치 (`00_` 사전, `0_` 0단계, `1_` 현물1, `2_` 현물2)
-- 2026-06-11: README 파이프라인 단계별 상세 설명 추가 (0단계 공통 + 현물·선물 1~3단계)
-- 2026-06-11: README 파이프라인을 0단계(공통) + 현물·선물 각 1~3단계 체계로 정리
-- 2026-06-11: 선물 1~3단계·`04_run_causal_futures` 제거, 현물 0·1·2단계만 유지
-- 2026-06-11: `docs/spot/`·`docs/futures/` 상위 분리 (`data/`와 동일 구조)
-- 2026-06-11: `data/spot/`·`data/futures/` 완전 분리 (`coins.db`만 공유), 선물 GT·MTF·causal 독립 경로
-- 2026-06-11: 2단계 38종 기법 리포트 재생성, 3단계 causal/MTF/walkforward 재검증, README 39종 표 반영
-- 2026-06-11: 캔들 수집 기본 동작을 DB 최신 이후 증분 갱신으로 변경 (`--full`로 전체 재수집)
-- 2026-06-10: docs를 단계별 폴더(`0_ground_truth`~`3_causal`)로 재구성, 단계 정의 정렬 (0=GT 타점, 1=sim)
-- 2026-06-09: 파이프라인 번호를 0~4단계 체계로 통일 (0=벤치마크, 1=GT+기법, 2=인과분석, 3=시뮬, 4=실거래)
-- 2026-06-09: 3단계 인과 선물 전략 (composite_v3 + ATR) + 0단계 벤치마크 비교
-- 2026-06-09: v3 신호 유형별 GT 정합 리포트 + composite_v3 통합 기법
-- 2026-06-09: 선물 GT 차트 futures/gt/ 정리, 마커 UI 개선
-- 2026-06-08: Ground Truth v1/v2/v3 + 매매 기법 8종
+- 2026-06-12: `data/`·`docs/`를 **common / spot / futures** 3유형 구조로 재편, `coins.db` → `data/common/`, 0단계 차트 → `docs/{spot,futures}/0_ground_truth/`
+- 2026-06-12: `0_ground_truth_futures.py` — 현물 GT → 선물 JSON·차트 변환 로직 보완
+- 2026-06-12: README 현물 파이프라인 전체 순서 갱신
+- 2026-06-12: `src/deepcoin/data/` 모듈 복원
+- 2026-06-11: 파이프라인 단계별 상세 설명 추가
+- 2026-06-08: Ground Truth v1/v2/v3
- 2026-06-07: 캔들 수집 모듈 초기 구현
diff --git a/data/spot/mtf/mtf_rules_v3.json b/data/spot/mtf/mtf_rules_v3.json
new file mode 100644
index 0000000..ce26f5e
--- /dev/null
+++ b/data/spot/mtf/mtf_rules_v3.json
@@ -0,0 +1,285 @@
+{
+ "version": "v1",
+ "min_rules_pass": 2,
+ "min_cohens_d": 1.2,
+ "max_rules_per_type": 4,
+ "source_report": "2026-06-12 13:47:36",
+ "rules_by_type": {
+ "swing_low": [
+ {
+ "signal_type": "swing_low",
+ "timeframe_label": "3분",
+ "interval_min": 3,
+ "feature": "close_vs_ema60_pct",
+ "operator": "<=",
+ "threshold": -0.8141,
+ "cohens_d": -3.4583,
+ "positive_mean": -1.6348,
+ "negative_mean": 0.0067
+ },
+ {
+ "signal_type": "swing_low",
+ "timeframe_label": "5분",
+ "interval_min": 5,
+ "feature": "close_vs_ema60_pct",
+ "operator": "<=",
+ "threshold": -1.074,
+ "cohens_d": -3.4018,
+ "positive_mean": -2.1584,
+ "negative_mean": 0.0103
+ },
+ {
+ "signal_type": "swing_low",
+ "timeframe_label": "3분",
+ "interval_min": 3,
+ "feature": "ema60_slope_5_pct",
+ "operator": "<=",
+ "threshold": -0.1281,
+ "cohens_d": -3.2318,
+ "positive_mean": -0.2577,
+ "negative_mean": 0.0015
+ },
+ {
+ "signal_type": "swing_low",
+ "timeframe_label": "1분",
+ "interval_min": 1,
+ "feature": "ema60_slope_5_pct",
+ "operator": "<=",
+ "threshold": -0.094,
+ "cohens_d": -3.2067,
+ "positive_mean": -0.1875,
+ "negative_mean": -0.0004
+ }
+ ],
+ "pullback": [
+ {
+ "signal_type": "pullback",
+ "timeframe_label": "1분",
+ "interval_min": 1,
+ "feature": "close_vs_ema60_pct",
+ "operator": "<=",
+ "threshold": -0.21,
+ "cohens_d": -1.9881,
+ "positive_mean": -0.4191,
+ "negative_mean": -0.0009
+ },
+ {
+ "signal_type": "pullback",
+ "timeframe_label": "1분",
+ "interval_min": 1,
+ "feature": "ema60_slope_5_pct",
+ "operator": "<=",
+ "threshold": -0.0361,
+ "cohens_d": -1.9745,
+ "positive_mean": -0.0718,
+ "negative_mean": -0.0004
+ },
+ {
+ "signal_type": "pullback",
+ "timeframe_label": "3분",
+ "interval_min": 3,
+ "feature": "close_vs_ema60_pct",
+ "operator": "<=",
+ "threshold": -0.2996,
+ "cohens_d": -1.7009,
+ "positive_mean": -0.606,
+ "negative_mean": 0.0067
+ },
+ {
+ "signal_type": "pullback",
+ "timeframe_label": "5분",
+ "interval_min": 5,
+ "feature": "rsi14",
+ "operator": "<=",
+ "threshold": 40.0938,
+ "cohens_d": -1.679,
+ "positive_mean": 30.2127,
+ "negative_mean": 49.9748
+ }
+ ],
+ "breakout": [
+ {
+ "signal_type": "breakout",
+ "timeframe_label": "3분",
+ "interval_min": 3,
+ "feature": "bb_position",
+ "operator": ">=",
+ "threshold": 0.8018,
+ "cohens_d": 1.9334,
+ "positive_mean": 1.108,
+ "negative_mean": 0.4955
+ },
+ {
+ "signal_type": "breakout",
+ "timeframe_label": "3분",
+ "interval_min": 3,
+ "feature": "rsi14",
+ "operator": ">=",
+ "threshold": 61.5584,
+ "cohens_d": 1.9135,
+ "positive_mean": 73.1124,
+ "negative_mean": 50.0044
+ },
+ {
+ "signal_type": "breakout",
+ "timeframe_label": "1분",
+ "interval_min": 1,
+ "feature": "rsi14",
+ "operator": ">=",
+ "threshold": 61.5836,
+ "cohens_d": 1.8031,
+ "positive_mean": 73.3036,
+ "negative_mean": 49.8635
+ },
+ {
+ "signal_type": "breakout",
+ "timeframe_label": "5분",
+ "interval_min": 5,
+ "feature": "bb_position",
+ "operator": ">=",
+ "threshold": 0.7618,
+ "cohens_d": 1.6638,
+ "positive_mean": 1.0303,
+ "negative_mean": 0.4933
+ }
+ ],
+ "div_bull": [
+ {
+ "signal_type": "div_bull",
+ "timeframe_label": "1분",
+ "interval_min": 1,
+ "feature": "ema60_slope_5_pct",
+ "operator": "<=",
+ "threshold": -0.0302,
+ "cohens_d": -1.6499,
+ "positive_mean": -0.0601,
+ "negative_mean": -0.0004
+ },
+ {
+ "signal_type": "div_bull",
+ "timeframe_label": "3분",
+ "interval_min": 3,
+ "feature": "close_vs_ema60_pct",
+ "operator": "<=",
+ "threshold": -0.2916,
+ "cohens_d": -1.638,
+ "positive_mean": -0.59,
+ "negative_mean": 0.0067
+ },
+ {
+ "signal_type": "div_bull",
+ "timeframe_label": "5분",
+ "interval_min": 5,
+ "feature": "close_vs_ema60_pct",
+ "operator": "<=",
+ "threshold": -0.3808,
+ "cohens_d": -1.6297,
+ "positive_mean": -0.7718,
+ "negative_mean": 0.0103
+ },
+ {
+ "signal_type": "div_bull",
+ "timeframe_label": "15분",
+ "interval_min": 15,
+ "feature": "rsi14",
+ "operator": "<=",
+ "threshold": 41.5274,
+ "cohens_d": -1.5867,
+ "positive_mean": 32.6455,
+ "negative_mean": 50.4093
+ }
+ ],
+ "swing_high": [
+ {
+ "signal_type": "swing_high",
+ "timeframe_label": "1분",
+ "interval_min": 1,
+ "feature": "ema60_slope_5_pct",
+ "operator": ">=",
+ "threshold": 0.0687,
+ "cohens_d": 3.007,
+ "positive_mean": 0.1378,
+ "negative_mean": -0.0004
+ },
+ {
+ "signal_type": "swing_high",
+ "timeframe_label": "3분",
+ "interval_min": 3,
+ "feature": "close_vs_ema60_pct",
+ "operator": ">=",
+ "threshold": 0.5761,
+ "cohens_d": 2.7548,
+ "positive_mean": 1.1456,
+ "negative_mean": 0.0067
+ },
+ {
+ "signal_type": "swing_high",
+ "timeframe_label": "1분",
+ "interval_min": 1,
+ "feature": "close_vs_ema60_pct",
+ "operator": ">=",
+ "threshold": 0.3562,
+ "cohens_d": 2.7521,
+ "positive_mean": 0.7133,
+ "negative_mean": -0.0009
+ },
+ {
+ "signal_type": "swing_high",
+ "timeframe_label": "5분",
+ "interval_min": 5,
+ "feature": "close_vs_ema60_pct",
+ "operator": ">=",
+ "threshold": 0.7193,
+ "cohens_d": 2.5029,
+ "positive_mean": 1.4283,
+ "negative_mean": 0.0103
+ }
+ ],
+ "div_bear": [
+ {
+ "signal_type": "div_bear",
+ "timeframe_label": "5분",
+ "interval_min": 5,
+ "feature": "rsi14",
+ "operator": ">=",
+ "threshold": 59.09,
+ "cohens_d": 1.5517,
+ "positive_mean": 68.2053,
+ "negative_mean": 49.9748
+ },
+ {
+ "signal_type": "div_bear",
+ "timeframe_label": "3분",
+ "interval_min": 3,
+ "feature": "close_vs_ema60_pct",
+ "operator": ">=",
+ "threshold": 0.2787,
+ "cohens_d": 1.4984,
+ "positive_mean": 0.5507,
+ "negative_mean": 0.0067
+ },
+ {
+ "signal_type": "div_bear",
+ "timeframe_label": "5분",
+ "interval_min": 5,
+ "feature": "close_vs_ema60_pct",
+ "operator": ">=",
+ "threshold": 0.3639,
+ "cohens_d": 1.4787,
+ "positive_mean": 0.7174,
+ "negative_mean": 0.0103
+ },
+ {
+ "signal_type": "div_bear",
+ "timeframe_label": "10분",
+ "interval_min": 10,
+ "feature": "rsi14",
+ "operator": ">=",
+ "threshold": 58.5968,
+ "cohens_d": 1.4392,
+ "positive_mean": 66.8792,
+ "negative_mean": 50.3143
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/docs/spot/2_analysis/stage2_design_guide.md b/docs/spot/2_analysis/stage2_design_guide.md
new file mode 100644
index 0000000..2235e6c
--- /dev/null
+++ b/docs/spot/2_analysis/stage2_design_guide.md
@@ -0,0 +1,315 @@
+# 현물 2단계 설계 가이드
+
+> DeepCoin 현물 파이프라인 2단계(인과 기법 분석)의 목적, 구조, 설계 근거를 정리한 문서입니다.
+> 작성 기준: 2026-06-12 · 기본 TF: 3분봉 · GT: v3
+
+---
+
+## Plan (계획)
+
+### 목적
+
+현물 2단계는 **0단계 Ground Truth(GT) 타점을 인과적으로 재현할 수 있는 매매 기법을 검증·순위화**하고, 3단계 실거래(`composite_v3` + MTF 필터)에 넘길 전략을 확정하는 단계입니다.
+
+| 단계 | 핵심 질문 | 미래 데이터 |
+|------|-----------|-------------|
+| 0단계 | 10년 3분봉에서 이론상 최적 매수/매도는 어디였나? | 사용 (사후 분석, 실거래 불가) |
+| 1단계 | GT를 완벽히 따라가면 수익 상한은? | GT 자체가 사후 타점 |
+| **2단계** | **과거 데이터만으로 GT 타점을 얼마나 맞출 수 있나?** | **미사용 (인과)** |
+| 3단계 (예정) | 검증된 전략을 실거래에 연결 | 실시간 인과 |
+
+### GT v3 신호 유형 (정답지)
+
+0단계 GT v3는 3분봉 `bar_index` 기준으로 아래 6종 신호를 포함합니다.
+
+| 코드 | 유형 | 의미 |
+|------|------|------|
+| B | swing_low | 스윙 매수 |
+| B* | pullback | 눌림목 매수 |
+| B^ | breakout | 돌파 매수 |
+| Bd | div_bull | 상승 다이버전스 매수 |
+| S | swing_high | 스윙 매도 |
+| Sd | div_bear | 하락 다이버전스 매도 |
+
+### 설계 원칙: 3분 = 타이밍, 상위 TF = 맥락
+
+2단계는 **두 레이어**로 나뉩니다.
+
+| 레이어 | 타임프레임 | 역할 |
+|--------|------------|------|
+| **기법 (2-1 ~ 2-3)** | 3분봉 | **언제** 사고팔지 — GT 타점 재현 |
+| **MTF (2-4, 3단계)** | 1분 ~ 월봉 | **그때 해도 되는지** — 과매수·추세·변동성 보조 |
+
+3분봉만 사용하는 것은 상위 TF를 무시한다는 뜻이 **아닙니다**. GT 정답지가 3분봉에 정의되어 있으므로 기법 평가는 동일 TF에서 수행하고, 일/주/월봉 맥락은 MTF 레이어에서 붙입니다.
+
+### 2단계가 하지 **않는** 것
+
+아래는 2단계의 목적과 맞지 않거나, 별도 설계가 필요한 접근입니다.
+
+- 39개 기법을 1분 ~ 월봉 **모든 TF**에 각각 실행
+- **모든 봉**에 시장 상태 라벨을 붙여 매수/매도를 처음부터 탐색
+- GT 없이 상태 분류만으로 전략을 발견
+
+---
+
+## Do (실행)
+
+### 파이프라인 실행
+
+```bash
+cd DeepCoin
+export PYTHONPATH=src
+bash scripts/2_run_stage2_all.sh
+```
+
+### 4단계 세부
+
+| 순서 | 스크립트 | 입력 | 산출물 |
+|------|----------|------|--------|
+| 2-1 | `2_run_techniques.py` | 3분봉 3650일, GT v3 | `data/spot/techniques/*.json`, `comparison_report.html` |
+| 2-2 | `2_run_causal_sim.py` | 2-1 결과 | `causal_sim_report.html`, 기법별 sim 차트 |
+| 2-3 | `2_run_signal_type_align.py` | 2-1 결과 (캐시) | `signal_type_report.html` |
+| 2-4 | `2_run_mtf_analysis.py` | GT v3, 1분~월봉 피처 | `mtf_correlation_report.html`, `data/spot/mtf/mtf_rules_v3.json` |
+
+### 2-1. 기법 실행
+
+**처리 흐름**
+
+1. `coins.db`에서 BTC **3분봉** 3650일 로드 (`GT_INTERVAL_MIN=3`)
+2. 등록된 **39개 인과 기법** 각각 `generate_signals()` 실행
+3. 조건 충족 봉에서만 `buy` / `sell` **이벤트** 생성 (모든 봉에 상태 라벨 X)
+4. 신호를 매수·매도 **레그(leg)** 로 묶고 수익률 계산
+5. GT v3 타점과 **정합(alignment)** 평가 — 기본 허용 오차 ±480봉 (약 24시간)
+
+**정합 지표**
+
+- recall, precision, F1
+- leg recall (매수·매도 쌍 단위)
+- 종합 **score** (터미널 `score=91.1` 등)
+
+**39개 기법 구성**
+
+- 단일 기법 33종: 스윙, 눌림목, 돌파, 다이버전스, 지표, 추세, 모멘텀, 변동성, 구조, 거래량
+- 복합 기법 6종: `composite_swing`, `composite_pullback`, `composite_breakout`, `composite_divergence`, **`composite_v3`**, `composite_full`
+
+`composite_v3`는 v3 GT 6종 신호를 가중 투표로 재현하는 **3단계 실거래 후보 전략**입니다.
+
+### 2-2. 인과 sim
+
+- 2-1에서 저장한 기법 신호를 **최근 3년** (`GT_SIM_LOOKBACK_DAYS=1095`) 구간으로 sim
+- 1단계 GT sim(정답지 수익 상한)과 비교
+- 기법별 HTML sim 차트 생성
+
+### 2-3. 신호 유형별 정합
+
+- GT v3의 B / B* / B^ / Bd / S / Sd **유형별 recall** 분석
+- 예: `ema_pullback`은 B*(눌림목)에 강한가, `rsi_divergence`는 Bd/Sd에 강한가
+- 유형별 최고 recall 기법 리포트
+
+### 2-4. MTF 상관 분석
+
+39기법을 TF마다 다시 돌리지 **않습니다**. 대신:
+
+1. GT v3 **매수/매도 시점**(양성 샘플)에서 1분 ~ 월봉 피처 스냅샷 추출
+2. GT가 아닌 **랜덤 3분봉 시점**(음성 샘플, 기본 2000건)에서 동일 추출
+3. 양성 vs 음성 피처 차이(Cohen's d) 분석
+4. 신호 유형별 MTF **필터 규칙** 도출 → `mtf_rules_v3.json`
+
+**사용 TF** (`DEFAULT_DOWNLOAD_INTERVALS`)
+
+1, 3, 5, 10, 15, 30, 60, 240분, 일(1440), 주(10080), 월(43200)
+
+**TF별 피처** (`src/deepcoin/mtf/features.py`)
+
+| 피처 | 용도 |
+|------|------|
+| `rsi14` | 과매수 / 과매도 |
+| `bb_position` | 볼린저 밴드 내 위치 |
+| `close_vs_ema60_pct` | EMA60 대비 이격 (추세·과열) |
+| `ema60_slope_5_pct` | EMA60 기울기 |
+| `trend_bias` | bullish / bearish |
+| `atr_pct` | 변동성 |
+| `zigzag_direction`, `zigzag_leg_pct` | 인과 ZigZag 상태 |
+
+모든 피처는 **인과적** — 해당 시점에 확정된 봉만 사용, 미래 데이터·미완성 상위 TF 봉 미사용.
+
+### 3단계 연동 (예정)
+
+```
+3분 composite_v3 신호 발생
+ → MtfFeatureExtractor: 그 시점 1분~월봉 스냅샷
+ → HtfTrendGate: 60분·일봉 EMA 이격으로 극단 구간 차단
+ → MtfSignalFilter: 신호 유형별 MTF 규칙 N개 이상 충족
+ → 통과 신호만 실거래
+```
+
+**고TF 추세 게이트** (`HtfTrendGate`) 기본값
+
+| 조건 | 동작 |
+|------|------|
+| 일봉 EMA60 대비 -25% 이하 | 매수 차단 (과매도·칼날) |
+| 60분 EMA60 대비 -15% 이하 | 매수 차단 |
+| 일봉 EMA60 대비 +35% 이상 | 매도 차단 (과열) |
+| 60분 EMA60 대비 +20% 이상 | 매도 차단 |
+
+### 데이터 흐름
+
+```mermaid
+flowchart TD
+ GT["0단계 GT v3\n(3분봉 정답 타점)"]
+ Candles["3분봉 3650일\n(coins.db)"]
+
+ subgraph step21 ["2-1 기법 실행"]
+ T["39개 인과 기법"]
+ T --> Sig["매수/매도 신호"]
+ Sig --> Align["GT 정합 score"]
+ end
+
+ subgraph step22 ["2-2 sim"]
+ Sim["최근 3년 수익률"]
+ end
+
+ subgraph step23 ["2-3 신호유형"]
+ Type["B/B*/B^/Bd/S/Sd별 recall"]
+ end
+
+ subgraph step24 ["2-4 MTF"]
+ MTF["1분~월봉 피처"]
+ MTF --> Rules["mtf_rules_v3.json"]
+ end
+
+ subgraph step3 ["3단계 (예정)"]
+ Filter["composite_v3 + MtfSignalFilter"]
+ end
+
+ GT --> Align
+ Candles --> step21
+ Align --> step22
+ Align --> step23
+ GT --> step24
+ MTF --> Rules
+ Rules --> Filter
+ Sig --> Filter
+```
+
+---
+
+## Check (검토)
+
+### 접근 방식 비교
+
+#### A. 현재 방식 (채택)
+
+3분봉 × 39기법 GT 정합 + 4단계 MTF 상관 분석
+
+#### B. 대안: 모든 기법 × 모든 TF × 모든 봉 상태
+
+| 구분 | A: 현재 2단계 | B: 전 TF·전 봉 상태 | C: 절충 (선택 실험) |
+|------|---------------|---------------------|---------------------|
+| **품질 (Q)** | GT와 동일 TF·bar_index로 평가 **명확** | TF마다 신호 의미 상이, **평가 기준 혼란** | 핵심 기법만 상위 TF 추가 검증 |
+| **비용 (C)** | 39기법 × 1 TF ≈ 1~2시간 | 39 × 11 TF ≈ 10~20시간+, 봉별 상태 저장 부담 큼 | +2~3 TF 시 +30~50% |
+| **일정 (D)** | 파이프라인·3단계 **구현 완료** | 정합·스키마·리포트 **전면 재설계** | 기존 유지 + 실험 브랜치 |
+| **리스크 (R)** | 단일 TF 편향 (MTF로 보완) | 차원 폭발 → **과적합** | 실험 범위 제한으로 통제 |
+| **장점 (S)** | 인과·실거래·GT 정합 **일관** | TF별 탐색적 발견 | A 명확성 + B 인사이트 일부 |
+| **단점 (W)** | 2-1 단독으론 상위 TF 안 보임 | GT(3분)와 **축 불일치** | 실험 설계 부담 |
+| **비고** | **현 단계 최적** | 2단계 **대체안 부적합** | 장기 개선 1순위 |
+
+**결론: A(현재 방식) 유지. 필요 시 C로 확장.**
+
+### 3분봉만으로 상위 TF 과매수/과매도 판단이 어렵지 않은가?
+
+**맞습니다.** 3분봉 신호만 따르면:
+
+- 일봉 RSI 과매수 구간 3분 눌림목 매수 → 역추세 진입
+- 주봉 하락 추세 3분 돌파 매수 → 가짜 돌파
+- 월봉 과매도 3분 매도 → 바닥 청산
+
+등의 문제가 발생합니다. DeepCoin은 이를 **MTF 레이어**로 보완합니다.
+
+| 상황 | MTF 해석 | 의도 |
+|------|----------|------|
+| 3분 B* + 주봉 RSI 과매도 + 일봉 trend bullish | MTF 규칙 다수 충족 | 적극 매수 (3단계 확장) |
+| 3분 B* + 일봉 RSI 과매수 | 게이트/규칙 미충족 | 보류 |
+| 3분 S + 월봉 과열 + 60분 기울기 꺾임 | 매도 규칙 충족 | 적극 매도 (3단계 확장) |
+
+현재 구현은 **차단(필터)** 중심이며, MTF 점수 기반 **포지션 크기 조절**은 3단계에서 확장 가능합니다.
+
+### KPI · 확인 방법
+
+| KPI | 확인 위치 | 기준 예시 |
+|-----|-----------|-----------|
+| GT 정합 score | `comparison_report.html` | composite_v3 상위권 |
+| leg recall | `data/spot/techniques/*.json` | 70% 이상 (기법별 상이) |
+| sim 수익률 (3년) | `causal_sim_report.html` | 1단계 GT sim 대비 합리적 |
+| 신호 유형 recall | `signal_type_report.html` | B/B*/B^/Bd/S/Sd 유형별 1위 기법 |
+| MTF Cohen's d | `mtf_correlation_report.html` | \|d\| ≥ 1.2 규칙 후보 |
+| MTF 규칙 통과율 | 3단계 `MtfSignalFilter` | kept/rejected 비율 |
+
+### 주요 환경 변수
+
+| 변수 | 설명 | 기본값 |
+|------|------|--------|
+| `GT_INTERVAL_MIN` | GT·기법 기준 인터벌(분) | `3` |
+| `GT_LOOKBACK_DAYS` | GT·기법 기간(일) | `3650` |
+| `GT_SIM_LOOKBACK_DAYS` | sim·MTF 분석 구간(일) | `1095` |
+| `GT_ALIGN_TOLERANCE_BARS` | GT 정합 허용 봉 수 | `480` |
+| `TECHNIQUES_DIR` | 기법 결과 JSON | `data/spot/techniques/` |
+| `MTF_RULES_JSON` | MTF 규칙 | `data/spot/mtf/mtf_rules_v3.json` |
+
+---
+
+## Act (개선)
+
+### 현재 한계
+
+| 항목 | 상태 | 비고 |
+|------|------|------|
+| 2-1 ~ 2-3 | MTF 미적용 | 의도적 — GT 정합 단계 |
+| `HtfTrendGate` | 60분·일봉만 | 주·월봉 RSI/BB 게이트 추가 가능 |
+| MTF 필터 | 통과/실패 이진 | 점수 기반 사이즈 조절 (3단계) |
+| 2-4 → rules JSON | `derive_rules_from_report()` 존재 | 2-4 스크립트 자동 저장 연동 검토 |
+
+### 권장 개선 (절충 C)
+
+2단계 골격은 유지하고, 아래만 **별도 실험**으로 진행합니다.
+
+1. **TF별 핵심 기법**: 스윙 5종 + `composite_v3`를 60분·일봉에 추가 실행 — 상위 TF 스윙과 3분 GT 정합 비교
+2. **MTF 4단계 강화**: 음성 샘플 수, 피처, `min_cohens_d` 조정
+3. **주·월봉 게이트**: `HtfTrendGate`에 RSI/BB 기반 조건 추가
+4. **레짐 라벨 모듈**: EMA/ADX 등 봉별 장세 라벨 — 39기법 전 TF 재실행 없이 보조
+
+### 하지 말아야 할 것
+
+- 39기법 × 11 TF × 175만 3분봉 상태를 2단계 **기본 산출물**로 포함
+- GT(3분)와 다른 TF 신호를 **동일 tolerance**로 무조건 비교
+
+### 실행 체크리스트
+
+- [ ] 0단계 GT v3 생성 완료 (`0_ground_truth.py --tier all`)
+- [ ] 1단계 GT sim 완료 (벤치마크 참조용)
+- [ ] `bash scripts/2_run_stage2_all.sh` 완료
+- [ ] `comparison_report.html` — 기법 순위 확인
+- [ ] `signal_type_report.html` — 유형별 강점 기법 확인
+- [ ] `mtf_correlation_report.html` — 일/주/월 RSI·EMA 패턴 확인
+- [x] `mtf_rules_v3.json` — 3단계 필터 규칙 반영 (`data/spot/mtf/mtf_rules_v3.json`)
+
+---
+
+## 참고: 관련 소스
+
+| 모듈 | 경로 |
+|------|------|
+| 기법 실행 | `scripts/2_run_techniques.py`, `src/deepcoin/techniques/runner.py` |
+| GT 정합 | `src/deepcoin/evaluation/gt_align.py` |
+| MTF 피처 | `src/deepcoin/mtf/features.py`, `extractor.py`, `store.py` |
+| MTF 규칙 | `src/deepcoin/mtf/rules.py` |
+| MTF 필터 | `src/deepcoin/mtf/filter.py`, `trend_gate.py` |
+| 통합 기법 | `src/deepcoin/techniques/composite_v3.py` |
+
+---
+
+## 변경 이력
+
+| 날짜 | 내용 |
+|------|------|
+| 2026-06-12 | 초版 작성 — 2단계 목적, 4단계 구조, MTF 보완, 접근 방식 비교 정리 |
diff --git a/docs/spot/2_analysis/stage2_final_summary.md b/docs/spot/2_analysis/stage2_final_summary.md
new file mode 100644
index 0000000..df85f67
--- /dev/null
+++ b/docs/spot/2_analysis/stage2_final_summary.md
@@ -0,0 +1,290 @@
+# 현물 2단계 최종 정리 — 결과 해석 및 운영 권고
+
+> DeepCoin 현물 파이프라인 2단계(인과 기법 분석) 완료 후 종합 정리 문서
+> 작성 기준: 2026-06-12 · 데이터: BTC · 3분봉 · GT v3 · 분석 기간 3650일 · sim 기간 최근 3년(1095일)
+
+---
+
+## 요약 (Executive Summary)
+
+| 질문 | 결론 |
+|------|------|
+| **sim 1위 `fractal_swing`을 바로 실거래에 써도 되나?** | **아니요.** 연구·벤치마크용으로는 유효하나, 체결 빈도·비용·슬리피지를 고려하면 실거래 부적합 |
+| **2단계에서 무엇을 얻었나?** | GT v3 타점을 **인과적으로** 재현하는 기법 39종의 **정합 순위**, **3년 sim 비교**, **신호 유형별 강점**, **MTF 상관 패턴** |
+| **운영 후보는?** | 원안 **`composite_v3` + MTF 필터(3단계)** — 단, 현재 sim 기준 composite_v3는 **-97.5%**로 **3단계 튜닝 전 실거래 금지** |
+| **당장 관찰용으로 쓸 만한 기법** | `zigzag_causal`(3년 sim +92,711%, 체결 97회), `minor_swing`(+286,537%, 831회) — **모의·소액 검증 후** 판단 |
+
+---
+
+## Plan (계획) — 2단계가 무엇을 했는가
+
+### 목적
+
+0단계에서 도출한 **Ground Truth v3(사후 최적 타점)** 을, **미래 데이터 없이** 인과 규칙만으로 얼마나 재현할 수 있는지 검증하고, 3단계 실거래 전략의 근거를 마련하는 단계입니다.
+
+| 단계 | 역할 | 미래 데이터 |
+|------|------|-------------|
+| 0단계 | 10년 3분봉 이론적 최적 매수/매도(GT v3) | 사용 (연구용) |
+| 1단계 | GT 타점을 그대로 sim했을 때 3년 수익 **벤치마크** | GT 자체가 사후 타점 |
+| **2단계** | **39개 인과 기법**으로 GT 재현도·sim·MTF 분석 | **미사용** |
+| 3단계 (예정) | 검증된 전략 + MTF 필터 실거래 연결 | 실시간 인과 |
+
+### GT v3 신호 체계 (정답지)
+
+| 코드 | 유형 | 10년 GT 건수(매수/매도) |
+|------|------|-------------------------|
+| B | swing_low (스윙 매수) | 944 |
+| B* | pullback (눌림목) | 406 |
+| B^ | breakout (돌파) | 122 |
+| Bd | div_bull (상승 다이버전스) | 115 |
+| S | swing_high (스윙 매도) | 944 |
+| Sd | div_bear (하락 다이버전스) | 144 |
+
+### 실행 구조 (2-1 ~ 2-4)
+
+| 순서 | 내용 | 주요 산출물 |
+|------|------|-------------|
+| 2-1 | 39개 기법 신호 생성 + GT 정합 score | `data/spot/techniques/*.json`, `comparison_report.html` |
+| 2-2 | 동일 sim 엔진으로 최근 3년 수익률 비교 | `causal_sim_report.html`, `causal_sim_chart_best_technique.html` |
+| 2-3 | B/B*/B^/Bd/S/Sd 유형별 recall | `signal_type_report.html` |
+| 2-4 | GT 시점 vs 랜덤 시점 MTF 피처 상관 | `mtf_correlation_report.html` |
+
+설계 상세는 [`stage2_design_guide.md`](stage2_design_guide.md)를 참고하세요.
+
+---
+
+## Do (실행) — 핵심 결과
+
+### 1. GT 정합 score 상위 (2-1, 10년 전체)
+
+정합 허용 오차: **±480봉(약 24시간)**. score는 recall·precision·leg recall 등을 종합한 0~1 지표입니다.
+
+| 순위 | 기법 | score | buy/sell recall | leg recall | 비고 |
+|------|------|-------|-----------------|------------|------|
+| 1 | **fractal_swing** | **0.914** | 100% / 100% | 75.3% | 프랙탈(span=2) 극저점·극고점 |
+| 2 | **pivot_swing** | **0.911** | 100% / 100% | 74.6% | 피벗 기반 스윙 |
+| 3 | **minor_swing** | **0.864** | 87.9% / 95.0% | 73.2% | 소형 스윙 하이브리드 |
+| 4 | local_extrema | 0.839 | 86.3% / 90.7% | 70.6% | 국소 극값 |
+| 5 | **zigzag_causal** | **0.776** | 59.9% / 86.8% | 74.2% | 인과 ZigZag — **스윙(B/S)에 특화** |
+| … | composite_v3 | 0.546 | 97.5% / 89.0% | **22.9%** | 3단계 운영 후보이나 leg 정합 낮음 |
+
+**해석:** 상위 3종은 모두 **스윙 타이밍** 기법입니다. `composite_v3`는 개별 신호 recall은 높지만 **매수·매도 쌍(leg) 정합이 22.9%**에 그쳐, “타점은 근처에 있으나 한 사이클로 묶기 어렵다”는 특성이 있습니다.
+
+### 2. 3년 인과 sim (2-2) — 1단계 GT v3 벤치마크 대비
+
+- **sim 기간:** 2023-06-12 ~ 2026-06-11 (1095일)
+- **초기 자본:** 200,000원
+- **엔진:** 1단계와 동일 `simulate_gt_signals_pnl` (클러스터 분할, 매수 상한, 수수료 반영, 슬리피지 미반영)
+
+#### 1단계 벤치마크
+
+| 항목 | 1단계 GT v3 sim |
+|------|-----------------|
+| 3년 수익률 | **+94,154%** |
+| 최종 평가 | 약 1.89억 원 |
+| 체결 | 매수 239 / 매도 151 |
+| 기간 내 신호 | 390건 |
+
+#### 2단계 sim 상위
+
+| 순위 | 기법 | 3년 sim 수익률 | 최종 평가 | 매수/매도 체결 | GT 정합 |
+|------|------|----------------|-----------|----------------|---------|
+| 1 | fractal_swing | **+7,560,826%** | 약 151억 | **56,893 / 56,892** | 0.914 |
+| 2 | pivot_swing | +4,687,495% | 약 94억 | 12,656 / 12,658 | 0.911 |
+| 3 | minor_swing | +286,537% | 약 5.7억 | 831 / 887 | 0.864 |
+| 4 | keltner_reversal | +203,632% | 약 4.1억 | 26,554 / 24,839 | 0.723 |
+| … | **zigzag_causal** | **+92,711%** | 약 1.86억 | **97 / 97** | 0.776 |
+| … | **composite_v3** | **-97.5%** | 약 5,000원 | 1,885 / 1,237 (스킵 다수) | 0.546 |
+
+**일평균 체결 빈도(매수 기준, 3년):**
+
+| 기법 | 연간 약 | 일평균 약 |
+|------|---------|-----------|
+| fractal_swing | 18,964회 | **52회/일** |
+| pivot_swing | 4,219회 | 12회/일 |
+| minor_swing | 277회 | 0.8회/일 |
+| zigzag_causal | 32회 | **0.09회/일** |
+| GT v3 (1단계) | 80회 | 0.2회/일 |
+
+### 3. sim 1위가 GT 벤치마크보다 높은 이유 (핵심 메커니즘)
+
+동일 sim 엔진임에도 수익률이 역전되는 이유는 **전략 품질이 아니라 체결 구조** 때문입니다.
+
+| 요인 | GT v3 (1단계 sim) | fractal_swing (sim 1위) |
+|------|-------------------|-------------------------|
+| 신호 수 (3년) | 390 | **113,786** |
+| 클러스터 | 평균 1.62신호/클러스터, 다중 매수 분할 | **99.7%가 1신호=1체결** |
+| 매매 패턴 | 드문 타점, 보수적 분할·상한 | **매수·매도 거의 매 스윙마다 교대** |
+| 수수료 (3년 sim) | 상대적으로 적음 | **약 65억 원** (이상적 체결 가정) |
+| sim의 의미 | “최적 타점을 보수적으로 따라감” | “초고빈도 복리 + 상승장 + 슬리피지 없음” |
+
+**정리:** fractal의 높은 sim 수익률은 **GT 재현 우수성의 증거가 아니라**, tolerance(±24시간) 안에서 **모든 미세 스윙이 GT에 걸리는 구조**와 **과매매 복리**가 합쳐진 **백테스트 착시**에 가깝습니다.
+
+### 4. 신호 유형별 정합 (2-3) — v3 6종 커버리지
+
+#### fractal_swing — tolerance 내 “전 유형 100% recall”
+
+±480봉 허용 시 스윙·눌림·돌파·다이버전스 **모든 GT 유형에 recall 100%**로 집계됩니다. 이는 각 유형을 **정확히 구분해 맞춘다**는 뜻이 아니라, **3분봉 미세 스윙이 24시간 안에 GT 타점과 겹친다**는 통계적 결과입니다.
+
+#### zigzag_causal — 스윙 특화, 복합 유형 약함
+
+| GT 유형 | zigzag recall | 해석 |
+|---------|---------------|------|
+| B (swing_low) | **100%** | 스윙 매수에 최적 |
+| S (swing_high) | **100%** | 스윙 매도에 최적 |
+| B* (pullback) | 38.4% | 눌림목은 별도 기법 필요 |
+| B^ (breakout) | 15.6% | 돌파 약함 |
+| Bd (div_bull) | 4.4% | 다이버전스 거의 미포착 |
+| Sd (div_bear) | 2.8% | 다이버전스 거의 미포착 |
+
+→ v3 **6종 전체**를 한 기법으로 운영하려면 **스윙 + 눌림 + 돌파 + 다이버전스**를 조합한 `composite_v3` 또는 유형별 전문 기법 조합이 필요합니다.
+
+#### composite_v3 (3단계 설계 후보)
+
+- 10년 정합 score **0.546** (39종 중 하위권)
+- leg recall **22.9%** — 신호는 많지만 **한 사이클(매수→매도) 단위 정합 낮음**
+- 3년 sim **-97.5%**, buys_skipped **6,090건** — 신호 과다 + 자본·상한·클러스터 규칙과 충돌
+
+### 5. MTF 상관 분석 (2-4)
+
+GT v3 **매수/매도 시점(양성)** vs **랜덤 3분봉(음성)** 에서 1분~월봉 피처를 비교했습니다.
+
+**대표 패턴 (|Cohen's d| 큰 항목 예시):**
+
+| 상황 | TF | 피처 | 방향 |
+|------|-----|------|------|
+| 스윙 매도(S) | 15분 | RSI14 | GT 시점이 랜덤보다 RSI 높음 (과매수 근처) |
+| 눌림목(B*) | 3분 | close_vs_ema60_pct | GT 눌림이 EMA60 대비 더 아래 |
+| 스윙 매수(B) | 30분 | ema60_slope | GT 매수 시 기울기가 상대적으로 완만/하락 |
+
+**의미:** 3분 신호만으로는 부족한 **상위 TF 과열·추세** 정보가 GT 타점과 통계적으로 연관됩니다. 3단계 `HtfTrendGate` + `MtfSignalFilter`로 보완하는 설계가 타당합니다.
+
+**현재 상태:** `mtf_correlation_report.html`·`.json`은 생성됨. `data/spot/mtf/mtf_rules_v3.json`은 **아직 자동 저장 미연동** — 3단계 전 규칙 JSON 확정·연동 필요.
+
+---
+
+## Check (검토) — “가장 좋은 기법”을 운영에 써도 되는가?
+
+### 결론: **fractal_swing 단독 실거래는 권장하지 않습니다**
+
+| 검토 항목 | fractal_swing | 실거래 적합성 |
+|-----------|---------------|---------------|
+| 체결 빈도 | 일 **약 52회** 매수 | 거래소 API·수수료·세금·운영 부담 과다 |
+| 슬리피지 | sim **미반영** | 고빈도일수록 체결 가격 악화 누적 |
+| 수수료 | 3년 sim만 **약 65억** 가정 | 실제로도 수익 잠식 극심 |
+| 신호 의미 | 미세 스윙 전부 | 노이즈·휩소에 취약 |
+| GT 정합 | score 최상 | **±24h tolerance** 효과 — “정확한 v3 타입 재현”과 다름 |
+| 3단계 설계 | 원안은 composite_v3 + MTF | fractal은 **연구 1위**, **운영 1안 아님** |
+
+**가능한 예외 (제한적):**
+
+- **연구·모니터링:** 차트·알림으로 “스윙 후보” 참고용
+- **극소액 실험:** 일 체결 상한(예: 1~2회/일), 포지션 캡, 슬리피지 가정 추가 후 **모의거래 3개월 이상**
+
+### 운영 후보 비교 (QCD)
+
+| 구분 | A. fractal_swing 단독 | B. composite_v3 + MTF (원안) | C. zigzag + 유형별 보조 + MTF | D. minor_swing + MTF |
+|------|----------------------|------------------------------|------------------------------|----------------------|
+| **Q (품질)** | 스윙 타이밍만, 유형 혼동 | v3 6종 통합 의도 | B/S 강함, B*/B^/Bd 약 — 보조 기법 필요 | 스윙+하이브리드, 균형 |
+| **C (비용)** | 수수료·API 호출 **최대** | 신호 필터 후 감소 예정 | **낮음** (연 ~32매수) | **중간** (연 ~277매수) |
+| **D (일정)** | 즉시 가능하나 **리스크 최대** | 3단계 구현·튜닝 필요 | MTF·모의 1~2개월 | MTF·모의 1~2개월 |
+| **R (리스크)** | 과매매·슬리피지·장애 **극高** | 현재 sim -97.5% — **튜닝 전 위험** | 신호 적어 기회 손실 | 중간 빈도 휩소 |
+| **S (장점)** | GT 정합 1위, 구현 단순 | v3 철학과 일치, 확장성 | **GT sim과 유사 체결(97회)**, 해석 용이 | 정합 3위, sim 양호 |
+| **W (단점)** | 실거래 **비현실** | leg recall 낮음, 과다 스킵 | 6종 중 4종 약함 | pivot/fractal 대비 정합 낮음 |
+| **비고** | **운영 부적합** | **장기 정석**(튜닝 후) | **단기 모의 1순위** | 모의 2순위 |
+
+**최종 의견:**
+- **지금 당장 실거래:** 위 네 안 모두 **완료 전제 미충족**. 최소 **3단계 MTF 필터 + 모의거래 + composite/조합 튜닝** 후 소액.
+- **2단계 성과의 올바른 사용:** sim 1위 기법을 “운영 전략”으로 고르지 말고, **정합·유형·체결 빈도·MTF**를 함께 보고 **3단계 설계 입력**으로 사용.
+- **단기 모의 우선순위:** **C (zigzag_causal + MTF)** → **D (minor_swing + MTF)**.
+- **중장기 운영 정석:** **B (composite_v3 + MTF)** — 임계값·스킵 로직·leg 정합 개선 후.
+
+### KPI 달성 여부 (2단계 관점)
+
+| KPI | 목표(가이드) | 결과 | 판정 |
+|-----|--------------|------|------|
+| 인과 기법 39종 실행 | 완료 | 완료 | 달성 |
+| GT 정합 ranking | 상위 기법 식별 | fractal/pivot/minor | 달성 |
+| 1단계 sim 대비 3년 비교 | 해석 가능한 보고 | causal_sim_report | 달성 (단, sim만으로 운영 선정 금지) |
+| 유형별 recall | B~Sd별 1위 기법 | signal_type_report | 달성 |
+| MTF Cohen's d | 필터 규칙 후보 | mtf_correlation_report | 달성 |
+| mtf_rules_v3.json | 3단계 입력 | `data/spot/mtf/mtf_rules_v3.json` (6유형×4규칙) | 달성 |
+
+---
+
+## Act (개선) — 3단계로 넘기기 전 체크리스트
+
+### 하지 말아야 할 것
+
+1. **causal_sim_report 수익률 1위 = 실거래 전략**으로 간주
+2. **fractal_swing 풀오토** (일 50회+ 체결)
+3. **composite_v3 미튜닝 실거래** (현재 3년 sim -97.5%)
+4. **0단계 전기간 GT 수익**과 2단계 3년 sim **직접 비교** (sim 규칙이 다름)
+
+### 권장 다음 단계
+
+| 순서 | 작업 | 목적 |
+|------|------|------|
+| 1 | `mtf_rules_v3.json` 생성·`MtfSignalFilter` 연동 | 3단계 필터 기반 확보 |
+| 2 | `composite_v3` 임계값·스킵 원인 분석 (buys_skipped 6,090) | leg 정합·자본 효율 개선 |
+| 3 | **zigzag_causal** + MTF **모의거래** (슬리피지 0.05~0.1% 가정) | 현실적 체결 검증 |
+| 4 | 유형별 보조: B*→ema_pullback, B^→donchian, Bd→rsi_divergence 등 **composite 재가중** | v3 6종 커버리지 |
+| 5 | sim에 **슬리피지·일 최대 체결 횟수** 옵션 추가 | 고빈도 기법 과대평가 방지 |
+| 6 | 소액 실거래 전 **최소 3개월 paper trading** KPI | MDD, 승률, 실체결률 |
+
+### 관련 산출물 위치
+
+| 문서/데이터 | 경로 |
+|-------------|------|
+| 기법 정합 순위 | `docs/spot/2_analysis/comparison_report.html` |
+| 3년 sim 순위 | `docs/spot/2_analysis/causal_sim_report.html` |
+| 1위 vs 1단계 GT 비교 차트 | `docs/spot/2_analysis/causal_sim_chart_best_technique.html` |
+| 신호 유형별 recall | `docs/spot/2_analysis/signal_type_report.html` |
+| MTF 상관 | `docs/spot/2_analysis/mtf_correlation_report.html` |
+| 기법별 sim 차트 (39종) | `docs/spot/2_analysis/technique_chart_sim_*.html` |
+| 기법 JSON | `data/spot/techniques/*.json` |
+| 2단계 설계 가이드 | `docs/spot/2_analysis/stage2_design_guide.md` |
+
+---
+
+## 부록 A — 지표 읽는 법
+
+### GT 정합 score vs sim 수익률
+
+- **score:** “GT 타점 **근처**에 인과 신호가 있었는가” (위치 품질)
+- **sim 수익률:** “그 신호를 **현재 체결 규칙**으로 얼마나 자주·얼마나 크게 탔는가” (빈도·복리)
+
+두 지표는 **상관이 없을 수 있음**. fractal이 대표 사례.
+
+### 1단계 GT sim이 “상한”이 아닌 이유
+
+- 클러스터 **분할 매수** (v3 평균 매수 클러스터 ~2신호)
+- **매수 상한** (총평가 1억/10억/100억 구간별 10%/5%/1%)
+- 3년 구간 **신호 390건**에 한정
+
+0단계 `simulate_gt_pnl`(레그당 전액 in/out)과는 다른 척도입니다.
+
+### fractal이 모든 signal_type recall 100%인 이유
+
+정합 tolerance **±480봉(24시간)**. 3분봉 fractal은 하루에도 수십 개 스윙을 내므로, GT의 스윙·눌림·돌파·다이버전스 타점 대부분이 **시간 창 안에至少 하나의 fractal 신호**와 매칭됩니다. **유형 분류 정확도**와 혼동하지 말 것.
+
+---
+
+## 부록 B — 3년 sim 참고 표 (체결 빈도별)
+
+| 기법 | sim 수익률 | 매수 체결 | GT 정합 | 운영 관점 |
+|------|-------------|-----------|---------|-----------|
+| fractal_swing | +7,560,826% | 56,893 | 0.914 | 연구용, 실거래 비권장 |
+| pivot_swing | +4,687,495% | 12,656 | 0.911 | 고빈도, 실거래 비권장 |
+| minor_swing | +286,537% | 831 | 0.864 | 모의 후보 |
+| zigzag_causal | +92,711% | 97 | 0.776 | **모의 1순위** (GT sim과 유사 빈도) |
+| GT v3 (1단계) | +94,154% | 239 | — | 벤치마크 |
+| composite_v3 | -97.5% | 1,885 | 0.546 | 3단계 튜닝 필수 |
+
+---
+
+## 변경 이력
+
+| 날짜 | 내용 |
+|------|------|
+| 2026-06-12 | 2단계 완료 후 최종 정리 — 운영 권고, sim 역전 해석, 3단계 체크리스트 |
diff --git a/scripts/0_ground_truth_futures.py b/scripts/0_ground_truth_futures.py
index b754fee..9fb1e08 100644
--- a/scripts/0_ground_truth_futures.py
+++ b/scripts/0_ground_truth_futures.py
@@ -7,6 +7,7 @@ import argparse
import json
import logging
import sys
+from copy import deepcopy
from pathlib import Path
from typing import Any
@@ -16,7 +17,9 @@ if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC))
from deepcoin.config import Settings, load_settings
+from deepcoin.ground_truth.futures import futures_events_from_gt_signals
from deepcoin.ground_truth.futures_chart import render_futures_ground_truth_chart
+from deepcoin.ground_truth.ground_truth import save_ground_truth
TIER_DESCRIPTIONS = {
"v1": "스윙만 (최소 매수·매도)",
@@ -35,18 +38,24 @@ def _configure_logging(verbose: bool) -> None:
)
-def _tier_targets(settings: Settings, tier_arg: str) -> list[tuple[str, Path, Path]]:
- """생성할 티어 목록 (tier, futures_json_path, futures_chart_path)."""
- all_tiers: dict[str, tuple[Path, Path]] = {
+def _tier_targets(
+ settings: Settings,
+ tier_arg: str,
+) -> list[tuple[str, Path, Path, Path]]:
+ """생성할 티어 목록 (tier, spot_json, futures_json, futures_chart)."""
+ all_tiers: dict[str, tuple[Path, Path, Path]] = {
"v1": (
+ settings.ground_truth_v1_file,
settings.ground_truth_futures_v1_file,
settings.ground_truth_futures_chart_v1_file,
),
"v2": (
+ settings.ground_truth_v2_file,
settings.ground_truth_futures_v2_file,
settings.ground_truth_futures_chart_v2_file,
),
"v3": (
+ settings.ground_truth_file,
settings.ground_truth_futures_file,
settings.ground_truth_futures_chart_v3_file,
),
@@ -57,29 +66,48 @@ def _tier_targets(settings: Settings, tier_arg: str) -> list[tuple[str, Path, Pa
def _load_gt(json_path: Path) -> dict[str, Any]:
- """선물 GT JSON을 로드한다."""
+ """GT JSON을 로드한다."""
with json_path.open(encoding="utf-8") as fp:
return json.load(fp)
-def _print_summary(tier: str, gt_result: dict[str, Any], chart_path: Path) -> None:
- """티어별 선물 차트 요약을 출력한다."""
+def _build_futures_gt(spot_gt: dict[str, Any]) -> dict[str, Any]:
+ """현물 GT JSON을 선물 GT JSON으로 변환한다."""
+ signals = spot_gt.get("signals") or []
+ futures_gt = deepcopy(spot_gt)
+ futures_gt["meta"] = {
+ **spot_gt.get("meta", {}),
+ "market_type": "futures",
+ "source": "spot_ground_truth",
+ }
+ futures_gt["futures_events"] = futures_events_from_gt_signals(signals)
+ return futures_gt
+
+
+def _print_summary(
+ tier: str,
+ gt_result: dict[str, Any],
+ json_path: Path,
+ chart_path: Path,
+) -> None:
+ """티어별 선물 GT 요약을 출력한다."""
meta = gt_result["meta"]
summary = gt_result["summary"]
- print(f"\n=== 선물 GT 차트 {tier.upper()} ({TIER_DESCRIPTIONS[tier]}) ===")
+ print(f"\n=== 선물 GT {tier.upper()} ({TIER_DESCRIPTIONS[tier]}) ===")
print(f"대상: {meta['symbol']} ({meta['interval_label']})")
print(f"GT 기간: {meta['data_from']} ~ {meta['data_to']}")
print(
f"선물 GT 타점: 매수 {summary['buy_count']} / 매도 {summary['sell_count']} "
f"→ 선물 상방·하방 각 {summary['buy_count']}/{summary['sell_count']} 마커"
)
+ print(f"JSON: {json_path}")
print(f"차트: {chart_path}")
def main() -> int:
"""CLI 진입점."""
parser = argparse.ArgumentParser(
- description="선물 GT JSON 기반 Ground Truth 차트 (롱·숏 4색)"
+ description="선물 GT JSON·차트 생성 (현물 GT → 롱·숏 4색)"
)
parser.add_argument(
"--tier",
@@ -94,23 +122,29 @@ def main() -> int:
settings = load_settings()
tiers = _tier_targets(settings, args.tier)
- print("\n=== 선물 Ground Truth 차트 생성 ===")
+ print("\n=== 선물 Ground Truth 생성 ===")
print("현물 GT 타점 → L↑상방매수 L↓상방매도 S↓하방매수 S↑하방매도")
- for tier, json_path, chart_path in tiers:
- if not json_path.exists():
- logging.error("현물 GT JSON 없음: %s — 먼저 0_ground_truth.py 실행", json_path)
+ for tier, spot_json_path, futures_json_path, chart_path in tiers:
+ if not spot_json_path.exists():
+ logging.error(
+ "현물 GT JSON 없음: %s — 먼저 0_ground_truth.py 실행",
+ spot_json_path,
+ )
return 1
- gt_result = _load_gt(json_path)
+ spot_gt = _load_gt(spot_json_path)
+ futures_gt = _build_futures_gt(spot_gt)
+ save_ground_truth(futures_gt, futures_json_path)
+
render_futures_ground_truth_chart(
db_path=settings.db_path,
symbol=settings.symbol,
- gt_result=gt_result,
+ gt_result=spot_gt,
output_path=chart_path,
chart_lookback_days=settings.download_days,
)
- _print_summary(tier, gt_result, chart_path)
+ _print_summary(tier, futures_gt, futures_json_path, chart_path)
return 0
diff --git a/scripts/2_render_best_technique_chart.py b/scripts/2_render_best_technique_chart.py
new file mode 100644
index 0000000..fd5bfd2
--- /dev/null
+++ b/scripts/2_render_best_technique_chart.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+"""2단계: 수익률 1위 기법 sim 차트 (1단계 v3 sim과 동일 형식·기간)."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import logging
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+SRC = ROOT / "src"
+if str(SRC) not in sys.path:
+ sys.path.insert(0, str(SRC))
+
+from deepcoin.config import load_settings
+from deepcoin.data.candle_loader import load_candles
+from deepcoin.evaluation.causal_sim import (
+ best_technique_chart_path,
+ pick_best_technique_row,
+ render_best_technique_comparison_chart,
+ run_technique_causal_sim,
+)
+from deepcoin.techniques.runner import load_ground_truth, load_technique_result
+
+
+def _configure_logging(verbose: bool) -> None:
+ """로깅 레벨을 설정한다."""
+ level = logging.DEBUG if verbose else logging.INFO
+ logging.basicConfig(
+ level=level,
+ format="%(asctime)s [%(levelname)s] %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+ )
+
+
+def main() -> int:
+ """CLI 진입점."""
+ parser = argparse.ArgumentParser(
+ description="2단계 최고 수익 기법 sim 차트 (1단계 v3 sim 대조용)"
+ )
+ parser.add_argument(
+ "--technique",
+ type=str,
+ default=None,
+ help="기법 ID (기본: causal_sim_report 수익률 1위)",
+ )
+ parser.add_argument(
+ "--report",
+ type=str,
+ default=None,
+ help="causal_sim_report.json 경로 (기본: .env)",
+ )
+ parser.add_argument("-v", "--verbose", action="store_true")
+ args = parser.parse_args()
+
+ _configure_logging(args.verbose)
+ settings = load_settings()
+ analysis_dir = settings.causal_sim_report_json.parent
+ report_path = Path(args.report) if args.report else settings.causal_sim_report_json
+
+ gt_result = load_ground_truth(settings.ground_truth_file)
+ gt_meta = gt_result.get("meta", {})
+
+ technique_id = args.technique
+ if not technique_id:
+ if not report_path.exists():
+ logging.error(
+ "리포트 없음: %s — 먼저 2_run_causal_sim.py 실행",
+ report_path,
+ )
+ return 1
+ report = json.loads(report_path.read_text(encoding="utf-8"))
+ best = pick_best_technique_row(report)
+ if not best:
+ logging.error("리포트에 기법 순위 없음: %s", report_path)
+ return 1
+ technique_id = best["technique_id"]
+
+ tech_path = settings.techniques_dir / f"{technique_id}.json"
+ if not tech_path.exists():
+ logging.error("기법 결과 없음: %s", tech_path)
+ return 1
+
+ result = load_technique_result(tech_path)
+ df = load_candles(
+ db_path=settings.db_path,
+ symbol=settings.symbol,
+ interval_min=settings.gt_interval_min,
+ lookback_days=settings.gt_lookback_days,
+ )
+ last_close = float(df["close"].iloc[-1])
+ sim_pnl = run_technique_causal_sim(
+ result,
+ initial_cash_krw=settings.gt_initial_cash_krw,
+ fee_rate=settings.gt_trading_fee_rate,
+ sim_lookback_days=settings.gt_sim_lookback_days,
+ data_end=gt_meta.get("data_to"),
+ last_mark_price=last_close,
+ )
+
+ output_path = best_technique_chart_path(analysis_dir)
+ render_best_technique_comparison_chart(
+ db_path=settings.db_path,
+ symbol=settings.symbol,
+ gt_result=gt_result,
+ result=result,
+ sim_pnl=sim_pnl,
+ output_path=output_path,
+ chart_lookback_days=settings.download_days,
+ )
+
+ print("\n=== 최고 수익 기법 sim 차트 (1단계 v3 대조) ===")
+ print(f"기법: {result.technique_name} ({result.technique_id})")
+ print(
+ f"3년 sim: {sim_pnl.get('total_return_pct', 0):+.2f}% | "
+ f"체결 {sim_pnl.get('buys_executed', 0)}/{sim_pnl.get('sells_executed', 0)}"
+ )
+ print(f"차트 기간: 최근 {settings.download_days}일 (1단계 v3 sim과 동일)")
+ print(f"HTML: {output_path}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/2_run_causal_sim.py b/scripts/2_run_causal_sim.py
index 8059d7e..d42947b 100644
--- a/scripts/2_run_causal_sim.py
+++ b/scripts/2_run_causal_sim.py
@@ -18,7 +18,10 @@ from deepcoin.config import load_settings
from deepcoin.data.candle_loader import load_candles
from deepcoin.data.intervals import interval_label
from deepcoin.evaluation.causal_sim import (
+ best_technique_chart_path,
build_causal_sim_report,
+ pick_best_technique_row,
+ render_best_technique_comparison_chart,
render_causal_sim_html,
render_technique_sim_chart,
run_technique_causal_sim,
@@ -145,6 +148,30 @@ def main() -> int:
json_path = save_causal_sim_report(report, settings.causal_sim_report_json)
html_path = render_causal_sim_html(report, settings.causal_sim_report_html)
+ best_row = pick_best_technique_row(report)
+ if best_row:
+ best_result = next(
+ (r for r in results if r.technique_id == best_row["technique_id"]),
+ None,
+ )
+ if best_result is not None:
+ best_chart = best_technique_chart_path(analysis_dir)
+ render_best_technique_comparison_chart(
+ db_path=settings.db_path,
+ symbol=settings.symbol,
+ gt_result=gt_result,
+ result=best_result,
+ sim_pnl=sim_pnls[best_result.technique_id],
+ output_path=best_chart,
+ chart_lookback_days=settings.download_days,
+ )
+ print(
+ f"1단계 v3 대조 차트: {best_chart} "
+ f"({best_result.technique_name}, "
+ f"{sim_pnls[best_result.technique_id].get('total_return_pct', 0):+.2f}%)",
+ flush=True,
+ )
+
elapsed = time.monotonic() - t0
print(f"\n=== 2단계 인과 sim 완료 ({elapsed/60:.1f}분) ===", flush=True)
if stage1_sim:
diff --git a/scripts/2_run_mtf_analysis.py b/scripts/2_run_mtf_analysis.py
index 3e9702f..dfdadee 100644
--- a/scripts/2_run_mtf_analysis.py
+++ b/scripts/2_run_mtf_analysis.py
@@ -20,6 +20,7 @@ from deepcoin.evaluation.mtf_report import (
save_mtf_report,
)
from deepcoin.mtf.extractor import MtfFeatureExtractor
+from deepcoin.mtf.rules import derive_rules_from_report, save_mtf_rules
from deepcoin.mtf.store import MultiTimeframeStore
from deepcoin.techniques.runner import load_ground_truth
@@ -106,6 +107,9 @@ def main() -> int:
json_path = save_mtf_report(report, settings.mtf_report_json)
html_path = render_mtf_html(report, settings.mtf_report_html)
+ rule_set = derive_rules_from_report(report)
+ rules_path = save_mtf_rules(rule_set, settings.mtf_rules_json)
+
gt = report.get("gt", {})
top = (report.get("global_feature_ranking") or [])[:5]
print("\n=== GT v3 MTF 상관 분석 ===")
@@ -123,6 +127,7 @@ def main() -> int:
)
print(f"\nJSON: {json_path}")
print(f"HTML: {html_path}")
+ print(f"MTF rules: {rules_path}")
return 0
diff --git a/src/deepcoin/config.py b/src/deepcoin/config.py
index bc24d82..8c3118b 100644
--- a/src/deepcoin/config.py
+++ b/src/deepcoin/config.py
@@ -116,7 +116,7 @@ def load_settings(env_path: Path | None = None) -> Settings:
intervals_raw = os.getenv("DOWNLOAD_INTERVALS", default_intervals)
intervals = sorted(set(_parse_int_list(intervals_raw)))
- db_raw = os.getenv("DB_PATH", "coins.db")
+ db_raw = os.getenv("DB_PATH", "data/common/coins.db")
db_path = Path(db_raw)
if not db_path.is_absolute():
db_path = _PROJECT_ROOT / db_path
@@ -159,13 +159,13 @@ def load_settings(env_path: Path | None = None) -> Settings:
os.getenv("GROUND_TRUTH_V2_FILE", "data/spot/ground_truth/ground_truth_trades_v2.json")
),
ground_truth_chart_v1_file=_resolve_project_path(
- os.getenv("GROUND_TRUTH_CHART_V1_FILE", "docs/0_ground_truth/spot/ground_truth_chart_v1.html")
+ os.getenv("GROUND_TRUTH_CHART_V1_FILE", "docs/spot/0_ground_truth/ground_truth_chart_v1.html")
),
ground_truth_chart_v2_file=_resolve_project_path(
- os.getenv("GROUND_TRUTH_CHART_V2_FILE", "docs/0_ground_truth/spot/ground_truth_chart_v2.html")
+ os.getenv("GROUND_TRUTH_CHART_V2_FILE", "docs/spot/0_ground_truth/ground_truth_chart_v2.html")
),
ground_truth_chart_v3_file=_resolve_project_path(
- os.getenv("GROUND_TRUTH_CHART_V3_FILE", "docs/0_ground_truth/spot/ground_truth_chart_v3.html")
+ os.getenv("GROUND_TRUTH_CHART_V3_FILE", "docs/spot/0_ground_truth/ground_truth_chart_v3.html")
),
ground_truth_futures_file=_resolve_project_path(
os.getenv(
@@ -188,19 +188,19 @@ def load_settings(env_path: Path | None = None) -> Settings:
ground_truth_futures_chart_v1_file=_resolve_project_path(
os.getenv(
"GROUND_TRUTH_FUTURES_CHART_V1_FILE",
- "docs/0_ground_truth/futures/ground_truth_chart_v1.html",
+ "docs/futures/0_ground_truth/ground_truth_chart_v1.html",
)
),
ground_truth_futures_chart_v2_file=_resolve_project_path(
os.getenv(
"GROUND_TRUTH_FUTURES_CHART_V2_FILE",
- "docs/0_ground_truth/futures/ground_truth_chart_v2.html",
+ "docs/futures/0_ground_truth/ground_truth_chart_v2.html",
)
),
ground_truth_futures_chart_v3_file=_resolve_project_path(
os.getenv(
"GROUND_TRUTH_FUTURES_CHART_V3_FILE",
- "docs/0_ground_truth/futures/ground_truth_chart_v3.html",
+ "docs/futures/0_ground_truth/ground_truth_chart_v3.html",
)
),
ground_truth_chart_sim_v1_file=_resolve_project_path(
diff --git a/src/deepcoin/data/__init__.py b/src/deepcoin/data/__init__.py
new file mode 100644
index 0000000..b85fe69
--- /dev/null
+++ b/src/deepcoin/data/__init__.py
@@ -0,0 +1 @@
+"""캔들 수집·저장·조회."""
diff --git a/src/deepcoin/data/candle_loader.py b/src/deepcoin/data/candle_loader.py
new file mode 100644
index 0000000..0fa753e
--- /dev/null
+++ b/src/deepcoin/data/candle_loader.py
@@ -0,0 +1,44 @@
+"""SQLite 캔들 조회."""
+
+from __future__ import annotations
+
+from datetime import timedelta
+from pathlib import Path
+
+import pandas as pd
+
+from deepcoin.data.candle_store import CandleStore
+
+
+def load_candles(
+ db_path: Path | str,
+ symbol: str,
+ interval_min: int,
+ lookback_days: int | None = None,
+) -> pd.DataFrame:
+ """DB에서 캔들 DataFrame을 로드한다.
+
+ Args:
+ db_path: SQLite 경로.
+ symbol: 코인 심볼 (예: BTC).
+ interval_min: 분 단위 인터벌 코드.
+ lookback_days: 최근 N일만 사용. None이면 전체.
+
+ Returns:
+ ``datetime``, ``open``, ``high``, ``low``, ``close``, ``volume`` 컬럼.
+ 시간 오름차순 정렬.
+ """
+ store = CandleStore(db_path)
+ try:
+ df = store.read_dataframe(symbol, interval_min)
+ finally:
+ store.close()
+
+ if df.empty:
+ return df
+
+ if lookback_days is not None and lookback_days > 0:
+ cutoff = df["datetime"].max() - timedelta(days=lookback_days)
+ df = df[df["datetime"] >= cutoff].reset_index(drop=True)
+
+ return df
diff --git a/src/deepcoin/data/candle_store.py b/src/deepcoin/data/candle_store.py
new file mode 100644
index 0000000..e4efccc
--- /dev/null
+++ b/src/deepcoin/data/candle_store.py
@@ -0,0 +1,191 @@
+"""SQLite 캔들 저장소."""
+
+from __future__ import annotations
+
+import sqlite3
+from datetime import datetime
+from pathlib import Path
+
+import pandas as pd
+
+from deepcoin.api.bithumb import parse_kst_datetime
+
+
+class CandleStore:
+ """``{SYMBOL}_{interval}`` 테이블에 OHLCV를 저장·조회한다."""
+
+ _CREATE_SQL = """
+ CREATE TABLE IF NOT EXISTS {table} (
+ CODE text,
+ NAME text,
+ ymdhms datetime,
+ ymd text,
+ hms text,
+ Close REAL,
+ Open REAL,
+ High REAL,
+ Low REAL,
+ Volume REAL
+ )
+ """
+ _INDEX_SQL = "CREATE INDEX IF NOT EXISTS {table}_idx ON {table}(CODE, ymdhms)"
+ _UNIQUE_SQL = "CREATE UNIQUE INDEX IF NOT EXISTS {table}_uk ON {table}(CODE, ymdhms)"
+
+ def __init__(self, db_path: Path | str) -> None:
+ """저장소를 연다.
+
+ Args:
+ db_path: SQLite 파일 경로.
+ """
+ self.db_path = Path(db_path)
+ self._conn = sqlite3.connect(self.db_path)
+
+ def close(self) -> None:
+ """DB 연결을 닫는다."""
+ self._conn.close()
+
+ @staticmethod
+ def table_name(symbol: str, interval_min: int) -> str:
+ """테이블명을 반환한다."""
+ return f"{symbol.upper()}_{interval_min}"
+
+ def ensure_table(self, symbol: str, interval_min: int) -> str:
+ """테이블·인덱스가 없으면 생성한다.
+
+ Returns:
+ 테이블명.
+ """
+ table = self.table_name(symbol, interval_min)
+ self._conn.execute(self._CREATE_SQL.format(table=table))
+ self._conn.execute(self._INDEX_SQL.format(table=table))
+ self._conn.execute(self._UNIQUE_SQL.format(table=table))
+ self._conn.commit()
+ return table
+
+ def get_range(
+ self,
+ symbol: str,
+ interval_min: int,
+ ) -> tuple[int, datetime | None, datetime | None]:
+ """저장된 행 수와 최소·최대 시각을 반환한다.
+
+ Args:
+ symbol: 코인 심볼.
+ interval_min: 분 단위 인터벌.
+
+ Returns:
+ ``(row_count, min_dt, max_dt)``. 데이터 없으면 ``(0, None, None)``.
+ """
+ table = self.table_name(symbol, interval_min)
+ try:
+ row = self._conn.execute(
+ f"SELECT COUNT(*), MIN(ymdhms), MAX(ymdhms) FROM {table} WHERE CODE = ?",
+ (symbol.upper(),),
+ ).fetchone()
+ except sqlite3.OperationalError:
+ return 0, None, None
+
+ if row is None or row[0] == 0 or row[1] is None:
+ return 0, None, None
+
+ return int(row[0]), parse_kst_datetime(str(row[1])), parse_kst_datetime(str(row[2]))
+
+ def read_dataframe(self, symbol: str, interval_min: int) -> pd.DataFrame:
+ """캔들을 pandas DataFrame으로 읽는다.
+
+ Args:
+ symbol: 코인 심볼.
+ interval_min: 분 단위 인터벌.
+
+ Returns:
+ 소문자 OHLCV 컬럼 DataFrame. 테이블 없으면 빈 DataFrame.
+ """
+ table = self.table_name(symbol, interval_min)
+ try:
+ raw = pd.read_sql_query(
+ f"""
+ SELECT ymdhms, Open, High, Low, Close, Volume
+ FROM {table}
+ WHERE CODE = ?
+ ORDER BY ymdhms ASC
+ """,
+ self._conn,
+ params=(symbol.upper(),),
+ )
+ except Exception:
+ return pd.DataFrame(
+ columns=["datetime", "open", "high", "low", "close", "volume"]
+ )
+
+ if raw.empty:
+ return pd.DataFrame(
+ columns=["datetime", "open", "high", "low", "close", "volume"]
+ )
+
+ raw["datetime"] = pd.to_datetime(raw["ymdhms"])
+ raw = raw.rename(
+ columns={
+ "Open": "open",
+ "High": "high",
+ "Low": "low",
+ "Close": "close",
+ "Volume": "volume",
+ }
+ )
+ return raw[["datetime", "open", "high", "low", "close", "volume"]].reset_index(
+ drop=True
+ )
+
+ def upsert_rows(
+ self,
+ symbol: str,
+ coin_name: str,
+ interval_min: int,
+ rows: list[tuple],
+ ) -> int:
+ """캔들 행을 upsert한다.
+
+ Args:
+ symbol: 코인 심볼.
+ coin_name: 코인 이름.
+ interval_min: 분 단위 인터벌.
+ rows: ``(ymdhms, open, high, low, close, volume)`` 튜플 리스트.
+
+ Returns:
+ 저장(시도) 행 수.
+ """
+ if not rows:
+ return 0
+
+ table = self.ensure_table(symbol, interval_min)
+ code = symbol.upper()
+ payload: list[tuple] = []
+ for ymdhms, open_p, high_p, low_p, close_p, volume in rows:
+ dt = parse_kst_datetime(str(ymdhms)) if isinstance(ymdhms, str) else ymdhms
+ ymd = dt.strftime("%Y-%m-%d")
+ hms = dt.strftime("%H:%M:%S")
+ payload.append(
+ (
+ code,
+ coin_name,
+ dt.strftime("%Y-%m-%d %H:%M:%S"),
+ ymd,
+ hms,
+ close_p,
+ open_p,
+ high_p,
+ low_p,
+ volume,
+ )
+ )
+
+ self._conn.executemany(
+ f"""
+ INSERT OR REPLACE INTO {table}
+ (CODE, NAME, ymdhms, ymd, hms, Close, Open, High, Low, Volume)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ payload,
+ )
+ self._conn.commit()
+ return len(payload)
diff --git a/src/deepcoin/data/downloader.py b/src/deepcoin/data/downloader.py
new file mode 100644
index 0000000..8684b95
--- /dev/null
+++ b/src/deepcoin/data/downloader.py
@@ -0,0 +1,185 @@
+"""빗썸 캔들 역방향 수집."""
+
+from __future__ import annotations
+
+import logging
+from dataclasses import dataclass
+from datetime import datetime, timedelta
+from typing import Any
+
+from deepcoin.api.bithumb import BithumbCandleClient, parse_kst_datetime
+from deepcoin.config import Settings
+from deepcoin.data.candle_store import CandleStore
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class DownloadResult:
+ """인터벌별 수집 결과."""
+
+ interval_min: int
+ mode: str
+ requests: int
+ saved_rows: int
+ reached_target: bool
+
+
+def _candle_rows_from_api(
+ candles: list[dict[str, Any]],
+) -> list[tuple[str, float, float, float, float, float]]:
+ """API 응답을 DB upsert 튜플로 변환한다."""
+ rows: list[tuple[str, float, float, float, float, float]] = []
+ for candle in candles:
+ ts = candle.get("candle_date_time_kst") or candle.get("candle_date_time_utc")
+ if not ts:
+ continue
+ rows.append(
+ (
+ str(ts).replace("T", " "),
+ float(candle["opening_price"]),
+ float(candle["high_price"]),
+ float(candle["low_price"]),
+ float(candle["trade_price"]),
+ float(candle.get("candle_acc_trade_volume", 0.0)),
+ )
+ )
+ return rows
+
+
+class CandleDownloader:
+ """설정 기반 캔들 다운로더."""
+
+ def __init__(self, settings: Settings) -> None:
+ """다운로더를 초기화한다.
+
+ Args:
+ settings: 애플리케이션 설정.
+ """
+ self.settings = settings
+ self._client = BithumbCandleClient(
+ base_url=settings.api_url,
+ count=settings.candle_count,
+ sleep_sec=settings.request_sleep_sec,
+ retries=settings.request_retries,
+ )
+
+ def download_all(
+ self,
+ store: CandleStore,
+ *,
+ days: int,
+ full: bool = False,
+ ) -> list[DownloadResult]:
+ """모든 인터벌을 수집한다.
+
+ Args:
+ store: 캔들 저장소.
+ days: 풀 다운 목표 일수.
+ full: True면 목표 일수까지 역방향 풀 다운.
+
+ Returns:
+ 인터벌별 DownloadResult 리스트.
+ """
+ results: list[DownloadResult] = []
+ for interval_min in self.settings.download_intervals:
+ results.append(
+ self._download_interval(store, interval_min, days=days, full=full)
+ )
+ return results
+
+ def _download_interval(
+ self,
+ store: CandleStore,
+ interval_min: int,
+ *,
+ days: int,
+ full: bool,
+ ) -> DownloadResult:
+ """단일 인터벌을 수집한다."""
+ symbol = self.settings.symbol
+ count_before, _, db_max = store.get_range(symbol, interval_min)
+ target_from = datetime.now() - timedelta(days=max(1, days))
+
+ if full or db_max is None:
+ mode = "full"
+ stop_at = target_from
+ else:
+ mode = "incremental"
+ if db_max >= datetime.now() - timedelta(minutes=max(interval_min, 1)):
+ return DownloadResult(
+ interval_min=interval_min,
+ mode="uptodate",
+ requests=0,
+ saved_rows=0,
+ reached_target=True,
+ )
+ stop_at = db_max - timedelta(minutes=interval_min)
+
+ to_kst: datetime | None = None
+ requests = 0
+ saved_rows = 0
+ reached_target = False
+ oldest_seen: datetime | None = None
+
+ while True:
+ candles = self._client.fetch_candles(
+ self.settings.market,
+ interval_min,
+ to_kst=to_kst,
+ )
+ requests += 1
+ if not candles:
+ break
+
+ rows = _candle_rows_from_api(candles)
+ if not rows:
+ break
+
+ saved_rows += store.upsert_rows(
+ symbol,
+ self.settings.coin_name,
+ interval_min,
+ rows,
+ )
+
+ batch_oldest = min(parse_kst_datetime(r[0]) for r in rows)
+ if oldest_seen is None or batch_oldest < oldest_seen:
+ oldest_seen = batch_oldest
+
+ if batch_oldest <= stop_at:
+ reached_target = True
+ break
+
+ to_kst = batch_oldest
+ if to_kst <= stop_at:
+ reached_target = True
+ break
+
+ if mode == "full" and oldest_seen is not None and oldest_seen <= target_from:
+ reached_target = True
+ if mode == "incremental" and requests == 0 and count_before > 0:
+ return DownloadResult(
+ interval_min=interval_min,
+ mode="uptodate",
+ requests=0,
+ saved_rows=0,
+ reached_target=True,
+ )
+
+ logger.info(
+ "수집 완료 %s_%s mode=%s requests=%s saved=%s reached=%s",
+ symbol,
+ interval_min,
+ mode,
+ requests,
+ saved_rows,
+ reached_target,
+ )
+ return DownloadResult(
+ interval_min=interval_min,
+ mode=mode,
+ requests=requests,
+ saved_rows=saved_rows,
+ reached_target=reached_target,
+ )
diff --git a/src/deepcoin/data/intervals.py b/src/deepcoin/data/intervals.py
new file mode 100644
index 0000000..be5a90d
--- /dev/null
+++ b/src/deepcoin/data/intervals.py
@@ -0,0 +1,89 @@
+"""봉 간격 상수 및 라벨·다운로드 추정 유틸."""
+
+from __future__ import annotations
+
+import math
+
+INTERVAL_1MIN = 1
+INTERVAL_DAILY = 1440
+INTERVAL_WEEKLY = 10080
+INTERVAL_MONTHLY = 43200
+
+CALENDAR_INTERVALS: frozenset[int] = frozenset(
+ {INTERVAL_DAILY, INTERVAL_WEEKLY, INTERVAL_MONTHLY}
+)
+
+DEFAULT_DOWNLOAD_INTERVALS: tuple[int, ...] = (
+ 1,
+ 3,
+ 5,
+ 10,
+ 15,
+ 30,
+ 60,
+ 240,
+ INTERVAL_DAILY,
+ INTERVAL_WEEKLY,
+ INTERVAL_MONTHLY,
+)
+
+
+def interval_label(interval_min: int) -> str:
+ """인터벌 코드를 사람이 읽기 쉬운 라벨로 변환한다.
+
+ Args:
+ interval_min: 분 단위 인터벌 코드.
+
+ Returns:
+ 예: ``3분``, ``일``, ``주``, ``월``.
+ """
+ if interval_min == INTERVAL_DAILY:
+ return "일"
+ if interval_min == INTERVAL_WEEKLY:
+ return "주"
+ if interval_min == INTERVAL_MONTHLY:
+ return "월"
+ if interval_min < INTERVAL_DAILY:
+ return f"{interval_min}분"
+ if interval_min % 1440 == 0:
+ return f"{interval_min // 1440}일"
+ return f"{interval_min}분"
+
+
+def bars_per_day(interval_min: int) -> float:
+ """하루당 예상 봉 수를 반환한다.
+
+ Args:
+ interval_min: 분 단위 인터벌 코드.
+
+ Returns:
+ 일봉 이상은 1 미만(주·월)일 수 있다.
+ """
+ if interval_min == INTERVAL_DAILY:
+ return 1.0
+ if interval_min == INTERVAL_WEEKLY:
+ return 1.0 / 7.0
+ if interval_min == INTERVAL_MONTHLY:
+ return 1.0 / 30.0
+ return (24 * 60) / interval_min
+
+
+def estimate_download_requests(
+ interval_min: int,
+ days: int,
+ batch_size: int = 200,
+) -> int:
+ """역방향 페이지네이션 시 예상 API 요청 횟수를 추정한다.
+
+ Args:
+ interval_min: 분 단위 인터벌 코드.
+ days: 수집 목표 일수.
+ batch_size: 요청당 캔들 수.
+
+ Returns:
+ 최소 1회.
+ """
+ days = max(1, days)
+ batch_size = max(1, batch_size)
+ total_bars = max(1, math.ceil(days * bars_per_day(interval_min)))
+ return max(1, math.ceil(total_bars / batch_size))
diff --git a/src/deepcoin/evaluation/causal_sim.py b/src/deepcoin/evaluation/causal_sim.py
index 5c4347b..c166e9f 100644
--- a/src/deepcoin/evaluation/causal_sim.py
+++ b/src/deepcoin/evaluation/causal_sim.py
@@ -62,6 +62,12 @@ def run_technique_causal_sim(
)
+def _chart_meta_base(gt_meta: dict[str, Any]) -> dict[str, Any]:
+ """차트용 GT 메타에서 chart_tier 등 2단계에 혼동되는 필드를 제거한다."""
+ skip = {"chart_tier"}
+ return {k: v for k, v in gt_meta.items() if k not in skip}
+
+
def _gt_shell_for_chart(
gt_meta: dict[str, Any],
technique: TechniqueResult,
@@ -69,7 +75,7 @@ def _gt_shell_for_chart(
"""차트 렌더용 최소 GT 구조를 만든다."""
return {
"meta": {
- **gt_meta,
+ **_chart_meta_base(gt_meta),
"technique_id": technique.technique_id,
"technique_name": technique.technique_name,
"stage": "spot_2_causal_sim",
@@ -83,6 +89,67 @@ def technique_sim_chart_path(analysis_dir: Path, technique_id: str) -> Path:
return analysis_dir / f"technique_chart_sim_{technique_id}.html"
+def best_technique_chart_path(analysis_dir: Path) -> Path:
+ """1단계 v3 sim 차트와 대조용 — 최고 수익 인과 기법 sim 차트 경로."""
+ return analysis_dir / "causal_sim_chart_best_technique.html"
+
+
+def pick_best_technique_row(report: dict[str, Any]) -> dict[str, Any] | None:
+ """인과 sim 리포트에서 수익률 1위 기법 행을 반환한다."""
+ rows = report.get("ranking") or []
+ return rows[0] if rows else None
+
+
+def render_best_technique_comparison_chart(
+ *,
+ db_path: Path,
+ symbol: str,
+ gt_result: dict[str, Any],
+ result: TechniqueResult,
+ sim_pnl: dict[str, Any],
+ output_path: Path,
+ chart_lookback_days: int,
+ stage1_chart_ref: str = "docs/spot/1_simulation/ground_truth_chart_sim_v3.html",
+) -> Path:
+ """수익률 1위 인과 기법 sim을 1단계 v3 sim 차트와 동일 UI·기간으로 렌더한다.
+
+ Args:
+ db_path: SQLite 경로.
+ symbol: 코인 심볼.
+ gt_result: v3 GT JSON (메타·비교 기준).
+ result: 최고 수익 기법 결과.
+ sim_pnl: 해당 기법 3년 인과 sim 결과.
+ output_path: HTML 출력 경로.
+ chart_lookback_days: 1단계와 동일 캔들 표시 일수 (보통 DOWNLOAD_DAYS).
+ stage1_chart_ref: 1단계 v3 sim 차트 상대 경로 (안내용).
+
+ Returns:
+ HTML 저장 경로.
+ """
+ gt_meta = gt_result.get("meta", {})
+ benchmark = stage1_benchmark_from_gt(gt_result)
+ comparison_gt = {
+ "meta": {
+ **_chart_meta_base(gt_meta),
+ "technique_id": result.technique_id,
+ "technique_name": result.technique_name,
+ "stage": "spot_2_causal_sim_best",
+ "comparison_ref": stage1_chart_ref,
+ "comparison_label": "1단계 ground_truth_chart_sim_v3.html (사후 GT)",
+ "stage1_benchmark_return_pct": (benchmark or {}).get("total_return_pct"),
+ },
+ "signals": [],
+ }
+ return render_ground_truth_sim_chart(
+ db_path=db_path,
+ symbol=symbol,
+ gt_result=comparison_gt,
+ sim_pnl=sim_pnl,
+ output_path=output_path,
+ chart_lookback_days=chart_lookback_days,
+ )
+
+
def render_technique_sim_chart(
*,
db_path: Path,
@@ -157,6 +224,7 @@ def build_causal_sim_report(
rows.sort(key=lambda r: r["sim_return_pct"], reverse=True)
period_from = benchmark.get("period_from") if benchmark else None
period_to = benchmark.get("period_to") if benchmark else None
+ best = rows[0] if rows else None
return {
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
@@ -170,6 +238,8 @@ def build_causal_sim_report(
"sim_lookback_days": gt_meta.get("sim_lookback_days")
or (benchmark or {}).get("sim_lookback_days"),
"stage1_benchmark_v3": benchmark,
+ "best_technique": best,
+ "best_technique_chart": "causal_sim_chart_best_technique.html",
"ranking": rows,
}
@@ -186,8 +256,20 @@ def render_causal_sim_html(report: dict[str, Any], html_path: Path) -> Path:
"""인과 sim 리포트 HTML을 생성한다."""
html_path.parent.mkdir(parents=True, exist_ok=True)
benchmark = report.get("stage1_benchmark_v3") or {}
+ best = report.get("best_technique") or {}
+ best_chart = report.get("best_technique_chart", "")
rows = report.get("ranking", [])
+ best_note = ""
+ if best and best_chart:
+ best_note = (
+ f'수익률 1위 기법 '
+ f'{best.get("technique_name", "")} '
+ f'({best.get("technique_id", "")}, {best.get("sim_return_pct", 0):+.2f}%) — '
+ f'1단계 v3 sim 차트와 동일 형식: '
+ f'causal_sim_chart_best_technique.html
'
+ )
+
bench_row = ""
if benchmark:
bench_row = f"""
@@ -242,6 +324,7 @@ def render_causal_sim_html(report: dict[str, Any], html_path: Path) -> Path:
생성: {report.get('generated_at', '')}
{report.get('description', '')}
+ {best_note}
diff --git a/src/deepcoin/ground_truth/chart.py b/src/deepcoin/ground_truth/chart.py
index 7df2545..cda6aaf 100644
--- a/src/deepcoin/ground_truth/chart.py
+++ b/src/deepcoin/ground_truth/chart.py
@@ -254,7 +254,7 @@ _HTML_TEMPLATE = """
- Ground Truth Chart
+ DeepCoin Chart
@@ -284,7 +284,7 @@ __EXTRA_STYLES__
- Ground Truth Chart
+ DeepCoin Chart
__EXTRA_BODY__
@@ -717,6 +717,71 @@ __EXTRA_BODY__
__EXTRA_SCRIPT__
+ function resolveChartPresentation(m, simMode, chartLabel, gtLabel, simPnl) {
+ const stage = m.stage || "";
+ const techniqueName = m.technique_name || "";
+ const techniqueId = m.technique_id || "";
+ const simDays = (simPnl && simPnl.sim_lookback_days) || 1095;
+ const simLabel = simDays >= 365
+ ? `최근 ${Math.round(simDays / 365)}년`
+ : `최근 ${simDays}일`;
+ const initCash = (simPnl && simPnl.initial_cash_krw) || 0;
+ const initLabel = initCash ? ` · 초기 ${Math.round(initCash).toLocaleString()}원` : "";
+
+ if (stage === "spot_2_causal_sim_best") {
+ const bench = m.stage1_benchmark_return_pct;
+ const benchNote = Number.isFinite(bench)
+ ? ` | 1단계 v3 GT sim ${bench >= 0 ? "+" : ""}${bench.toFixed(2)}%`
+ : "";
+ return {
+ pageTitle: `${m.symbol} 인과 sim — ${techniqueName}`,
+ title: `${m.symbol} 2단계 인과 sim · 최고 수익 (${techniqueName}, ${m.interval_label}) — 차트 ${chartLabel}`,
+ panelTitle: `2단계 인과 sim · 최고 수익 기법 (${simLabel}${initLabel})`,
+ legend: "B=매수 S=매도 | 과거 데이터만 · 인과 기법 신호",
+ simNoteExtra: `기법 ${techniqueId} | 미래 데이터 미사용${benchNote}`,
+ };
+ }
+ if (stage === "spot_2_causal_sim" || techniqueId) {
+ return {
+ pageTitle: `${m.symbol} 인과 sim — ${techniqueName}`,
+ title: `${m.symbol} 2단계 인과 sim (${techniqueName}, ${m.interval_label}) — 차트 ${chartLabel}`,
+ panelTitle: `2단계 인과 sim (${simLabel}${initLabel})`,
+ legend: "B=매수 S=매도 | 과거 데이터만 · 인과 기법 신호",
+ simNoteExtra: `기법 ${techniqueId} | 미래 데이터 미사용`,
+ };
+ }
+ if (simMode) {
+ const tier = m.chart_tier ? ` ${String(m.chart_tier).toUpperCase()}` : "";
+ const tierKey = (m.chart_tier || "v3").toLowerCase();
+ const legend = tierKey === "v1"
+ ? "B=스윙매수 S=스윙매도"
+ : tierKey === "v2"
+ ? "B/S=스윙 B*=눌림목"
+ : "B/S=스윙 B*=눌림 B^=돌파 Bd/Sd=다이버전스";
+ return {
+ pageTitle: `${m.symbol} GT sim${tier}`,
+ title: `${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel} · 1단계 sim`,
+ panelTitle: `1단계 GT sim (${simLabel}${initLabel}) · 사후 최적 타점`,
+ legend: legend,
+ simNoteExtra: "사후 GT 타점 · 미래 데이터 사용 (벤치마크)",
+ };
+ }
+ const tier = m.chart_tier ? ` ${String(m.chart_tier).toUpperCase()}` : "";
+ const tierKey = (m.chart_tier || "v3").toLowerCase();
+ const legend = tierKey === "v1"
+ ? "B=스윙매수 S=스윙매도"
+ : tierKey === "v2"
+ ? "B/S=스윙 B*=눌림목"
+ : "B/S=스윙 B*=눌림 B^=돌파 Bd/Sd=다이버전스";
+ return {
+ pageTitle: `${m.symbol} Ground Truth${tier}`,
+ title: `${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel}`,
+ panelTitle: "",
+ legend: legend,
+ simNoteExtra: "",
+ };
+ }
+
function init() {
DATA = window.CHART_DATA;
if (!DATA) throw new Error("차트 데이터 JS 없음");
@@ -726,35 +791,27 @@ __EXTRA_SCRIPT__
const gtDays = m.gt_lookback_days || m.lookback_days;
const chartLabel = chartDays >= 365 ? `${Math.round(chartDays / 365)}년` : `${chartDays}일`;
const gtLabel = gtDays >= 365 ? `${Math.round(gtDays / 365)}년` : `${gtDays}일`;
- const tier = m.chart_tier ? ` ${m.chart_tier.toUpperCase()}` : "";
- const simSuffix = simMode ? " · 1단계 sim" : "";
- document.getElementById("title").textContent =
- `${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel}${simSuffix}`;
+ const pres = resolveChartPresentation(m, simMode, chartLabel, gtLabel, DATA.sim_pnl);
+ document.title = pres.pageTitle;
+ document.getElementById("title").textContent = pres.title;
document.getElementById("btn-all").textContent = `전체 ${chartLabel}`;
const chartFrom = m.chart_data_from || m.data_from;
const chartTo = m.chart_data_to || m.data_to;
- const tierKey = (m.chart_tier || "v3").toLowerCase();
- const legend = tierKey === "v1"
- ? "B=스윙매수 S=스윙매도"
- : tierKey === "v2"
- ? "B/S=스윙 B*=눌림목"
- : "B/S=스윙 B*=눌림 B^=돌파 Bd/Sd=다이버전스";
const markerRange = simMode && m.sim_period_from
? `체결 ${DATA.buy_markers.length}/${DATA.sell_markers.length} · ${m.sim_period_from.slice(0, 16)} ~ ${(m.sim_period_to || chartTo).slice(0, 16)}`
: gtLabel;
const legendExtra = simMode ? " | ▼보라=거래시작" : "";
- document.getElementById("meta").textContent =
- `차트 ${chartFrom} ~ ${chartTo} (${DATA.bar_count.toLocaleString()}봉) | 매수 ${DATA.buy_markers.length} / 매도 ${DATA.sell_markers.length} (${markerRange}) | ${legend}${legendExtra}`;
+ let metaLine =
+ `차트 ${chartFrom} ~ ${chartTo} (${DATA.bar_count.toLocaleString()}봉) | 매수 ${DATA.buy_markers.length} / 매도 ${DATA.sell_markers.length} (${markerRange}) | ${pres.legend}${legendExtra}`;
+ if (m.comparison_label) {
+ metaLine += ` | 대조: ${m.comparison_label}`;
+ }
+ document.getElementById("meta").textContent = metaLine;
+ window.__SIM_NOTE_EXTRA__ = pres.simNoteExtra || "";
if (simMode) {
- const simDays = DATA.sim_pnl.sim_lookback_days || 1095;
- const simLabel = simDays >= 365
- ? `최근 ${Math.round(simDays / 365)}년`
- : `최근 ${simDays}일`;
const panelTitle = document.getElementById("sim-panel-title");
- if (panelTitle) {
- const initCash = DATA.sim_pnl?.initial_cash_krw || 0;
- const initLabel = initCash ? `${Math.round(initCash).toLocaleString()}원` : "";
- panelTitle.textContent = `1단계 수익 sim (${simLabel}${initLabel ? ` · 초기 ${initLabel}` : ""})`;
+ if (panelTitle && pres.panelTitle) {
+ panelTitle.textContent = pres.panelTitle;
}
renderSimPanel();
}
@@ -834,7 +891,7 @@ _SIM_EXTRA_STYLES = """
_SIM_EXTRA_BODY = """
@@ -910,7 +967,8 @@ _SIM_EXTRA_SCRIPT = """
`시뮬 기간: ${p.period_from} ~ ${p.period_to} (${p.sim_lookback_days}일) | ` +
`신호 ${p.signals_in_period}건 | 분할매수/매도 클러스터 적용 | ` +
`스킵 매수 ${p.buys_skipped} / 매도 ${p.sells_skipped} | 수수료 ${(p.fee_rate * 100).toFixed(2)}%` +
- (p.buy_sizing_rule ? ` | 매수 ${p.buy_sizing_rule}` : "");
+ (p.buy_sizing_rule ? ` | 매수 ${p.buy_sizing_rule}` : "") +
+ (window.__SIM_NOTE_EXTRA__ ? ` | ${window.__SIM_NOTE_EXTRA__}` : "");
const tbody = document.getElementById("trade-body");
tbody.innerHTML = "";
(p.trades || []).forEach(t => {