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

수익률 1위 기법 ' + f'{best.get("technique_name", "")} ' + f'({best.get("technique_id", "")}, {best.get("sim_return_pct", 0):+.2f}%) — ' + f'1단계 v3 sim 차트와 동일 형식: ' + f'causal_sim_chart_best_technique.html

' + ) + bench_row = "" if benchmark: bench_row = f""" @@ -242,6 +324,7 @@ def render_causal_sim_html(report: dict[str, Any], html_path: Path) -> Path: 생성: {report.get('generated_at', '')}

{report.get('description', '')}

+ {best_note} diff --git a/src/deepcoin/ground_truth/chart.py b/src/deepcoin/ground_truth/chart.py index 7df2545..cda6aaf 100644 --- a/src/deepcoin/ground_truth/chart.py +++ b/src/deepcoin/ground_truth/chart.py @@ -254,7 +254,7 @@ _HTML_TEMPLATE = """ - Ground Truth Chart + DeepCoin Chart @@ -284,7 +284,7 @@ __EXTRA_STYLES__
-

Ground Truth Chart

+

DeepCoin Chart

__EXTRA_BODY__ @@ -717,6 +717,71 @@ __EXTRA_BODY__ __EXTRA_SCRIPT__ + function resolveChartPresentation(m, simMode, chartLabel, gtLabel, simPnl) { + const stage = m.stage || ""; + const techniqueName = m.technique_name || ""; + const techniqueId = m.technique_id || ""; + const simDays = (simPnl && simPnl.sim_lookback_days) || 1095; + const simLabel = simDays >= 365 + ? `최근 ${Math.round(simDays / 365)}년` + : `최근 ${simDays}일`; + const initCash = (simPnl && simPnl.initial_cash_krw) || 0; + const initLabel = initCash ? ` · 초기 ${Math.round(initCash).toLocaleString()}원` : ""; + + if (stage === "spot_2_causal_sim_best") { + const bench = m.stage1_benchmark_return_pct; + const benchNote = Number.isFinite(bench) + ? ` | 1단계 v3 GT sim ${bench >= 0 ? "+" : ""}${bench.toFixed(2)}%` + : ""; + return { + pageTitle: `${m.symbol} 인과 sim — ${techniqueName}`, + title: `${m.symbol} 2단계 인과 sim · 최고 수익 (${techniqueName}, ${m.interval_label}) — 차트 ${chartLabel}`, + panelTitle: `2단계 인과 sim · 최고 수익 기법 (${simLabel}${initLabel})`, + legend: "B=매수 S=매도 | 과거 데이터만 · 인과 기법 신호", + simNoteExtra: `기법 ${techniqueId} | 미래 데이터 미사용${benchNote}`, + }; + } + if (stage === "spot_2_causal_sim" || techniqueId) { + return { + pageTitle: `${m.symbol} 인과 sim — ${techniqueName}`, + title: `${m.symbol} 2단계 인과 sim (${techniqueName}, ${m.interval_label}) — 차트 ${chartLabel}`, + panelTitle: `2단계 인과 sim (${simLabel}${initLabel})`, + legend: "B=매수 S=매도 | 과거 데이터만 · 인과 기법 신호", + simNoteExtra: `기법 ${techniqueId} | 미래 데이터 미사용`, + }; + } + if (simMode) { + const tier = m.chart_tier ? ` ${String(m.chart_tier).toUpperCase()}` : ""; + const tierKey = (m.chart_tier || "v3").toLowerCase(); + const legend = tierKey === "v1" + ? "B=스윙매수 S=스윙매도" + : tierKey === "v2" + ? "B/S=스윙 B*=눌림목" + : "B/S=스윙 B*=눌림 B^=돌파 Bd/Sd=다이버전스"; + return { + pageTitle: `${m.symbol} GT sim${tier}`, + title: `${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel} · 1단계 sim`, + panelTitle: `1단계 GT sim (${simLabel}${initLabel}) · 사후 최적 타점`, + legend: legend, + simNoteExtra: "사후 GT 타점 · 미래 데이터 사용 (벤치마크)", + }; + } + const tier = m.chart_tier ? ` ${String(m.chart_tier).toUpperCase()}` : ""; + const tierKey = (m.chart_tier || "v3").toLowerCase(); + const legend = tierKey === "v1" + ? "B=스윙매수 S=스윙매도" + : tierKey === "v2" + ? "B/S=스윙 B*=눌림목" + : "B/S=스윙 B*=눌림 B^=돌파 Bd/Sd=다이버전스"; + return { + pageTitle: `${m.symbol} Ground Truth${tier}`, + title: `${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel}`, + panelTitle: "", + legend: legend, + simNoteExtra: "", + }; + } + function init() { DATA = window.CHART_DATA; if (!DATA) throw new Error("차트 데이터 JS 없음"); @@ -726,35 +791,27 @@ __EXTRA_SCRIPT__ const gtDays = m.gt_lookback_days || m.lookback_days; const chartLabel = chartDays >= 365 ? `${Math.round(chartDays / 365)}년` : `${chartDays}일`; const gtLabel = gtDays >= 365 ? `${Math.round(gtDays / 365)}년` : `${gtDays}일`; - const tier = m.chart_tier ? ` ${m.chart_tier.toUpperCase()}` : ""; - const simSuffix = simMode ? " · 1단계 sim" : ""; - document.getElementById("title").textContent = - `${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel}${simSuffix}`; + const pres = resolveChartPresentation(m, simMode, chartLabel, gtLabel, DATA.sim_pnl); + document.title = pres.pageTitle; + document.getElementById("title").textContent = pres.title; document.getElementById("btn-all").textContent = `전체 ${chartLabel}`; const chartFrom = m.chart_data_from || m.data_from; const chartTo = m.chart_data_to || m.data_to; - const tierKey = (m.chart_tier || "v3").toLowerCase(); - const legend = tierKey === "v1" - ? "B=스윙매수 S=스윙매도" - : tierKey === "v2" - ? "B/S=스윙 B*=눌림목" - : "B/S=스윙 B*=눌림 B^=돌파 Bd/Sd=다이버전스"; const markerRange = simMode && m.sim_period_from ? `체결 ${DATA.buy_markers.length}/${DATA.sell_markers.length} · ${m.sim_period_from.slice(0, 16)} ~ ${(m.sim_period_to || chartTo).slice(0, 16)}` : gtLabel; const legendExtra = simMode ? " | ▼보라=거래시작" : ""; - document.getElementById("meta").textContent = - `차트 ${chartFrom} ~ ${chartTo} (${DATA.bar_count.toLocaleString()}봉) | 매수 ${DATA.buy_markers.length} / 매도 ${DATA.sell_markers.length} (${markerRange}) | ${legend}${legendExtra}`; + let metaLine = + `차트 ${chartFrom} ~ ${chartTo} (${DATA.bar_count.toLocaleString()}봉) | 매수 ${DATA.buy_markers.length} / 매도 ${DATA.sell_markers.length} (${markerRange}) | ${pres.legend}${legendExtra}`; + if (m.comparison_label) { + metaLine += ` | 대조: ${m.comparison_label}`; + } + document.getElementById("meta").textContent = metaLine; + window.__SIM_NOTE_EXTRA__ = pres.simNoteExtra || ""; if (simMode) { - const simDays = DATA.sim_pnl.sim_lookback_days || 1095; - const simLabel = simDays >= 365 - ? `최근 ${Math.round(simDays / 365)}년` - : `최근 ${simDays}일`; const panelTitle = document.getElementById("sim-panel-title"); - if (panelTitle) { - const initCash = DATA.sim_pnl?.initial_cash_krw || 0; - const initLabel = initCash ? `${Math.round(initCash).toLocaleString()}원` : ""; - panelTitle.textContent = `1단계 수익 sim (${simLabel}${initLabel ? ` · 초기 ${initLabel}` : ""})`; + if (panelTitle && pres.panelTitle) { + panelTitle.textContent = pres.panelTitle; } renderSimPanel(); } @@ -834,7 +891,7 @@ _SIM_EXTRA_STYLES = """ _SIM_EXTRA_BODY = """
-

1단계 수익 sim

+

수익 sim

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