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
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·분석·운영

17
.gitignore vendored
View File

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

539
README.md
View File

@@ -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: 캔들 수집 모듈 초기 구현

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

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.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:

View File

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

View File

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

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(
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'<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 = ""
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', '')}
</p>
<p class="meta">{report.get('description', '')}</p>
{best_note}
<table>
<thead>
<tr>

View File

@@ -254,7 +254,7 @@ _HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="ko">
<head>
<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" />
<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>
@@ -284,7 +284,7 @@ __EXTRA_STYLES__
</head>
<body>
<header>
<h1 id="title">Ground Truth Chart</h1>
<h1 id="title">DeepCoin Chart</h1>
<div class="meta" id="meta"></div>
</header>
__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 = """
<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-note" id="sim-note"></div>
</section>
@@ -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 => {