feat(spot): 2단계 인과 기법 분석 파이프라인 마무리

common/spot/futures 경로 정비, 캔들 데이터 모듈 복원, MTF 규칙 자동 저장 및 2단계 설계·최종 정리 문서를 반영해 3단계 착수 기반을 확정한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
xavis
2026-06-12 16:09:32 +09:00
parent 741c949470
commit 2d515dd669
18 changed files with 2073 additions and 335 deletions

View File

@@ -16,8 +16,8 @@ COIN_TELEGRAM_CHAT_ID=
SYMBOL=BTC SYMBOL=BTC
COIN_NAME=비트코인 COIN_NAME=비트코인
# --- 공: 캔들 DB (현물·선물 유일한 공유 리소스) --- # --- 공: 캔들 DB (현물·선물 유) ---
DB_PATH=coins.db DB_PATH=data/common/coins.db
DOWNLOAD_DAYS=3650 DOWNLOAD_DAYS=3650
DOWNLOAD_INTERVALS=1,3,5,10,15,30,60,240,1440,10080,43200 DOWNLOAD_INTERVALS=1,3,5,10,15,30,60,240,1440,10080,43200
API_REQUEST_SLEEP_SEC=0.35 API_REQUEST_SLEEP_SEC=0.35
@@ -25,7 +25,7 @@ API_REQUEST_RETRIES=3
# --- 0단계: GT 타점 (현물·선물 공통 파라미터) --- # --- 0단계: GT 타점 (현물·선물 공통 파라미터) ---
GT_INTERVAL_MIN=3 GT_INTERVAL_MIN=3
GT_LOOKBACK_DAYS=3447 GT_LOOKBACK_DAYS=3650
GT_INITIAL_CASH_KRW=200000 GT_INITIAL_CASH_KRW=200000
GT_TRADING_FEE_RATE=0.0005 GT_TRADING_FEE_RATE=0.0005
GT_ZIGZAG_REVERSAL_PCT=5.0 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_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_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_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_V1_FILE=docs/spot/0_ground_truth/ground_truth_chart_v1.html
GROUND_TRUTH_CHART_V2_FILE=docs/0_ground_truth/spot/ground_truth_chart_v2.html GROUND_TRUTH_CHART_V2_FILE=docs/spot/0_ground_truth/ground_truth_chart_v2.html
GROUND_TRUTH_CHART_V3_FILE=docs/0_ground_truth/spot/ground_truth_chart_v3.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_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_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_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_V1_FILE=docs/futures/0_ground_truth/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_V2_FILE=docs/futures/0_ground_truth/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_V3_FILE=docs/futures/0_ground_truth/ground_truth_chart_v3.html
# --- 현물 1단계: GT sim --- # --- 현물 1단계: GT sim ---
GROUND_TRUTH_CHART_SIM_V1_FILE=docs/spot/1_simulation/ground_truth_chart_sim_v1.html 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 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_JSON=docs/spot/2_analysis/causal_sim_report.json
CAUSAL_SIM_REPORT_HTML=docs/spot/2_analysis/causal_sim_report.html CAUSAL_SIM_REPORT_HTML=docs/spot/2_analysis/causal_sim_report.html
# 폴더 구조: data|docs / {common, spot, futures}
# common — coins.db 등 공유 리소스
# spot — 현물 GT·기법·분석·운영
# futures — 선물 GT·분석·운영

17
.gitignore vendored
View File

@@ -3,9 +3,20 @@
.DS_Store .DS_Store
.idea .idea
# Runtime data & generated artifacts (scripts로 재생성) # Runtime data & generated artifacts (scripts로 재생성) — 저장소 루트만
data/ /data/**
docs/ !/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/ logs/
*.db *.db

539
README.md
View File

@@ -1,13 +1,14 @@
# DeepCoin # DeepCoin
빗썸 KRW 마켓 암호화폐 캔들 데이터 수집 및 현물·선물 매매 전략 파이프라인. 빗썸 KRW 마켓 암호화폐 캔들 데이터 수집 및 **현물**·**선물** 매매 전략 파이프라인.
기본 전략 축: **3분봉 현물**, 최근 **10년** 데이터. `data/`·`docs/`**공통(common)** · **현물(spot)** · **선물(futures)** 세 유형으로 구분합니다.
## 주요 기능 ## 주요 기능
- 빗썸 Public API(v1) 기반 분·일·주·월봉 캔들 수집 - 빗썸 Public API(v1) 기반 분·일·주·월봉 캔들 수집 (1분봉 포함)
- SQLite(`coins.db`) 저장 — 테이블명 `{SYMBOL}_{인터벌코드}` (예: `BTC_60`, `BTC_10080`) - SQLite 캔들 DB — 현물·선물 **공통** (`data/common/coins.db`)
- 2017-01-01~ 역방향 페이지네이션 수집 (기본 3650일·10년, **1분봉 포함**) - Ground Truth(GT) 기반 현물·선물 벤치마크·인과 기법 분석·(예정) 실거래 운영
- Ground Truth 기반 현물·선물 벤치마크 및 인과 기법 분석
## 요구사항 ## 요구사항
@@ -18,297 +19,285 @@
```bash ```bash
cd DeepCoin cd DeepCoin
conda activate ncue conda activate ncue # 또는 xavis
pip install -r requirements.txt 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` | | `SYMBOL` | 코인 심볼 | `BTC` |
| `COIN_NAME` | 코인 이름 | `비트코인` | | `DOWNLOAD_DAYS` | 수집·차트 일수 | `3650` |
| `DB_PATH` | SQLite 경로 | `coins.db` | | `GT_INTERVAL_MIN` | GT·기법 기준 인터벌(분) | `3` |
| `DOWNLOAD_DAYS` | 전체 수집·차트 일수 (10년) | `3650` | | `GT_LOOKBACK_DAYS` | GT 타점 기간(일) | `3650` |
| `DOWNLOAD_INTERVALS` | 인터벌 코드 목록 (`1`=1분봉 포함) | `1,3,5,10,15,30,60,240,1440,10080,43200` | | `GT_SIM_LOOKBACK_DAYS` | sim 거래 기간(일) | `1095` |
| `BITHUMB_API_CANDLE_COUNT` | 요청당 캔들 수 (최대 200) | `200` | | `GT_INITIAL_CASH_KRW` | sim 초기 자본(원) | `200000` |
| `API_REQUEST_SLEEP_SEC` | API 호출 간격(초) | `0.35` |
인터벌 코드: 분봉은 분 단위 숫자, 일봉=`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, 미래 데이터 미사용.
| 구분 | 단계 | 내용 | 산출물 | 스크립트 | | ID | 기법 | 유형 |
|------|------|------|--------|----------| |----|------|------|
| 사전 | — | 캔들 수집 | `coins.db` | `00_download.py` | | `zigzag_causal` | 인과 ZigZag | 스윙 B/S |
| `minor_swing` | 소형 스윙 하이브리드 | 하이브리드 |
### 0단계 — GT 타점 (현물·선물 공통) | `local_extrema` | 국소 극값 | 스윙 B/S |
| `pivot_swing` | 피벗 스윙 | 스윙 B/S |
| 시장 | 산출물 | 스크립트 | | `fractal_swing` | 프랙탈 스윙 | 스윙 B/S |
|------|--------|----------| | `swing_failure` | 스윙 실패 | 스윙 B/S |
| 현물 | `docs/0_ground_truth/spot/` | `0_ground_truth.py` | | `donchian` | 돈치안 채널 | 스윙 B/S |
| 선물 | `docs/0_ground_truth/futures/` | `0_ground_truth_futures.py` | | `ema_pullback` | EMA 눌림목 | 눌림목 B* |
| `fib_pullback` | 피보나치 눌림목 | 눌림목 B* |
### 현물 단계 | `support_bounce` | 지지·저항 반등 | 눌림목 B* |
| `keltner_breakout` | Keltner 돌파 | 돌파 B^ |
| 단계 | 내용 | 산출물 | 스크립트 | | `range_breakout` | 레인지 돌파 | 돌파 B^ |
|------|------|--------|----------| | `volume_breakout` | 거래량 돌파 | 돌파 B^ |
| **현물 1단계** | GT sim (벤치마크) 현물 | `docs/spot/1_simulation/` | `1_ground_truth_sim.py` | | `bb_squeeze_breakout` | BB 스퀴즈 돌파 | 돌파 B^ |
| **현물 2단계** | 인과 기법 분석 (현물) | `docs/spot/2_analysis/` | `2_run_techniques.py` | | `rsi_divergence` | RSI 다이버전스 | Bd/Sd |
| **현물 3단계** | 현물 실거래 운영 | (예정) | (예정) | | `macd_divergence` | MACD 다이버전스 | Bd/Sd |
| `obv_divergence` | OBV 다이버전스 | Bd/Sd |
### 선물 단계 | `bb_reversal` | 볼린저 역추세 | 지표 |
| `ma_cross` | EMA 크로스 | 지표 |
| 단계 | 내용 | 산출물 | 스크립트 | | `rsi_swing` | RSI 스윙 | 지표 |
|------|------|--------|----------| | `macd_cross` | MACD 크로스 | 지표 |
| **선물 1단계** | GT sim (벤치마크) 선물 | (예정) | (예정) | | `supertrend` | Supertrend | 추세 |
| **선물 2단계** | 인과 기법 분석 (선물) | (예정) | (예정) | | `adx_trend` | ADX 추세 | 추세 |
| **선물 3단계** | 선물 실거래 운영 | (예정) | (예정) | | `ichimoku_trend` | 일목 추세 | 추세 |
| `parabolic_sar` | Parabolic SAR | 추세 |
### 단계별 상세 설명 | `stochastic_cross` | Stochastic 크로스 | 모멘텀 |
| `cci_extreme` | CCI 극값 | 모멘텀 |
#### 사전 — 캔들 수집 | `roc_reversal` | ROC 반전 | 모멘텀 |
| `keltner_reversal` | Keltner 역추세 | 변동성 |
빗썸 Public API로 분·일·주·월봉(1분봉 포함) OHLCV를 수집해 `coins.db`에 저장합니다. 이후 모든 단계는 이 DB만 참조하며, 현물·선물이 DB를 공유합니다. | `atr_channel` | ATR 채널 | 변동성 |
| `pivot_points` | 피벗 포인트 | 구조 |
- 기본 동작: DB 최신 시각 이후 **증분 갱신** (`00_download.py`) | `support_resistance` | 구조적 지지·저항 | 구조 |
- 전체 재수집: `--full` 옵션 (최초 1회·DB 재구축 시) | `volume_spike` | 거래량 스파이크 | 거래량 |
- 테이블명: `{SYMBOL}_{인터벌코드}` (예: `BTC_3`, `BTC_1440`) | `composite_swing` | 스윙 복합 | 복합 |
| `composite_pullback` | 눌림목 복합 | 복합 |
#### 0단계 — GT 타점 (현물·선물 공통) | `composite_breakout` | 돌파 복합 | 복합 |
| `composite_divergence` | 다이버전스 복합 | 복합 |
**Ground Truth(GT)** 는 사후적으로 “이상적인 매매 타점”을 정의한 기준 데이터입니다. 미래 캔들을 참조해 ZigZag 스윙·눌림목·돌파·다이버전스 타점을 찾으므로 **실거래에 직접 쓸 수 없고**, 이후 단계(벤치마크·기법 분석)의 **정답지(기준선)** 역할을 합니다. | `composite_v3` | v3 통합 스코어링 | 복합 |
| `composite_full` | 전체 통합 복합 | 복합 |
| 티어 | 포함 신호 | 설명 |
|------|-----------|------|
| **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 추세 필터 |
--- ---
## 디렉터리 구조 ## 구현 현황
```text | 유형 | 단계 | 상태 |
DeepCoin/ |------|------|------|
├── src/deepcoin/ | common | 사전 (캔들) | 구현됨 |
│ ├── data/ # 캔들 수집·로드 | spot | 0~2단계 | 구현됨 |
│ ├── ground_truth/ # 0·1단계 GT·차트 | spot | 3단계 (운영) | 예정 |
│ ├── techniques/ # 현물 2단계 인과 기법 (39종) | futures | 0단계 | 구현됨 |
│ ├── evaluation/ # 현물 2단계 GT 정합 평가 | futures | 1~3단계 | 예정 |
│ └── 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/
```
현물·선물은 `coins.db`만 공유합니다. 현재 구현 범위는 **0단계(공통) + 현물 1·2단계**이며, 선물 1~3단계·현물 3단계는 추후 구현 예정입니다. ---
## 변경 이력 ## 변경 이력
- 2026-06-11: `docs/0_ground_truth/{spot,futures}/` 0단계 차트를 docs 직하로 이동 - 2026-06-12: `data/`·`docs/`**common / spot / futures** 3유형 구조로 재편, `coins.db``data/common/`, 0단계 차트`docs/{spot,futures}/0_ground_truth/`
- 2026-06-11: `scripts/` 접두사를 파이프라인 단계와 일치 (`00_` 사전, `0_` 0단계, `1_` 현물1, `2_` 현물2) - 2026-06-12: `0_ground_truth_futures.py` — 현물 GT → 선물 JSON·차트 변환 로직 보완
- 2026-06-11: README 파이프라인 단계별 상세 설명 추가 (0단계 공통 + 현물·선물 1~3단계) - 2026-06-12: README 현물 파이프라인 전체 순서 갱신
- 2026-06-11: README 파이프라인을 0단계(공통) + 현물·선물 각 1~3단계 체계로 정리 - 2026-06-12: `src/deepcoin/data/` 모듈 복원
- 2026-06-11: 선물 1~3단계·`04_run_causal_futures` 제거, 현물 0·1·2단계만 유지 - 2026-06-11: 파이프라인 단계별 상세 설명 추가
- 2026-06-11: `docs/spot/`·`docs/futures/` 상위 분리 (`data/`와 동일 구조) - 2026-06-08: Ground Truth v1/v2/v3
- 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-07: 캔들 수집 모듈 초기 구현 - 2026-06-07: 캔들 수집 모듈 초기 구현

View File

@@ -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
}
]
}
}

View File

@@ -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 보완, 접근 방식 비교 정리 |

View File

@@ -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단계 체크리스트 |

View File

@@ -7,6 +7,7 @@ import argparse
import json import json
import logging import logging
import sys import sys
from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -16,7 +17,9 @@ if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import Settings, load_settings 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.futures_chart import render_futures_ground_truth_chart
from deepcoin.ground_truth.ground_truth import save_ground_truth
TIER_DESCRIPTIONS = { TIER_DESCRIPTIONS = {
"v1": "스윙만 (최소 매수·매도)", "v1": "스윙만 (최소 매수·매도)",
@@ -35,18 +38,24 @@ def _configure_logging(verbose: bool) -> None:
) )
def _tier_targets(settings: Settings, tier_arg: str) -> list[tuple[str, Path, Path]]: def _tier_targets(
"""생성할 티어 목록 (tier, futures_json_path, futures_chart_path).""" settings: Settings,
all_tiers: dict[str, tuple[Path, Path]] = { 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": ( "v1": (
settings.ground_truth_v1_file,
settings.ground_truth_futures_v1_file, settings.ground_truth_futures_v1_file,
settings.ground_truth_futures_chart_v1_file, settings.ground_truth_futures_chart_v1_file,
), ),
"v2": ( "v2": (
settings.ground_truth_v2_file,
settings.ground_truth_futures_v2_file, settings.ground_truth_futures_v2_file,
settings.ground_truth_futures_chart_v2_file, settings.ground_truth_futures_chart_v2_file,
), ),
"v3": ( "v3": (
settings.ground_truth_file,
settings.ground_truth_futures_file, settings.ground_truth_futures_file,
settings.ground_truth_futures_chart_v3_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]: def _load_gt(json_path: Path) -> dict[str, Any]:
"""선물 GT JSON을 로드한다.""" """GT JSON을 로드한다."""
with json_path.open(encoding="utf-8") as fp: with json_path.open(encoding="utf-8") as fp:
return json.load(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"] meta = gt_result["meta"]
summary = gt_result["summary"] 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"대상: {meta['symbol']} ({meta['interval_label']})")
print(f"GT 기간: {meta['data_from']} ~ {meta['data_to']}") print(f"GT 기간: {meta['data_from']} ~ {meta['data_to']}")
print( print(
f"선물 GT 타점: 매수 {summary['buy_count']} / 매도 {summary['sell_count']} " f"선물 GT 타점: 매수 {summary['buy_count']} / 매도 {summary['sell_count']} "
f"→ 선물 상방·하방 각 {summary['buy_count']}/{summary['sell_count']} 마커" f"→ 선물 상방·하방 각 {summary['buy_count']}/{summary['sell_count']} 마커"
) )
print(f"JSON: {json_path}")
print(f"차트: {chart_path}") print(f"차트: {chart_path}")
def main() -> int: def main() -> int:
"""CLI 진입점.""" """CLI 진입점."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="선물 GT JSON 기반 Ground Truth 차트 (롱·숏 4색)" description="선물 GT JSON·차트 생성 (현물 GT → 롱·숏 4색)"
) )
parser.add_argument( parser.add_argument(
"--tier", "--tier",
@@ -94,23 +122,29 @@ def main() -> int:
settings = load_settings() settings = load_settings()
tiers = _tier_targets(settings, args.tier) tiers = _tier_targets(settings, args.tier)
print("\n=== 선물 Ground Truth 차트 생성 ===") print("\n=== 선물 Ground Truth 생성 ===")
print("현물 GT 타점 → L↑상방매수 L↓상방매도 S↓하방매수 S↑하방매도") print("현물 GT 타점 → L↑상방매수 L↓상방매도 S↓하방매수 S↑하방매도")
for tier, json_path, chart_path in tiers: for tier, spot_json_path, futures_json_path, chart_path in tiers:
if not json_path.exists(): if not spot_json_path.exists():
logging.error("현물 GT JSON 없음: %s — 먼저 0_ground_truth.py 실행", json_path) logging.error(
"현물 GT JSON 없음: %s — 먼저 0_ground_truth.py 실행",
spot_json_path,
)
return 1 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( render_futures_ground_truth_chart(
db_path=settings.db_path, db_path=settings.db_path,
symbol=settings.symbol, symbol=settings.symbol,
gt_result=gt_result, gt_result=spot_gt,
output_path=chart_path, output_path=chart_path,
chart_lookback_days=settings.download_days, 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 return 0

View File

@@ -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())

View File

@@ -18,7 +18,10 @@ from deepcoin.config import load_settings
from deepcoin.data.candle_loader import load_candles from deepcoin.data.candle_loader import load_candles
from deepcoin.data.intervals import interval_label from deepcoin.data.intervals import interval_label
from deepcoin.evaluation.causal_sim import ( from deepcoin.evaluation.causal_sim import (
best_technique_chart_path,
build_causal_sim_report, build_causal_sim_report,
pick_best_technique_row,
render_best_technique_comparison_chart,
render_causal_sim_html, render_causal_sim_html,
render_technique_sim_chart, render_technique_sim_chart,
run_technique_causal_sim, run_technique_causal_sim,
@@ -145,6 +148,30 @@ def main() -> int:
json_path = save_causal_sim_report(report, settings.causal_sim_report_json) json_path = save_causal_sim_report(report, settings.causal_sim_report_json)
html_path = render_causal_sim_html(report, settings.causal_sim_report_html) 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 elapsed = time.monotonic() - t0
print(f"\n=== 2단계 인과 sim 완료 ({elapsed/60:.1f}분) ===", flush=True) print(f"\n=== 2단계 인과 sim 완료 ({elapsed/60:.1f}분) ===", flush=True)
if stage1_sim: if stage1_sim:

View File

@@ -20,6 +20,7 @@ from deepcoin.evaluation.mtf_report import (
save_mtf_report, save_mtf_report,
) )
from deepcoin.mtf.extractor import MtfFeatureExtractor 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.mtf.store import MultiTimeframeStore
from deepcoin.techniques.runner import load_ground_truth 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) json_path = save_mtf_report(report, settings.mtf_report_json)
html_path = render_mtf_html(report, settings.mtf_report_html) 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", {}) gt = report.get("gt", {})
top = (report.get("global_feature_ranking") or [])[:5] top = (report.get("global_feature_ranking") or [])[:5]
print("\n=== GT v3 MTF 상관 분석 ===") print("\n=== GT v3 MTF 상관 분석 ===")
@@ -123,6 +127,7 @@ def main() -> int:
) )
print(f"\nJSON: {json_path}") print(f"\nJSON: {json_path}")
print(f"HTML: {html_path}") print(f"HTML: {html_path}")
print(f"MTF rules: {rules_path}")
return 0 return 0

View File

@@ -116,7 +116,7 @@ def load_settings(env_path: Path | None = None) -> Settings:
intervals_raw = os.getenv("DOWNLOAD_INTERVALS", default_intervals) intervals_raw = os.getenv("DOWNLOAD_INTERVALS", default_intervals)
intervals = sorted(set(_parse_int_list(intervals_raw))) 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) db_path = Path(db_raw)
if not db_path.is_absolute(): if not db_path.is_absolute():
db_path = _PROJECT_ROOT / db_path 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") os.getenv("GROUND_TRUTH_V2_FILE", "data/spot/ground_truth/ground_truth_trades_v2.json")
), ),
ground_truth_chart_v1_file=_resolve_project_path( 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( 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( 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( ground_truth_futures_file=_resolve_project_path(
os.getenv( os.getenv(
@@ -188,19 +188,19 @@ def load_settings(env_path: Path | None = None) -> Settings:
ground_truth_futures_chart_v1_file=_resolve_project_path( ground_truth_futures_chart_v1_file=_resolve_project_path(
os.getenv( os.getenv(
"GROUND_TRUTH_FUTURES_CHART_V1_FILE", "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( ground_truth_futures_chart_v2_file=_resolve_project_path(
os.getenv( os.getenv(
"GROUND_TRUTH_FUTURES_CHART_V2_FILE", "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( ground_truth_futures_chart_v3_file=_resolve_project_path(
os.getenv( os.getenv(
"GROUND_TRUTH_FUTURES_CHART_V3_FILE", "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( ground_truth_chart_sim_v1_file=_resolve_project_path(

View File

@@ -0,0 +1 @@
"""캔들 수집·저장·조회."""

View File

@@ -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

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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))

View File

@@ -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( def _gt_shell_for_chart(
gt_meta: dict[str, Any], gt_meta: dict[str, Any],
technique: TechniqueResult, technique: TechniqueResult,
@@ -69,7 +75,7 @@ def _gt_shell_for_chart(
"""차트 렌더용 최소 GT 구조를 만든다.""" """차트 렌더용 최소 GT 구조를 만든다."""
return { return {
"meta": { "meta": {
**gt_meta, **_chart_meta_base(gt_meta),
"technique_id": technique.technique_id, "technique_id": technique.technique_id,
"technique_name": technique.technique_name, "technique_name": technique.technique_name,
"stage": "spot_2_causal_sim", "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" 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( def render_technique_sim_chart(
*, *,
db_path: Path, db_path: Path,
@@ -157,6 +224,7 @@ def build_causal_sim_report(
rows.sort(key=lambda r: r["sim_return_pct"], reverse=True) rows.sort(key=lambda r: r["sim_return_pct"], reverse=True)
period_from = benchmark.get("period_from") if benchmark else None period_from = benchmark.get("period_from") if benchmark else None
period_to = benchmark.get("period_to") if benchmark else None period_to = benchmark.get("period_to") if benchmark else None
best = rows[0] if rows else None
return { return {
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "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") "sim_lookback_days": gt_meta.get("sim_lookback_days")
or (benchmark or {}).get("sim_lookback_days"), or (benchmark or {}).get("sim_lookback_days"),
"stage1_benchmark_v3": benchmark, "stage1_benchmark_v3": benchmark,
"best_technique": best,
"best_technique_chart": "causal_sim_chart_best_technique.html",
"ranking": rows, "ranking": rows,
} }
@@ -186,8 +256,20 @@ def render_causal_sim_html(report: dict[str, Any], html_path: Path) -> Path:
"""인과 sim 리포트 HTML을 생성한다.""" """인과 sim 리포트 HTML을 생성한다."""
html_path.parent.mkdir(parents=True, exist_ok=True) html_path.parent.mkdir(parents=True, exist_ok=True)
benchmark = report.get("stage1_benchmark_v3") or {} benchmark = report.get("stage1_benchmark_v3") or {}
best = report.get("best_technique") or {}
best_chart = report.get("best_technique_chart", "")
rows = report.get("ranking", []) rows = report.get("ranking", [])
best_note = ""
if best and best_chart:
best_note = (
f'<p class="meta">수익률 1위 기법 '
f'<a href="{best_chart}">{best.get("technique_name", "")}</a> '
f'({best.get("technique_id", "")}, {best.get("sim_return_pct", 0):+.2f}%) — '
f'1단계 v3 sim 차트와 동일 형식: '
f'<a href="{best_chart}">causal_sim_chart_best_technique.html</a></p>'
)
bench_row = "" bench_row = ""
if benchmark: if benchmark:
bench_row = f""" 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('generated_at', '')}
</p> </p>
<p class="meta">{report.get('description', '')}</p> <p class="meta">{report.get('description', '')}</p>
{best_note}
<table> <table>
<thead> <thead>
<tr> <tr>

View File

@@ -254,7 +254,7 @@ _HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="ko"> <html lang="ko">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Ground Truth Chart</title> <title>DeepCoin Chart</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css" />
<script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
@@ -284,7 +284,7 @@ __EXTRA_STYLES__
</head> </head>
<body> <body>
<header> <header>
<h1 id="title">Ground Truth Chart</h1> <h1 id="title">DeepCoin Chart</h1>
<div class="meta" id="meta"></div> <div class="meta" id="meta"></div>
</header> </header>
__EXTRA_BODY__ __EXTRA_BODY__
@@ -717,6 +717,71 @@ __EXTRA_BODY__
__EXTRA_SCRIPT__ __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() { function init() {
DATA = window.CHART_DATA; DATA = window.CHART_DATA;
if (!DATA) throw new Error("차트 데이터 JS 없음"); if (!DATA) throw new Error("차트 데이터 JS 없음");
@@ -726,35 +791,27 @@ __EXTRA_SCRIPT__
const gtDays = m.gt_lookback_days || m.lookback_days; const gtDays = m.gt_lookback_days || m.lookback_days;
const chartLabel = chartDays >= 365 ? `${Math.round(chartDays / 365)}년` : `${chartDays}일`; const chartLabel = chartDays >= 365 ? `${Math.round(chartDays / 365)}년` : `${chartDays}일`;
const gtLabel = gtDays >= 365 ? `${Math.round(gtDays / 365)}년` : `${gtDays}일`; const gtLabel = gtDays >= 365 ? `${Math.round(gtDays / 365)}년` : `${gtDays}일`;
const tier = m.chart_tier ? ` ${m.chart_tier.toUpperCase()}` : ""; const pres = resolveChartPresentation(m, simMode, chartLabel, gtLabel, DATA.sim_pnl);
const simSuffix = simMode ? " · 1단계 sim" : ""; document.title = pres.pageTitle;
document.getElementById("title").textContent = document.getElementById("title").textContent = pres.title;
`${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel}${simSuffix}`;
document.getElementById("btn-all").textContent = `전체 ${chartLabel}`; document.getElementById("btn-all").textContent = `전체 ${chartLabel}`;
const chartFrom = m.chart_data_from || m.data_from; const chartFrom = m.chart_data_from || m.data_from;
const chartTo = m.chart_data_to || m.data_to; 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 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)}` ? `체결 ${DATA.buy_markers.length}/${DATA.sell_markers.length} · ${m.sim_period_from.slice(0, 16)} ~ ${(m.sim_period_to || chartTo).slice(0, 16)}`
: gtLabel; : gtLabel;
const legendExtra = simMode ? " | ▼보라=거래시작" : ""; const legendExtra = simMode ? " | ▼보라=거래시작" : "";
document.getElementById("meta").textContent = let metaLine =
`차트 ${chartFrom} ~ ${chartTo} (${DATA.bar_count.toLocaleString()}봉) | 매수 ${DATA.buy_markers.length} / 매도 ${DATA.sell_markers.length} (${markerRange}) | ${legend}${legendExtra}`; `차트 ${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) { 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"); const panelTitle = document.getElementById("sim-panel-title");
if (panelTitle) { if (panelTitle && pres.panelTitle) {
const initCash = DATA.sim_pnl?.initial_cash_krw || 0; panelTitle.textContent = pres.panelTitle;
const initLabel = initCash ? `${Math.round(initCash).toLocaleString()}원` : "";
panelTitle.textContent = `1단계 수익 sim (${simLabel}${initLabel ? ` · 초기 ${initLabel}` : ""})`;
} }
renderSimPanel(); renderSimPanel();
} }
@@ -834,7 +891,7 @@ _SIM_EXTRA_STYLES = """
_SIM_EXTRA_BODY = """ _SIM_EXTRA_BODY = """
<section class="sim-panel" id="sim-panel"> <section class="sim-panel" id="sim-panel">
<h2 id="sim-panel-title">1단계 수익 sim</h2> <h2 id="sim-panel-title">수익 sim</h2>
<div class="sim-grid" id="sim-grid"></div> <div class="sim-grid" id="sim-grid"></div>
<div class="sim-note" id="sim-note"></div> <div class="sim-note" id="sim-note"></div>
</section> </section>
@@ -910,7 +967,8 @@ _SIM_EXTRA_SCRIPT = """
`시뮬 기간: ${p.period_from} ~ ${p.period_to} (${p.sim_lookback_days}일) | ` + `시뮬 기간: ${p.period_from} ~ ${p.period_to} (${p.sim_lookback_days}일) | ` +
`신호 ${p.signals_in_period}건 | 분할매수/매도 클러스터 적용 | ` + `신호 ${p.signals_in_period}건 | 분할매수/매도 클러스터 적용 | ` +
`스킵 매수 ${p.buys_skipped} / 매도 ${p.sells_skipped} | 수수료 ${(p.fee_rate * 100).toFixed(2)}%` + `스킵 매수 ${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"); const tbody = document.getElementById("trade-body");
tbody.innerHTML = ""; tbody.innerHTML = "";
(p.trades || []).forEach(t => { (p.trades || []).forEach(t => {