refactor: 프로젝트명 bithumb으로 변경 및 futures 파이프라인 제거

deepcoin 패키지를 bithumb으로 rename하고, 3단계 live 운영·사이징 튜닝·텔레그램 알림을 통합한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-06-13 17:47:11 +09:00
parent 4890abd9a6
commit c3334e4f77
129 changed files with 1846 additions and 1642 deletions

View File

@@ -1,4 +1,4 @@
# DeepCoin — .env.example (비밀값 없음). 복사: cp .env.example .env # Bithumb — .env.example (비밀값 없음). 복사: cp .env.example .env
# --- 빗썸 API (캔들 수집은 Public API, 키 선택) --- # --- 빗썸 API (캔들 수집은 Public API, 키 선택) ---
BITHUMB_ACCESS_KEY= BITHUMB_ACCESS_KEY=
@@ -52,14 +52,6 @@ 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_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 GROUND_TRUTH_CHART_V3_FILE=docs/spot/0_ground_truth/ground_truth_chart_v3.html
# --- 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/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 --- # --- 현물 1단계: GT sim ---
GROUND_TRUTH_CHART_SIM_V1_FILE=docs/spot/1_simulation/ground_truth_chart_sim_v1.html GROUND_TRUTH_CHART_SIM_V1_FILE=docs/spot/1_simulation/ground_truth_chart_sim_v1.html
GROUND_TRUTH_CHART_SIM_V2_FILE=docs/spot/1_simulation/ground_truth_chart_sim_v2.html GROUND_TRUTH_CHART_SIM_V2_FILE=docs/spot/1_simulation/ground_truth_chart_sim_v2.html
@@ -78,13 +70,21 @@ CAUSAL_SIM_REPORT_JSON=docs/spot/2_analysis/causal_sim_report.json
CAUSAL_SIM_REPORT_HTML=docs/spot/2_analysis/causal_sim_report.html CAUSAL_SIM_REPORT_HTML=docs/spot/2_analysis/causal_sim_report.html
# --- 현물 3단계: 운영 (기본 paper) --- # --- 현물 3단계: 운영 (기본 paper) ---
# live 전환: OPS_MODE=live → bash scripts/3_run_fractal_live.sh
OPS_MODE=paper OPS_MODE=paper
OPS_TECHNIQUE_ID=fractal_swing OPS_TECHNIQUE_ID=fractal_swing
OPS_MTF_ENABLED=false OPS_MTF_ENABLED=false
OPS_TREND_GATE_ENABLED=false OPS_TREND_GATE_ENABLED=false
OPS_DAILY_MAX_TRADES=100 OPS_DAILY_MAX_TRADES=100
OPS_MIN_ORDER_KRW=5000 OPS_MIN_ORDER_KRW=5000
# 1회 매수·매도 분할 (0.10=총평가/보유의 10%, 20만원→약 2만원/회)
OPS_BUY_CASH_PCT=0.10
OPS_SELL_COIN_PCT=0.10
OPS_SLIPPAGE_RATE=0.0005 OPS_SLIPPAGE_RATE=0.0005
# 빗썸 시장가 매수 시 주문금액+예약수수료 lock (전액 주문 400 방지)
OPS_EXCHANGE_FEE_LOCK_RATE=0.0025
# live 시장가 매수 안전 여유: floor(가용/(1+lock)) - N원
OPS_BUY_SAFETY_BUFFER_KRW=1000
OPS_ORDER_INTERVAL_SEC=0.35 OPS_ORDER_INTERVAL_SEC=0.35
OPS_SYNC_CANDLES=true OPS_SYNC_CANDLES=true
# 비우면 DOWNLOAD_INTERVALS 전체 증분 sync # 비우면 DOWNLOAD_INTERVALS 전체 증분 sync
@@ -94,9 +94,10 @@ OPS_SIGNAL_TAIL_BARS=800
OPS_STATE_JSON=data/spot/operations/fractal_ops_state.json OPS_STATE_JSON=data/spot/operations/fractal_ops_state.json
OPS_REPORT_JSON=docs/spot/3_operations/fractal_ops_report.json OPS_REPORT_JSON=docs/spot/3_operations/fractal_ops_report.json
OPS_FILTERED_BACKTEST_JSON=docs/spot/3_operations/fractal_filtered_backtest_report.json OPS_FILTERED_BACKTEST_JSON=docs/spot/3_operations/fractal_filtered_backtest_report.json
# 1_tune_order_sizing.py 학습 결과 (sim/live/backtest 공통)
OPS_SIZING_RULES_JSON=data/spot/operations/sizing_rules.json
# composite_v3 운영 시: OPS_TECHNIQUE_ID=composite_v3, OPS_MIN_SCORE=2.5, OPS_MTF_ENABLED=true # composite_v3 운영 시: OPS_TECHNIQUE_ID=composite_v3, OPS_MIN_SCORE=2.5, OPS_MTF_ENABLED=true
# 폴더 구조: data|docs / {common, spot, futures} # 폴더 구조: data|docs / {common, spot}
# common — coins.db 등 공유 리소스 # common — coins.db 등 공유 리소스
# spot — 현물 GT·기법·분석·운영 # spot — 현물 GT·기법·분석·운영
# futures — 선물 GT·분석·운영

View File

@@ -1,15 +1,15 @@
# DeepCoin # Bithumb
빗썸 KRW 마켓 암호화폐 캔들 수집 및 **현물**·**선물** 매매 전략 파이프라인. 빗썸 KRW 마켓 암호화폐 캔들 수집 및 **현물** 매매 전략 파이프라인.
- **기본 축:** 3분봉 현물 BTC, 최근 **10년** 캔들 (`DOWNLOAD_DAYS=3650`) - **기본 축:** 3분봉 현물 BTC, 최근 **10년** 캔들 (`DOWNLOAD_DAYS=3650`)
- **데이터·문서 분류:** `common` (공유) · `spot` (현물) · `futures` (선물) - **데이터·문서 분류:** `common` (공유) · `spot` (현물)
- **현재 운영 전략:** `fractal_swing` + MTF off — paper/live tick 운영 구현 완료 - **현재 운영 전략:** `fractal_swing` + MTF off — paper/live tick 운영 구현 완료
## 주요 기능 ## 주요 기능
- 빗썸 Public API(v1) 분·일·주·월봉 캔들 수집 (11개 TF, 1분봉 포함) - 빗썸 Public API(v1) 분·일·주·월봉 캔들 수집 (11개 TF, 1분봉 포함)
- SQLite 공유 DB (`data/common/coins.db`) — 현물·선물·MTF 공용 - SQLite 공유 DB (`data/common/coins.db`) — 현물·MTF 공용
- Ground Truth(GT) 벤치마크 → 39종 인과 기법 분석 → **실거래 운영(paper/live)** - Ground Truth(GT) 벤치마크 → 39종 인과 기법 분석 → **실거래 운영(paper/live)**
- 운영 tick: 캔들 증분 sync, 신호 tail 갱신, 슬리피지·일 체결 상한, 텔레그램 체결 알림 - 운영 tick: 캔들 증분 sync, 신호 tail 갱신, 슬리피지·일 체결 상한, 텔레그램 체결 알림
@@ -21,7 +21,7 @@
## 설치 ## 설치
```bash ```bash
cd DeepCoin cd Bithumb
conda activate ncue # 또는 xavis conda activate ncue # 또는 xavis
pip install -r requirements.txt pip install -r requirements.txt
cp .env.example .env # API 키·텔레그램 등 로컬 설정 cp .env.example .env # API 키·텔레그램 등 로컬 설정
@@ -56,7 +56,6 @@ OPS_DAILY_MAX_TRADES=100
| **spot 1단계** | GT 타점 완벽 추종 sim 상한선 | GT 자체가 사후 | 불가 | | **spot 1단계** | GT 타점 완벽 추종 sim 상한선 | GT 자체가 사후 | 불가 |
| **spot 2단계** | 39종 인과 기법 평가·MTF 규칙 | 미사용 | 불가 | | **spot 2단계** | 39종 인과 기법 평가·MTF 규칙 | 미사용 | 불가 |
| **spot 3단계** | paper/live tick 운영 | 미사용 | **가능** | | **spot 3단계** | paper/live tick 운영 | 미사용 | **가능** |
| **futures 0단계** | 현물 GT → 선물 롱·숏 마커 | — | — |
### 현물 3단계 운영 아키텍처 (fractal_swing) ### 현물 3단계 운영 아키텍처 (fractal_swing)
@@ -93,8 +92,8 @@ flowchart TD
## 폴더 구조 ## 폴더 구조
```text ```text
DeepCoin/ Bithumb/
├── src/deepcoin/ ├── src/bithumb/
│ ├── api/ # 빗썸 Public·Private REST │ ├── api/ # 빗썸 Public·Private REST
│ ├── data/ # 캔들 수집·DB·로더 │ ├── data/ # 캔들 수집·DB·로더
│ ├── ground_truth/ # GT 타점·sim·차트 │ ├── ground_truth/ # GT 타점·sim·차트
@@ -112,16 +111,14 @@ DeepCoin/
│ │ ├── techniques/ # 2단계 기법 결과 (fractal_swing.json 등) │ │ ├── techniques/ # 2단계 기법 결과 (fractal_swing.json 등)
│ │ ├── mtf/ # mtf_rules_v3.json │ │ ├── mtf/ # mtf_rules_v3.json
│ │ └── operations/ # fractal_ops_state.json │ │ └── operations/ # fractal_ops_state.json
│ └── futures/ground_truth/ # 선물 GT JSON
└── docs/ └── docs/
├── live/ # 운영 백테스트 매매 차트 (index.html) ├── live/ # 운영 백테스트 매매 차트 (index.html)
── spot/ ── spot/
├── 0_ground_truth/ # GT 차트 HTML ├── 0_ground_truth/ # GT 차트 HTML
├── 1_simulation/ # 1단계 sim 차트 ├── 1_simulation/ # 1단계 sim 차트
├── 2_analysis/ # 2단계 리포트·설계 가이드 ├── 2_analysis/ # 2단계 리포트·설계 가이드
└── 3_operations/ # 운영·백테스트 JSON 리포트 └── 3_operations/ # 운영·백테스트 JSON 리포트
└── futures/0_ground_truth/ # 선물 GT 차트
``` ```
테이블명: `{SYMBOL}_{인터벌분}` (예: `BTC_3`, `BTC_1440`). 인터벌: 분봉=분 숫자, 일=`1440`, 주=`10080`, 월=`43200`. 테이블명: `{SYMBOL}_{인터벌분}` (예: `BTC_3`, `BTC_1440`). 인터벌: 분봉=분 숫자, 일=`1440`, 주=`10080`, 월=`43200`.
@@ -136,7 +133,6 @@ flowchart LR
B --> C[1_ground_truth_sim] B --> C[1_ground_truth_sim]
C --> D[2_run_stage2_all] C --> D[2_run_stage2_all]
D --> E[3_run_operations] D --> E[3_run_operations]
B --> F[0_ground_truth_futures]
``` ```
| 순서 | 단계 | 스크립트 | 산출물 | | 순서 | 단계 | 스크립트 | 산출물 |
@@ -146,7 +142,6 @@ flowchart LR
| 2 | spot 1단계 | `1_ground_truth_sim.py` | `docs/spot/1_simulation/` | | 2 | spot 1단계 | `1_ground_truth_sim.py` | `docs/spot/1_simulation/` |
| 3 | spot 2단계 | `2_run_*.py`, `2_run_stage2_all.sh` | `data/spot/techniques/`, `docs/spot/2_analysis/` | | 3 | spot 2단계 | `2_run_*.py`, `2_run_stage2_all.sh` | `data/spot/techniques/`, `docs/spot/2_analysis/` |
| 4 | spot 3단계 | `3_run_*.py`, `3_run_fractal_ops.sh` | `data/spot/operations/`, `docs/spot/3_operations/` | | 4 | spot 3단계 | `3_run_*.py`, `3_run_fractal_ops.sh` | `data/spot/operations/`, `docs/spot/3_operations/` |
| — | futures 0단계 | `0_ground_truth_futures.py` | `data/futures/ground_truth/`, `docs/futures/0_ground_truth/` |
### 권장 명령 ### 권장 명령
@@ -163,9 +158,6 @@ python scripts/0_ground_truth.py --interval 3 --days 3650 --tier all
python scripts/1_ground_truth_sim.py --tier all python scripts/1_ground_truth_sim.py --tier all
bash scripts/2_run_stage2_all.sh bash scripts/2_run_stage2_all.sh
# futures 0단계
python scripts/0_ground_truth_futures.py --tier all
# spot 3단계 — fractal_swing 운영 # spot 3단계 — fractal_swing 운영
python scripts/3_run_filtered_backtest.py # 운영 조건 3년 sim 검증 python scripts/3_run_filtered_backtest.py # 운영 조건 3년 sim 검증
python scripts/3_render_live_chart.py # docs/live 매매 차트 python scripts/3_render_live_chart.py # docs/live 매매 차트
@@ -274,12 +266,6 @@ OPS_TREND_GATE_ENABLED=true
OPS_DAILY_MAX_TRADES=20 OPS_DAILY_MAX_TRADES=20
``` ```
### futures 0단계 — 선물 GT
현물 GT buy/sell → 롱·숏 4색 마커 (L↑/L↓/S↑/S↓).
산출: `data/futures/ground_truth/`, `docs/futures/0_ground_truth/`
선물 1~3단계는 예정.
--- ---
## 환경 변수 ## 환경 변수
@@ -359,7 +345,7 @@ OPS_DAILY_MAX_TRADES=20
## 현물 2단계 인과 기법 (39종) ## 현물 2단계 인과 기법 (39종)
`src/deepcoin/techniques/` — 단일 33 + 복합 6, 미래 데이터 미사용. `src/bithumb/techniques/` — 단일 33 + 복합 6, 미래 데이터 미사용.
| ID | 기법 | 유형 | | ID | 기법 | 유형 |
|----|------|------| |----|------|------|
@@ -412,18 +398,17 @@ OPS_DAILY_MAX_TRADES=20
| common | 캔들 수집·증분 sync | 구현됨 | | common | 캔들 수집·증분 sync | 구현됨 |
| spot | 0~2단계 (GT·기법·MTF) | 구현됨 | | spot | 0~2단계 (GT·기법·MTF) | 구현됨 |
| spot | 3단계 (fractal paper/live·백테스트·텔레그램) | **구현됨** | | spot | 3단계 (fractal paper/live·백테스트·텔레그램) | **구현됨** |
| futures | 0단계 GT | 구현됨 |
| futures | 1~3단계 | 예정 |
--- ---
## 변경 이력 ## 변경 이력
- 2026-06-13: 프로젝트명 **Bithumb**으로 변경, 선물(futures) 파이프라인 제거
- 2026-06-13: 텔레그램 매수·매도 체결 알림 (`notifications/telegram.py`) - 2026-06-13: 텔레그램 매수·매도 체결 알림 (`notifications/telegram.py`)
- 2026-06-13: `docs/live/` 운영 백테스트 매매 차트 (`3_render_live_chart.py`) - 2026-06-13: `docs/live/` 운영 백테스트 매매 차트 (`3_render_live_chart.py`)
- 2026-06-13: fractal_swing live 운영 — 슬리피지·일 체결 상한·전 TF 증분 sync·신호 tail 갱신 - 2026-06-13: fractal_swing live 운영 — 슬리피지·일 체결 상한·전 TF 증분 sync·신호 tail 갱신
- 2026-06-13: 운영 백테스트 **+1,873,140%** (3년, 슬리피지 0.05%, 일 100회) 검증 - 2026-06-13: 운영 백테스트 **+1,873,140%** (3년, 슬리피지 0.05%, 일 100회) 검증
- 2026-06-12: `data/`·`docs/` common/spot/futures 3유형 구조 재편 - 2026-06-12: `data/`·`docs/` common/spot 구조 재편
- 2026-06-12: 3단계 운영 파이프라인 초기 구현 (composite_v3 + MTF paper/live) - 2026-06-12: 3단계 운영 파이프라인 초기 구현 (composite_v3 + MTF paper/live)
- 2026-06-12: 2단계 인과 기법 분석 파이프라인 완료 - 2026-06-12: 2단계 인과 기법 분석 파이프라인 완료
- 2026-06-08: Ground Truth v1/v2/v3 - 2026-06-08: Ground Truth v1/v2/v3

View File

@@ -2,7 +2,7 @@
<html lang="ko"> <html lang="ko">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>DeepCoin Live — 운영 백테스트 차트</title> <title>Bithumb Live — 운영 백테스트 차트</title>
<style> <style>
body { font-family: "Malgun Gothic", Arial, sans-serif; margin: 32px; color: #333; background: #f5f5f5; } body { font-family: "Malgun Gothic", Arial, sans-serif; margin: 32px; color: #333; background: #f5f5f5; }
h1 { font-size: 22px; margin-bottom: 8px; } h1 { font-size: 22px; margin-bottom: 8px; }
@@ -15,23 +15,23 @@
</style> </style>
</head> </head>
<body> <body>
<h1>DeepCoin Live — 운영 백테스트</h1> <h1>Bithumb Live — 운영 백테스트</h1>
<p class="meta"> <p class="meta">
BTC · 프랙탈 스윙 (fractal_swing)<br> BTC · 프랙탈 스윙 (fractal_swing)<br>
sim 기간: 최근 1095일 · sim 기간: 최근 1095일 ·
슬리피지 0.05% · 슬리피지 0.05% ·
일 체결 상한 100 · 일 체결 상한 100 ·
MTF off MTF off<br>학습 비율: 매수 100% · 매도 100% (클러스터별 규칙 적용)
</p> </p>
<div class="card"> <div class="card">
<div>3년 수익률 (운영 규칙 sim)</div> <div>3년 수익률 (운영 규칙 sim)</div>
<div class="stat">+1874019.75%</div> <div class="stat">+1885460.27%</div>
<p> <p>
<a href="fractal_swing_ops_chart.html">매수·매도 타점 차트 열기</a> <a href="fractal_swing_ops_chart.html">매수·매도 타점 차트 열기</a>
</p> </p>
<ul> <ul>
<li>매수 53,521 / 매도 53,449 체결</li> <li>매수 53,519 / 매도 53,444 체결</li>
<li>초기 200,000원 → 최종 3,748,239,499원</li> <li>초기 200,000원 → 최종 3,771,120,549원</li>
<li>차트: B=매수 S=매도 마커, 이전/다음 타점 탐색, 기간 줌</li> <li>차트: B=매수 S=매도 마커, 이전/다음 타점 탐색, 기간 줌</li>
</ul> </ul>
</div> </div>

View File

@@ -1,6 +1,6 @@
# 현물 2단계 설계 가이드 # 현물 2단계 설계 가이드
> DeepCoin 현물 파이프라인 2단계(인과 기법 분석)의 목적, 구조, 설계 근거를 정리한 문서입니다. > Bithumb 현물 파이프라인 2단계(인과 기법 분석)의 목적, 구조, 설계 근거를 정리한 문서입니다.
> 작성 기준: 2026-06-12 · 기본 TF: 3분봉 · GT: v3 > 작성 기준: 2026-06-12 · 기본 TF: 3분봉 · GT: v3
--- ---
@@ -57,7 +57,7 @@
### 파이프라인 실행 ### 파이프라인 실행
```bash ```bash
cd DeepCoin cd Bithumb
export PYTHONPATH=src export PYTHONPATH=src
bash scripts/2_run_stage2_all.sh bash scripts/2_run_stage2_all.sh
``` ```
@@ -119,7 +119,7 @@ bash scripts/2_run_stage2_all.sh
1, 3, 5, 10, 15, 30, 60, 240분, 일(1440), 주(10080), 월(43200) 1, 3, 5, 10, 15, 30, 60, 240분, 일(1440), 주(10080), 월(43200)
**TF별 피처** (`src/deepcoin/mtf/features.py`) **TF별 피처** (`src/bithumb/mtf/features.py`)
| 피처 | 용도 | | 피처 | 용도 |
|------|------| |------|------|
@@ -224,7 +224,7 @@ flowchart TD
- 주봉 하락 추세 3분 돌파 매수 → 가짜 돌파 - 주봉 하락 추세 3분 돌파 매수 → 가짜 돌파
- 월봉 과매도 3분 매도 → 바닥 청산 - 월봉 과매도 3분 매도 → 바닥 청산
등의 문제가 발생합니다. DeepCoin은 이를 **MTF 레이어**로 보완합니다. 등의 문제가 발생합니다. Bithumb은 이를 **MTF 레이어**로 보완합니다.
| 상황 | MTF 해석 | 의도 | | 상황 | MTF 해석 | 의도 |
|------|----------|------| |------|----------|------|
@@ -299,12 +299,12 @@ flowchart TD
| 모듈 | 경로 | | 모듈 | 경로 |
|------|------| |------|------|
| 기법 실행 | `scripts/2_run_techniques.py`, `src/deepcoin/techniques/runner.py` | | 기법 실행 | `scripts/2_run_techniques.py`, `src/bithumb/techniques/runner.py` |
| GT 정합 | `src/deepcoin/evaluation/gt_align.py` | | GT 정합 | `src/bithumb/evaluation/gt_align.py` |
| MTF 피처 | `src/deepcoin/mtf/features.py`, `extractor.py`, `store.py` | | MTF 피처 | `src/bithumb/mtf/features.py`, `extractor.py`, `store.py` |
| MTF 규칙 | `src/deepcoin/mtf/rules.py` | | MTF 규칙 | `src/bithumb/mtf/rules.py` |
| MTF 필터 | `src/deepcoin/mtf/filter.py`, `trend_gate.py` | | MTF 필터 | `src/bithumb/mtf/filter.py`, `trend_gate.py` |
| 통합 기법 | `src/deepcoin/techniques/composite_v3.py` | | 통합 기법 | `src/bithumb/techniques/composite_v3.py` |
--- ---

View File

@@ -1,6 +1,6 @@
# 현물 2단계 최종 정리 — 결과 해석 및 운영 권고 # 현물 2단계 최종 정리 — 결과 해석 및 운영 권고
> DeepCoin 현물 파이프라인 2단계(인과 기법 분석) 완료 후 종합 정리 문서 > Bithumb 현물 파이프라인 2단계(인과 기법 분석) 완료 후 종합 정리 문서
> 작성 기준: 2026-06-12 · 데이터: BTC · 3분봉 · GT v3 · 분석 기간 3650일 · sim 기간 최근 3년(1095일) > 작성 기준: 2026-06-12 · 데이터: BTC · 3분봉 · GT v3 · 분석 기간 3650일 · sim 기간 최근 3년(1095일)
--- ---

View File

@@ -47,7 +47,7 @@ paper / live 체결 (구간별 매수 상한 동일)
| 일괄 | `3_run_stage3_all.sh` | 3-1 + 3-2 paper | | 일괄 | `3_run_stage3_all.sh` | 3-1 + 3-2 paper |
```bash ```bash
cd DeepCoin cd Bithumb
export PYTHONPATH=src export PYTHONPATH=src
# MTF 필터 백테스트 # MTF 필터 백테스트
@@ -87,12 +87,12 @@ bash scripts/3_run_stage3_all.sh
| 모듈 | 경로 | | 모듈 | 경로 |
|------|------| |------|------|
| 신호 파이프라인 | `src/deepcoin/operations/signal_pipeline.py` | | 신호 파이프라인 | `src/bithumb/operations/signal_pipeline.py` |
| signal_type 추론 | `src/deepcoin/operations/signal_type.py` | | signal_type 추론 | `src/bithumb/operations/signal_type.py` |
| 체결 엔진 | `src/deepcoin/operations/trade_engine.py` | | 체결 엔진 | `src/bithumb/operations/trade_engine.py` |
| paper/live | `src/deepcoin/operations/executor.py` | | paper/live | `src/bithumb/operations/executor.py` |
| 러너 | `src/deepcoin/operations/runner.py` | | 러너 | `src/bithumb/operations/runner.py` |
| 빗썸 Private | `src/deepcoin/api/bithumb_private.py` | | 빗썸 Private | `src/bithumb/api/bithumb_private.py` |
--- ---

View File

@@ -16,10 +16,10 @@ if str(SRC) not in sys.path:
from dataclasses import replace from dataclasses import replace
from deepcoin.config import load_settings from bithumb.config import load_settings
from deepcoin.data.candle_store import CandleStore from bithumb.data.candle_store import CandleStore
from deepcoin.data.downloader import CandleDownloader from bithumb.data.downloader import CandleDownloader
from deepcoin.data.intervals import INTERVAL_1MIN, estimate_download_requests, interval_label from bithumb.data.intervals import INTERVAL_1MIN, estimate_download_requests, interval_label
def _configure_logging(verbose: bool) -> None: def _configure_logging(verbose: bool) -> None:

View File

@@ -15,10 +15,10 @@ SRC = ROOT / "src"
if str(SRC) not in sys.path: if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import Settings, load_settings from bithumb.config import Settings, load_settings
from deepcoin.data.intervals import interval_label from bithumb.data.intervals import interval_label
from deepcoin.ground_truth.chart import render_ground_truth_chart from bithumb.ground_truth.chart import render_ground_truth_chart
from deepcoin.ground_truth.ground_truth import GtParams, build_ground_truth, save_ground_truth from bithumb.ground_truth.ground_truth import GtParams, build_ground_truth, save_ground_truth
TIER_DESCRIPTIONS = { TIER_DESCRIPTIONS = {
"v1": "스윙만 (최소 매수·매도)", "v1": "스윙만 (최소 매수·매도)",

View File

@@ -1,153 +0,0 @@
#!/usr/bin/env python3
"""0단계: 선물 GT 타점 차트 (현물 GT → 롱·숏 4색 마커)."""
from __future__ import annotations
import argparse
import json
import logging
import sys
from copy import deepcopy
from pathlib import Path
from typing import Any
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 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": "스윙만 (최소 매수·매도)",
"v2": "스윙 + 눌림목",
"v3": "스윙 + 눌림목 + 돌파 + 다이버전스",
}
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 _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,
),
}
if tier_arg == "all":
return [(t, *paths) for t, paths in all_tiers.items()]
return [(tier_arg, *all_tiers[tier_arg])]
def _load_gt(json_path: Path) -> dict[str, Any]:
"""GT JSON을 로드한다."""
with json_path.open(encoding="utf-8") as fp:
return json.load(fp)
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"대상: {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·차트 생성 (현물 GT → 롱·숏 4색)"
)
parser.add_argument(
"--tier",
choices=("v1", "v2", "v3", "all"),
default="all",
help="대상 GT 버전",
)
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
_configure_logging(args.verbose)
settings = load_settings()
tiers = _tier_targets(settings, args.tier)
print("\n=== 선물 Ground Truth 생성 ===")
print("현물 GT 타점 → L↑상방매수 L↓상방매도 S↓하방매수 S↑하방매도")
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
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=spot_gt,
output_path=chart_path,
chart_lookback_days=settings.download_days,
)
_print_summary(tier, futures_gt, futures_json_path, chart_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -16,12 +16,12 @@ SRC = ROOT / "src"
if str(SRC) not in sys.path: if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import Settings, load_settings from bithumb.config import Settings, load_settings
from deepcoin.data.candle_loader import load_candles from bithumb.data.candle_loader import load_candles
from deepcoin.data.intervals import interval_label from bithumb.data.intervals import interval_label
from deepcoin.ground_truth.chart import render_ground_truth_sim_chart from bithumb.ground_truth.chart import render_ground_truth_sim_chart
from deepcoin.ground_truth.ground_truth import GtParams, build_ground_truth, save_ground_truth from bithumb.ground_truth.ground_truth import GtParams, build_ground_truth, save_ground_truth
from deepcoin.ground_truth.pnl import simulate_gt_signals_pnl from bithumb.ground_truth.pnl import simulate_gt_signals_pnl
TIER_DESCRIPTIONS = { TIER_DESCRIPTIONS = {
"v1": "스윙만 (최소 매수·매도)", "v1": "스윙만 (최소 매수·매도)",

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""1단계: 연속 매수·매도 클러스터별 매수·매도 비율 튜닝 (타점 고정)."""
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 bithumb.config import load_settings
from bithumb.ground_truth.sizing_rules import save_sizing_rules
from bithumb.ground_truth.sizing_tune import tune_sizing_rules
from bithumb.operations.signal_pipeline import run_signal_pipeline
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="연속 매수·매도 클러스터 상태별 사이징 비율 학습",
)
parser.add_argument(
"-o",
"--output",
type=str,
default=None,
help="규칙 JSON 경로 (기본: OPS_SIZING_RULES_JSON)",
)
parser.add_argument(
"--min-bucket-samples",
type=int,
default=5,
help="클러스터 버킷별 최소 샘플 수",
)
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
_configure_logging(args.verbose)
settings = load_settings()
output_path = Path(args.output) if args.output else settings.ops_sizing_rules_json
if not output_path.is_absolute():
output_path = ROOT / output_path
print("\n=== 매수·매도 비율 튜닝 (타점 고정) ===", flush=True)
print(
f"기법: {settings.ops_technique_id} · sim {settings.gt_sim_lookback_days}일 · "
f"기본 {settings.ops_buy_cash_pct:.0%}/{settings.ops_sell_coin_pct:.0%}",
flush=True,
)
pipeline = run_signal_pipeline(settings, use_cache=True)
rules, final_sim = tune_sizing_rules(
settings,
pipeline["kept"],
data_end=pipeline["data_end"],
last_mark_price=pipeline["last_price"],
technique_id=pipeline["technique_id"],
min_bucket_samples=args.min_bucket_samples,
)
save_sizing_rules(rules, output_path)
tuning = rules.get("tuning") or {}
print(f"\n기준(고정 10%): {tuning.get('baseline_return_pct'):+.2f}%")
print(
f"학습 후: {final_sim.get('total_return_pct'):+.2f}% · "
f"매수/매도 {final_sim.get('buys_executed')}/{final_sim.get('sells_executed')}"
)
print(
f"전역 비율: 매수 {rules.get('default_buy_cash_pct', 0):.0%} · "
f"매도 {rules.get('default_sell_coin_pct', 0):.0%}"
)
by_cluster = rules.get("by_cluster") or {}
if by_cluster.get("buy") or by_cluster.get("sell"):
print("클러스터별:")
print(json.dumps(by_cluster, ensure_ascii=False, indent=2))
print(f"\n규칙 JSON: {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -14,15 +14,15 @@ SRC = ROOT / "src"
if str(SRC) not in sys.path: if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import load_settings from bithumb.config import load_settings
from deepcoin.data.candle_loader import load_candles from bithumb.data.candle_loader import load_candles
from deepcoin.evaluation.causal_sim import ( from bithumb.evaluation.causal_sim import (
best_technique_chart_path, best_technique_chart_path,
pick_best_technique_row, pick_best_technique_row,
render_best_technique_comparison_chart, render_best_technique_comparison_chart,
run_technique_causal_sim, run_technique_causal_sim,
) )
from deepcoin.techniques.runner import load_ground_truth, load_technique_result from bithumb.techniques.runner import load_ground_truth, load_technique_result
def _configure_logging(verbose: bool) -> None: def _configure_logging(verbose: bool) -> None:

View File

@@ -14,10 +14,10 @@ SRC = ROOT / "src"
if str(SRC) not in sys.path: if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import load_settings from bithumb.config import load_settings
from deepcoin.data.candle_loader import load_candles from bithumb.data.candle_loader import load_candles
from deepcoin.data.intervals import interval_label from bithumb.data.intervals import interval_label
from deepcoin.evaluation.causal_sim import ( from bithumb.evaluation.causal_sim import (
best_technique_chart_path, best_technique_chart_path,
build_causal_sim_report, build_causal_sim_report,
pick_best_technique_row, pick_best_technique_row,
@@ -28,7 +28,7 @@ from deepcoin.evaluation.causal_sim import (
save_causal_sim_report, save_causal_sim_report,
technique_sim_chart_path, technique_sim_chart_path,
) )
from deepcoin.techniques.runner import load_ground_truth, load_technique_results from bithumb.techniques.runner import load_ground_truth, load_technique_results
def _configure_logging(verbose: bool) -> None: def _configure_logging(verbose: bool) -> None:

View File

@@ -13,16 +13,16 @@ SRC = ROOT / "src"
if str(SRC) not in sys.path: if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import load_settings from bithumb.config import load_settings
from deepcoin.evaluation.mtf_report import ( from bithumb.evaluation.mtf_report import (
build_mtf_correlation_report, build_mtf_correlation_report,
render_mtf_html, render_mtf_html,
save_mtf_report, save_mtf_report,
) )
from deepcoin.mtf.extractor import MtfFeatureExtractor from bithumb.mtf.extractor import MtfFeatureExtractor
from deepcoin.mtf.rules import derive_rules_from_report, save_mtf_rules from bithumb.mtf.rules import derive_rules_from_report, save_mtf_rules
from deepcoin.mtf.store import MultiTimeframeStore from bithumb.mtf.store import MultiTimeframeStore
from deepcoin.techniques.runner import load_ground_truth from bithumb.techniques.runner import load_ground_truth
def _configure_logging(verbose: bool) -> None: def _configure_logging(verbose: bool) -> None:

View File

@@ -13,21 +13,21 @@ SRC = ROOT / "src"
if str(SRC) not in sys.path: if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import load_settings from bithumb.config import load_settings
from deepcoin.data.intervals import interval_label from bithumb.data.intervals import interval_label
from deepcoin.evaluation.report import ( from bithumb.evaluation.report import (
build_comparison_report, build_comparison_report,
render_comparison_html, render_comparison_html,
save_comparison_report, save_comparison_report,
) )
from deepcoin.evaluation.signal_type_report import ( from bithumb.evaluation.signal_type_report import (
build_signal_type_report, build_signal_type_report,
render_signal_type_html, render_signal_type_html,
save_signal_type_report, save_signal_type_report,
) )
from deepcoin.techniques.base import TechniqueParams from bithumb.techniques.base import TechniqueParams
from deepcoin.techniques.registry import list_technique_ids from bithumb.techniques.registry import list_technique_ids
from deepcoin.techniques.runner import ( from bithumb.techniques.runner import (
load_ground_truth, load_ground_truth,
load_technique_results, load_technique_results,
run_all_techniques, run_all_techniques,

View File

@@ -4,7 +4,7 @@ set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
export PYTHONPATH=src export PYTHONPATH=src
PY="${PY:-/opt/anaconda3/envs/ncue/bin/python}" PY="${PY:-/opt/anaconda3/envs/ncue/bin/python}"
LOG="${LOG:-/tmp/deepcoin_stage2.log}" LOG="${LOG:-/tmp/bithumb_stage2.log}"
echo "=== 현물 2단계 파이프라인 시작 $(date '+%Y-%m-%d %H:%M:%S') ===" | tee "$LOG" echo "=== 현물 2단계 파이프라인 시작 $(date '+%Y-%m-%d %H:%M:%S') ===" | tee "$LOG"

View File

@@ -13,16 +13,16 @@ SRC = ROOT / "src"
if str(SRC) not in sys.path: if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import load_settings from bithumb.config import load_settings
from deepcoin.data.intervals import interval_label from bithumb.data.intervals import interval_label
from deepcoin.evaluation.report import ( from bithumb.evaluation.report import (
build_comparison_report, build_comparison_report,
render_comparison_html, render_comparison_html,
save_comparison_report, save_comparison_report,
) )
from deepcoin.techniques.base import TechniqueParams from bithumb.techniques.base import TechniqueParams
from deepcoin.techniques.registry import list_technique_ids from bithumb.techniques.registry import list_technique_ids
from deepcoin.techniques.runner import ( from bithumb.techniques.runner import (
load_ground_truth, load_ground_truth,
run_all_techniques, run_all_techniques,
save_technique_result, save_technique_result,

90
scripts/3_init_live_state.py Executable file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""paper → live 상태 전환 — 거래소 잔고 동기화."""
from __future__ import annotations
import argparse
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 bithumb.config import load_settings
from bithumb.notifications.telegram import create_telegram_notifier
from bithumb.operations.live_bootstrap import init_live_state
def main() -> int:
"""CLI 진입점."""
parser = argparse.ArgumentParser(description="Bithumb live 상태 초기화")
parser.add_argument(
"--no-backup",
action="store_true",
help="기존 state JSON 백업 생략",
)
parser.add_argument(
"--reset-bar-cursor",
action="store_true",
help="last_processed_bar_index 를 -1 로 초기화",
)
parser.add_argument(
"--yes",
action="store_true",
help="확인 프롬프트 생략",
)
args = parser.parse_args()
settings = load_settings()
if settings.ops_mode != "live":
print(f"OPS_MODE={settings.ops_mode} — .env 에 OPS_MODE=live 설정 후 실행하세요.", file=sys.stderr)
return 1
if not settings.bithumb_access_key or not settings.bithumb_secret_key:
print("BITHUMB API 키가 필요합니다.", file=sys.stderr)
return 1
if not args.yes:
print("경고: live 상태 초기화 — 거래소 잔고가 state JSON에 반영됩니다.")
print(f"state: {settings.ops_state_json}")
answer = input("계속하시겠습니까? [y/N]: ").strip().lower()
if answer not in ("y", "yes"):
print("취소됨.")
return 1
result = init_live_state(
settings,
backup=not args.no_backup,
preserve_bar_cursor=not args.reset_bar_cursor,
)
bal = result["balances"]
print("=== live 상태 초기화 완료 ===")
if result["backup_path"]:
print(f"백업: {result['backup_path']}")
print(f"state: {result['state_path']}")
print(
f"잔고: KRW {bal['cash_krw']:,.0f}원 · "
f"{settings.symbol} {bal['coin_qty']:.8f}"
)
print(f"bar cursor: {result['last_processed_bar_index']}")
telegram = create_telegram_notifier(
settings.telegram_bot_token,
settings.telegram_chat_id,
enabled=settings.ops_telegram_enabled,
)
if telegram.is_active:
telegram.send_message(
"[Bithumb] LIVE 세션 시작\n"
f"{settings.coin_name} ({settings.symbol}) | {settings.ops_technique_id}\n"
f"KRW {bal['cash_krw']:,.0f} · {settings.symbol} {bal['coin_qty']:.8f}\n"
f"일 체결 상한 {settings.ops_daily_max_trades} · 슬리피지 {settings.ops_slippage_rate * 100:.2f}%"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

70
scripts/3_preflight_live.py Executable file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""live 운영 전 사전 점검 — API·잔고·캐시·텔레그램."""
from __future__ import annotations
import argparse
import json
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 bithumb.config import load_settings
from bithumb.operations.live_bootstrap import run_preflight, save_preflight_report
def main() -> int:
"""CLI 진입점."""
parser = argparse.ArgumentParser(description="Bithumb live 사전 점검")
parser.add_argument(
"--report",
default="docs/spot/3_operations/live_preflight_report.json",
help="점검 결과 JSON 경로",
)
parser.add_argument(
"--require-live",
action="store_true",
help="OPS_MODE=live 가 아니면 실패",
)
args = parser.parse_args()
settings = load_settings()
if args.require_live and settings.ops_mode != "live":
print(f"OPS_MODE={settings.ops_mode} — live 전환 후 다시 실행하세요.", file=sys.stderr)
return 1
report = run_preflight(settings)
report_path = save_preflight_report(
ROOT / args.report if not Path(args.report).is_absolute() else Path(args.report),
report,
)
print("=== Bithumb live 사전 점검 ===")
print(f"기법: {report['technique_id']} · 마켓: {report['market']} · 모드: {report['mode']}")
if report.get("balances"):
bal = report["balances"]
print(
f"잔고: KRW {bal['cash_krw']:,.0f}원 · "
f"{report['symbol']} {bal['coin_qty']:.8f}"
)
print()
for row in report["checks"]:
mark = "OK" if row["passed"] else "NG"
req = "필수" if row["required"] else "선택"
print(f"[{mark}] ({req}) {row['name']}: {row['detail']}")
print(f"\n리포트: {report_path}")
if report["ok"]:
print("\n사전 점검 통과 — live 운영을 시작할 수 있습니다.")
return 0
print("\n사전 점검 실패 — live 시작 전 위 항목을 해결하세요.", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -13,8 +13,8 @@ SRC = ROOT / "src"
if str(SRC) not in sys.path: if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import load_settings from bithumb.config import load_settings
from deepcoin.operations.chart import render_ops_live_chart from bithumb.operations.chart import render_ops_live_chart
def _configure_logging(verbose: bool) -> None: def _configure_logging(verbose: bool) -> None:
@@ -34,12 +34,21 @@ def _write_index_html(
"""docs/live 인덱스 HTML을 생성한다.""" """docs/live 인덱스 HTML을 생성한다."""
sim = report["filtered_sim"] sim = report["filtered_sim"]
ret = sim.get("total_return_pct", 0) ret = sim.get("total_return_pct", 0)
sizing_line = ""
if sim.get("sizing_rules_applied"):
lb = sim.get("learned_default_buy_cash_pct")
ls = sim.get("learned_default_sell_coin_pct")
if lb is not None and ls is not None:
sizing_line = (
f"<br>학습 비율: 매수 {float(lb) * 100:.0f}% · 매도 {float(ls) * 100:.0f}%"
f" (클러스터별 규칙 적용)"
)
index_path.parent.mkdir(parents=True, exist_ok=True) index_path.parent.mkdir(parents=True, exist_ok=True)
html = f"""<!DOCTYPE html> html = f"""<!DOCTYPE html>
<html lang="ko"> <html lang="ko">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>DeepCoin Live — 운영 백테스트 차트</title> <title>Bithumb Live — 운영 백테스트 차트</title>
<style> <style>
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 32px; color: #333; background: #f5f5f5; }} body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 32px; color: #333; background: #f5f5f5; }}
h1 {{ font-size: 22px; margin-bottom: 8px; }} h1 {{ font-size: 22px; margin-bottom: 8px; }}
@@ -52,13 +61,13 @@ def _write_index_html(
</style> </style>
</head> </head>
<body> <body>
<h1>DeepCoin Live — 운영 백테스트</h1> <h1>Bithumb Live — 운영 백테스트</h1>
<p class="meta"> <p class="meta">
{report.get("symbol", "BTC")} · {report.get("technique_name", "")} ({report.get("technique_id", "")})<br> {report.get("symbol", "BTC")} · {report.get("technique_name", "")} ({report.get("technique_id", "")})<br>
sim 기간: 최근 {report.get("sim_lookback_days", 1095)}일 · sim 기간: 최근 {report.get("sim_lookback_days", 1095)}일 ·
슬리피지 {report.get("slippage_rate", 0) * 100:.2f}% · 슬리피지 {report.get("slippage_rate", 0) * 100:.2f}% ·
일 체결 상한 {report.get("daily_max_trades", "-")} · 일 체결 상한 {report.get("daily_max_trades", "-")} ·
MTF {"on" if report.get("mtf_enabled") else "off"} MTF {"on" if report.get("mtf_enabled") else "off"}{sizing_line}
</p> </p>
<div class="card"> <div class="card">
<div>3년 수익률 (운영 규칙 sim)</div> <div>3년 수익률 (운영 규칙 sim)</div>

View File

@@ -13,8 +13,8 @@ SRC = ROOT / "src"
if str(SRC) not in sys.path: if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import load_settings from bithumb.config import load_settings
from deepcoin.operations.backtest import run_filtered_backtest, save_backtest_report from bithumb.operations.backtest import run_filtered_backtest, save_backtest_report
def _configure_logging(verbose: bool) -> None: def _configure_logging(verbose: bool) -> None:

26
scripts/3_run_fractal_live.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# fractal_swing LIVE 운영 — 사전 점검 → 상태 초기화 → 180초 tick loop
set -euo pipefail
cd "$(dirname "$0")/.."
export PYTHONPATH=src
if [[ "${OPS_MODE:-}" != "live" ]]; then
if grep -q '^OPS_MODE=live' .env 2>/dev/null; then
:
else
echo "오류: .env 에 OPS_MODE=live 가 필요합니다." >&2
exit 1
fi
fi
echo "=== 1/3 live 사전 점검 ==="
python scripts/3_preflight_live.py --require-live
echo ""
echo "=== 2/3 live 상태 초기화 (거래소 잔고 동기화) ==="
python scripts/3_init_live_state.py --yes
echo ""
echo "=== 3/3 LIVE 운영 (180초 loop) — Ctrl+C 종료 ==="
echo "경고: 실제 빗썸 시장가 주문이 발생합니다."
python scripts/3_run_operations.py --mode live --loop 180

View File

@@ -14,10 +14,10 @@ SRC = ROOT / "src"
if str(SRC) not in sys.path: if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import load_settings from bithumb.config import load_settings
from deepcoin.evaluation.causal_sim import normalize_signals_for_sim from bithumb.evaluation.causal_sim import normalize_signals_for_sim
from deepcoin.ground_truth.pnl import simulate_gt_signals_pnl from bithumb.ground_truth.pnl import simulate_gt_signals_pnl
from deepcoin.operations.signal_pipeline import ( from bithumb.operations.signal_pipeline import (
_signals_in_lookback, _signals_in_lookback,
generate_raw_signals, generate_raw_signals,
load_ops_candles, load_ops_candles,
@@ -64,6 +64,8 @@ def main() -> int:
min_order_krw=settings.ops_min_order_krw, min_order_krw=settings.ops_min_order_krw,
slippage_rate=sc["slippage_rate"], slippage_rate=sc["slippage_rate"],
daily_max_trades=sc["daily_max_trades"], daily_max_trades=sc["daily_max_trades"],
buy_cash_pct=settings.ops_buy_cash_pct,
sell_coin_pct=settings.ops_sell_coin_pct,
sim_lookback_days=settings.gt_sim_lookback_days, sim_lookback_days=settings.gt_sim_lookback_days,
data_end=end, data_end=end,
last_mark_price=price, last_mark_price=price,

View File

@@ -14,8 +14,8 @@ SRC = ROOT / "src"
if str(SRC) not in sys.path: if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC)) sys.path.insert(0, str(SRC))
from deepcoin.config import load_settings from bithumb.config import load_settings
from deepcoin.operations.runner import OperationsRunner from bithumb.operations.runner import OperationsRunner
def _configure_logging(verbose: bool) -> None: def _configure_logging(verbose: bool) -> None:
@@ -29,7 +29,7 @@ def _configure_logging(verbose: bool) -> None:
def main() -> int: def main() -> int:
"""CLI 진입점.""" """CLI 진입점."""
parser = argparse.ArgumentParser(description="3단계: DeepCoin 운영 tick") parser = argparse.ArgumentParser(description="3단계: Bithumb 운영 tick")
parser.add_argument( parser.add_argument(
"--mode", "--mode",
choices=("paper", "live"), choices=("paper", "live"),
@@ -67,19 +67,37 @@ def main() -> int:
sync = not args.no_sync sync = not args.no_sync
while True: while True:
try:
report = runner.tick(sync_candles=sync) report = runner.tick(sync_candles=sync)
port = report["portfolio"] except Exception as exc:
logging.exception("운영 tick 예외 (복구 후 계속)")
if runner.telegram.is_active:
runner.telegram.notify_ops_error(
mode=settings.ops_mode,
symbol=settings.symbol,
technique_id=settings.ops_technique_id,
stage="main_loop",
error=str(exc),
)
if args.loop <= 0:
break
time.sleep(args.loop)
continue
port = report.get("portfolio") or {}
print("\n=== 3단계 운영 tick ===") print("\n=== 3단계 운영 tick ===")
print(f"모드: {report['mode']}") if report.get("error"):
print(f"오류: [{report.get('error_stage')}] {report.get('error_message')}")
print(f"모드: {report.get('mode', settings.ops_mode)}")
print( print(
f"최신 봉 후보: {report.get('latest_bar_candidates', 0)} · " f"최신 봉 후보: {report.get('latest_bar_candidates', 0)} · "
f"필터 통과: {report['filtered_signals']} · " f"필터 통과: {report.get('filtered_signals', 0)} · "
f"처리 bar: {report.get('pending_bars', [])}" f"처리 bar: {report.get('pending_bars', [])}"
) )
print(f"이번 체결: {len(report['executions'])}") print(f"이번 체결: {len(report.get('executions', []))}")
print( print(
f"포트폴리오: 현금 {port['cash_krw']:,.0f}원 · " f"포트폴리오: 현금 {port.get('cash_krw', 0):,.0f}원 · "
f"코인 {port['coin_qty']:.8f} {settings.symbol}" f"코인 {port.get('coin_qty', 0):.8f} {settings.symbol}"
) )
print(f"리포트: {settings.ops_report_json}") print(f"리포트: {settings.ops_report_json}")

3
src/bithumb/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""Bithumb — 빗썸 암호화폐 데이터 수집·분석."""
__version__ = "0.1.0"

View File

@@ -9,7 +9,7 @@ from typing import Any
import requests import requests
from deepcoin.data.intervals import INTERVAL_DAILY, INTERVAL_MONTHLY, INTERVAL_WEEKLY from bithumb.data.intervals import INTERVAL_DAILY, INTERVAL_MONTHLY, INTERVAL_WEEKLY
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -3,16 +3,42 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import math
import time import time
from typing import Any from typing import Any
import requests import requests
from deepcoin.api.bithumb_auth import auth_headers, dumps_params from bithumb.api.bithumb_auth import auth_headers, dumps_params
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BithumbAPIError(Exception):
"""빗썸 Private API 오류."""
def __init__(self, status_code: int, error_name: str, message: str) -> None:
self.status_code = status_code
self.error_name = error_name
self.message = message
super().__init__(f"{error_name}: {message} (HTTP {status_code})")
def _parse_error_response(response: requests.Response) -> BithumbAPIError:
"""HTTP 오류 응답을 BithumbAPIError로 변환한다."""
error_name = "http_error"
message = response.text[:500]
try:
payload = response.json()
err = payload.get("error", payload)
if isinstance(err, dict):
error_name = str(err.get("name", error_name))
message = str(err.get("message", message))
except ValueError:
pass
return BithumbAPIError(response.status_code, error_name, message)
class BithumbPrivateClient: class BithumbPrivateClient:
"""빗썸 v2.1 Private API 클라이언트.""" """빗썸 v2.1 Private API 클라이언트."""
@@ -72,6 +98,18 @@ class BithumbPrivateClient:
logger.warning("Rate limit 429 — %ss 대기", wait) logger.warning("Rate limit 429 — %ss 대기", wait)
time.sleep(wait) time.sleep(wait)
continue continue
if response.status_code >= 400:
api_error = _parse_error_response(response)
logger.warning(
"Private API error (%s/%s): %s",
attempt,
self.retries,
api_error,
)
last_error = api_error
wait = self.sleep_sec * attempt * 2
time.sleep(wait)
continue
response.raise_for_status() response.raise_for_status()
payload = response.json() payload = response.json()
time.sleep(self.sleep_sec) time.sleep(self.sleep_sec)
@@ -116,10 +154,13 @@ class BithumbPrivateClient:
Returns: Returns:
주문 응답 dict. 주문 응답 dict.
""" """
order_krw = max(math.floor(float(krw_amount)), 0)
if order_krw <= 0:
raise ValueError("매수 원화 금액이 0 이하입니다.")
params = { params = {
"market": market, "market": market,
"side": "bid", "side": "bid",
"price": str(int(krw_amount)), "price": str(order_krw),
"ord_type": "price", "ord_type": "price",
} }
return self._request("POST", "/v1/orders", params=params) return self._request("POST", "/v1/orders", params=params)

View File

@@ -8,7 +8,7 @@ from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
from deepcoin.data.intervals import DEFAULT_DOWNLOAD_INTERVALS from bithumb.data.intervals import DEFAULT_DOWNLOAD_INTERVALS
_PROJECT_ROOT = Path(__file__).resolve().parents[2] _PROJECT_ROOT = Path(__file__).resolve().parents[2]
@@ -37,7 +37,7 @@ def _parse_int_list(raw: str) -> list[int]:
@dataclass(frozen=True) @dataclass(frozen=True)
class Settings: class Settings:
"""DeepCoin 실행 설정.""" """Bithumb 실행 설정."""
symbol: str symbol: str
coin_name: str coin_name: str
@@ -48,7 +48,7 @@ class Settings:
db_path: Path db_path: Path
request_sleep_sec: float request_sleep_sec: float
request_retries: int request_retries: int
# 0단계: GT 타점 (현물·선물 공통 파라미터) # 0단계: GT 타점 (현물)
gt_interval_min: int gt_interval_min: int
gt_lookback_days: int gt_lookback_days: int
gt_zigzag_reversal_pct: float gt_zigzag_reversal_pct: float
@@ -68,12 +68,6 @@ class Settings:
ground_truth_chart_v1_file: Path ground_truth_chart_v1_file: Path
ground_truth_chart_v2_file: Path ground_truth_chart_v2_file: Path
ground_truth_chart_v3_file: Path ground_truth_chart_v3_file: Path
ground_truth_futures_file: Path
ground_truth_futures_v1_file: Path
ground_truth_futures_v2_file: Path
ground_truth_futures_chart_v1_file: Path
ground_truth_futures_chart_v2_file: Path
ground_truth_futures_chart_v3_file: Path
# 현물 1단계: GT sim # 현물 1단계: GT sim
ground_truth_chart_sim_v1_file: Path ground_truth_chart_sim_v1_file: Path
ground_truth_chart_sim_v2_file: Path ground_truth_chart_sim_v2_file: Path
@@ -107,6 +101,11 @@ class Settings:
ops_daily_max_trades: int ops_daily_max_trades: int
ops_min_order_krw: float ops_min_order_krw: float
ops_slippage_rate: float ops_slippage_rate: float
ops_exchange_fee_lock_rate: float
ops_buy_safety_buffer_krw: float
ops_buy_cash_pct: float
ops_sell_coin_pct: float
ops_sizing_rules_json: Path
ops_order_interval_sec: float ops_order_interval_sec: float
ops_sync_candles: bool ops_sync_candles: bool
ops_sync_intervals: list[int] ops_sync_intervals: list[int]
@@ -189,42 +188,6 @@ def load_settings(env_path: Path | None = None) -> Settings:
ground_truth_chart_v3_file=_resolve_project_path( ground_truth_chart_v3_file=_resolve_project_path(
os.getenv("GROUND_TRUTH_CHART_V3_FILE", "docs/spot/0_ground_truth/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(
"GROUND_TRUTH_FUTURES_FILE",
"data/futures/ground_truth/ground_truth_trades_v3.json",
)
),
ground_truth_futures_v1_file=_resolve_project_path(
os.getenv(
"GROUND_TRUTH_FUTURES_V1_FILE",
"data/futures/ground_truth/ground_truth_trades_v1.json",
)
),
ground_truth_futures_v2_file=_resolve_project_path(
os.getenv(
"GROUND_TRUTH_FUTURES_V2_FILE",
"data/futures/ground_truth/ground_truth_trades_v2.json",
)
),
ground_truth_futures_chart_v1_file=_resolve_project_path(
os.getenv(
"GROUND_TRUTH_FUTURES_CHART_V1_FILE",
"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/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/futures/0_ground_truth/ground_truth_chart_v3.html",
)
),
ground_truth_chart_sim_v1_file=_resolve_project_path( ground_truth_chart_sim_v1_file=_resolve_project_path(
os.getenv( os.getenv(
"GROUND_TRUTH_CHART_SIM_V1_FILE", "GROUND_TRUTH_CHART_SIM_V1_FILE",
@@ -309,6 +272,20 @@ def load_settings(env_path: Path | None = None) -> Settings:
ops_daily_max_trades=int(os.getenv("OPS_DAILY_MAX_TRADES", "20")), ops_daily_max_trades=int(os.getenv("OPS_DAILY_MAX_TRADES", "20")),
ops_min_order_krw=float(os.getenv("OPS_MIN_ORDER_KRW", "5000")), ops_min_order_krw=float(os.getenv("OPS_MIN_ORDER_KRW", "5000")),
ops_slippage_rate=float(os.getenv("OPS_SLIPPAGE_RATE", "0.0005")), ops_slippage_rate=float(os.getenv("OPS_SLIPPAGE_RATE", "0.0005")),
ops_exchange_fee_lock_rate=float(
os.getenv("OPS_EXCHANGE_FEE_LOCK_RATE", "0.0025")
),
ops_buy_safety_buffer_krw=float(
os.getenv("OPS_BUY_SAFETY_BUFFER_KRW", "1000")
),
ops_buy_cash_pct=float(os.getenv("OPS_BUY_CASH_PCT", "0.10")),
ops_sell_coin_pct=float(os.getenv("OPS_SELL_COIN_PCT", "0.10")),
ops_sizing_rules_json=_resolve_project_path(
os.getenv(
"OPS_SIZING_RULES_JSON",
"data/spot/operations/sizing_rules.json",
)
),
ops_order_interval_sec=float(os.getenv("OPS_ORDER_INTERVAL_SEC", "0.35")), ops_order_interval_sec=float(os.getenv("OPS_ORDER_INTERVAL_SEC", "0.35")),
ops_sync_candles=os.getenv("OPS_SYNC_CANDLES", "true").strip().lower() ops_sync_candles=os.getenv("OPS_SYNC_CANDLES", "true").strip().lower()
in ("1", "true", "yes", "on"), in ("1", "true", "yes", "on"),

View File

@@ -7,7 +7,7 @@ from pathlib import Path
import pandas as pd import pandas as pd
from deepcoin.data.candle_store import CandleStore from bithumb.data.candle_store import CandleStore
def load_candles( def load_candles(

View File

@@ -8,7 +8,7 @@ from pathlib import Path
import pandas as pd import pandas as pd
from deepcoin.api.bithumb import parse_kst_datetime from bithumb.api.bithumb import parse_kst_datetime
class CandleStore: class CandleStore:

View File

@@ -7,9 +7,9 @@ from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from typing import Any
from deepcoin.api.bithumb import BithumbCandleClient, parse_kst_datetime from bithumb.api.bithumb import BithumbCandleClient, parse_kst_datetime
from deepcoin.config import Settings from bithumb.config import Settings
from deepcoin.data.candle_store import CandleStore from bithumb.data.candle_store import CandleStore
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -1,13 +1,13 @@
"""Ground Truth 정합 평가.""" """Ground Truth 정합 평가."""
from deepcoin.evaluation.gt_align import align_with_ground_truth from bithumb.evaluation.gt_align import align_with_ground_truth
from deepcoin.evaluation.mtf_report import ( from bithumb.evaluation.mtf_report import (
build_mtf_correlation_report, build_mtf_correlation_report,
render_mtf_html, render_mtf_html,
save_mtf_report, save_mtf_report,
) )
from deepcoin.evaluation.report import build_comparison_report, render_comparison_html, save_comparison_report from bithumb.evaluation.report import build_comparison_report, render_comparison_html, save_comparison_report
from deepcoin.evaluation.signal_type_report import ( from bithumb.evaluation.signal_type_report import (
build_signal_type_report, build_signal_type_report,
render_signal_type_html, render_signal_type_html,
save_signal_type_report, save_signal_type_report,

View File

@@ -7,9 +7,9 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from deepcoin.ground_truth.chart import render_ground_truth_sim_chart from bithumb.ground_truth.chart import render_ground_truth_sim_chart
from deepcoin.ground_truth.pnl import simulate_gt_signals_pnl from bithumb.ground_truth.pnl import simulate_gt_signals_pnl
from deepcoin.techniques.base import TechniqueResult from bithumb.techniques.base import TechniqueResult
def normalize_signals_for_sim(signals: list[dict[str, Any]]) -> list[dict[str, Any]]: def normalize_signals_for_sim(signals: list[dict[str, Any]]) -> list[dict[str, Any]]:
@@ -303,7 +303,7 @@ def render_causal_sim_html(report: dict[str, Any], html_path: Path) -> Path:
<html lang="ko"> <html lang="ko">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>DeepCoin 2단계 인과 sim</title> <title>Bithumb 2단계 인과 sim</title>
<style> <style>
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; color: #333; background: #f5f5f5; }} body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; color: #333; background: #f5f5f5; }}
h1 {{ font-size: 20px; margin-bottom: 8px; }} h1 {{ font-size: 20px; margin-bottom: 8px; }}

View File

@@ -247,7 +247,7 @@ def align_with_ground_truth(
tech_return = 0.0 tech_return = 0.0
if technique_legs: if technique_legs:
from deepcoin.ground_truth.pnl import simulate_gt_pnl from bithumb.ground_truth.pnl import simulate_gt_pnl
tech_pnl = simulate_gt_pnl( tech_pnl = simulate_gt_pnl(
technique_legs, technique_legs,

View File

@@ -11,9 +11,9 @@ from typing import Any
import pandas as pd import pandas as pd
from deepcoin.evaluation.gt_align import SIGNAL_TYPE_LABELS, SIGNAL_TYPE_SIDE from bithumb.evaluation.gt_align import SIGNAL_TYPE_LABELS, SIGNAL_TYPE_SIDE
from deepcoin.mtf.extractor import MtfFeatureExtractor, MtfSnapshot from bithumb.mtf.extractor import MtfFeatureExtractor, MtfSnapshot
from deepcoin.mtf.features import FEATURE_NAMES from bithumb.mtf.features import FEATURE_NAMES
NUMERIC_FEATURES: tuple[str, ...] = ( NUMERIC_FEATURES: tuple[str, ...] = (
"close_vs_ema60_pct", "close_vs_ema60_pct",

View File

@@ -7,7 +7,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from deepcoin.techniques.base import TechniqueResult from bithumb.techniques.base import TechniqueResult
def build_comparison_report( def build_comparison_report(
@@ -94,7 +94,7 @@ def render_comparison_html(report: dict[str, Any], html_path: Path) -> Path:
<html lang="ko"> <html lang="ko">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>DeepCoin 2단계 인과 GT 정합</title> <title>Bithumb 2단계 인과 GT 정합</title>
<style> <style>
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; color: #333; background: #f5f5f5; }} body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; color: #333; background: #f5f5f5; }}
h1 {{ font-size: 20px; margin-bottom: 8px; }} h1 {{ font-size: 20px; margin-bottom: 8px; }}
@@ -106,7 +106,7 @@ def render_comparison_html(report: dict[str, Any], html_path: Path) -> Path:
</style> </style>
</head> </head>
<body> <body>
<h1>DeepCoin 2단계 인과 기법 Ground Truth 정합</h1> <h1>Bithumb 2단계 인과 기법 Ground Truth 정합</h1>
<div class="meta"> <div class="meta">
생성: {report.get('generated_at', '')} | 생성: {report.get('generated_at', '')} |
{report.get('symbol', '')} | {report.get('symbol', '')} |

View File

@@ -7,12 +7,12 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from deepcoin.evaluation.gt_align import ( from bithumb.evaluation.gt_align import (
SIGNAL_TYPE_LABELS, SIGNAL_TYPE_LABELS,
SIGNAL_TYPE_PRIMARY_TECHNIQUES, SIGNAL_TYPE_PRIMARY_TECHNIQUES,
summarize_signal_type_matrix, summarize_signal_type_matrix,
) )
from deepcoin.techniques.base import TechniqueResult from bithumb.techniques.base import TechniqueResult
def build_signal_type_report( def build_signal_type_report(
@@ -163,7 +163,7 @@ def render_signal_type_html(report: dict[str, Any], html_path: Path) -> Path:
<html lang="ko"> <html lang="ko">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>DeepCoin v3 신호 유형별 GT 정합</title> <title>Bithumb v3 신호 유형별 GT 정합</title>
<style> <style>
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; color: #333; background: #f5f5f5; }} body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; color: #333; background: #f5f5f5; }}
h1 {{ font-size: 20px; margin-bottom: 4px; }} h1 {{ font-size: 20px; margin-bottom: 4px; }}
@@ -177,7 +177,7 @@ def render_signal_type_html(report: dict[str, Any], html_path: Path) -> Path:
</style> </style>
</head> </head>
<body> <body>
<h1>DeepCoin v3 신호 유형별 Ground Truth 정합</h1> <h1>Bithumb v3 신호 유형별 Ground Truth 정합</h1>
<div class="meta"> <div class="meta">
생성: {report.get('generated_at', '')} | 생성: {report.get('generated_at', '')} |
{report.get('symbol', '')} | {report.get('symbol', '')} |

View File

@@ -7,7 +7,7 @@ from typing import Protocol
import pandas as pd import pandas as pd
from deepcoin.ground_truth.zigzag import Pivot from bithumb.ground_truth.zigzag import Pivot
class _LegLike(Protocol): class _LegLike(Protocol):

View File

@@ -9,7 +9,7 @@ from typing import Any
import pandas as pd import pandas as pd
from deepcoin.data.candle_loader import load_candles from bithumb.data.candle_loader import load_candles
# 0이면 제한 없이 전체 봉 표시 # 0이면 제한 없이 전체 봉 표시
DEFAULT_MAX_CANDLES = 0 DEFAULT_MAX_CANDLES = 0
@@ -254,7 +254,7 @@ _HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="ko"> <html lang="ko">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>DeepCoin Chart</title> <title>Bithumb Chart</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css" />
<script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
@@ -284,7 +284,7 @@ __EXTRA_STYLES__
</head> </head>
<body> <body>
<header> <header>
<h1 id="title">DeepCoin Chart</h1> <h1 id="title">Bithumb Chart</h1>
<div class="meta" id="meta"></div> <div class="meta" id="meta"></div>
</header> </header>
__EXTRA_BODY__ __EXTRA_BODY__

View File

@@ -6,7 +6,7 @@ from dataclasses import dataclass
import pandas as pd import pandas as pd
from deepcoin.techniques.indicators import macd, rsi from bithumb.techniques.indicators import macd, rsi
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@@ -10,13 +10,13 @@ from typing import Any
import pandas as pd import pandas as pd
from deepcoin.data.candle_loader import load_candles from bithumb.data.candle_loader import load_candles
from deepcoin.data.intervals import interval_label from bithumb.data.intervals import interval_label
from deepcoin.ground_truth.pnl import simulate_gt_pnl from bithumb.ground_truth.pnl import simulate_gt_pnl
from deepcoin.ground_truth.breakout import find_breakout_buy_pivots from bithumb.ground_truth.breakout import find_breakout_buy_pivots
from deepcoin.ground_truth.divergence import find_divergence_signals from bithumb.ground_truth.divergence import find_divergence_signals
from deepcoin.ground_truth.pullback import find_pullback_buy_pivots from bithumb.ground_truth.pullback import find_pullback_buy_pivots
from deepcoin.ground_truth.zigzag import Pivot, find_zigzag_pivots from bithumb.ground_truth.zigzag import Pivot, find_zigzag_pivots
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@@ -0,0 +1,122 @@
"""총평가금액 구간별 매수(현금) 상한."""
from __future__ import annotations
from typing import Any
# 총평가금액(원) 구간 — 높은 구간이 우선 적용
EQUITY_TIER_100M = 100_000_000
EQUITY_TIER_1B = 1_000_000_000
EQUITY_TIER_10B = 10_000_000_000
BUY_SIZING_RULE_LABEL = "총평가 1억↑ 현금 10% · 10억↑ 5% · 100억↑ 1%"
BASE_BUY_CASH_PCT = 0.10
BASE_SELL_COIN_PCT = 0.10
def effective_buy_cash_pct(
equity_krw: float,
base_pct: float | None = BASE_BUY_CASH_PCT,
) -> float:
"""1회 매수 허용 현금 비율 (구간 tier + 소액 base).
1억 미만은 ``base_pct``(기본 10%)로 분할 매수한다.
1억 이상은 기존 tier(10%/5%/1%)가 적용된다.
"""
tier_pct = buy_cash_pct(equity_krw)
if tier_pct is not None:
return tier_pct
if base_pct is not None:
return max(0.0, min(float(base_pct), 1.0))
return 1.0
def effective_sell_coin_pct(sell_pct: float | None = BASE_SELL_COIN_PCT) -> float:
"""1회 매도 허용 코인 비율 (기본 10%)."""
if sell_pct is None:
return 1.0
return max(0.0, min(float(sell_pct), 1.0))
def buy_cash_pct(equity_krw: float) -> float | None:
"""총평가금액에 따른 1회 매수 허용 현금 비율.
Args:
equity_krw: 현재 총평가금액(원).
Returns:
허용 비율(0~1). 1억 미만이면 None(비율 상한 없음).
"""
if equity_krw >= EQUITY_TIER_10B:
return 0.01
if equity_krw >= EQUITY_TIER_1B:
return 0.05
if equity_krw >= EQUITY_TIER_100M:
return 0.10
return None
def max_buy_from_cash(
equity_krw: float,
cash_krw: float,
*,
base_pct: float | None = BASE_BUY_CASH_PCT,
) -> float:
"""구간별·base 규칙을 반영한 1회 매수 최대 금액.
Args:
equity_krw: 현재 총평가금액(원).
cash_krw: 보유 현금(원).
base_pct: 1억 미만 계좌 1회 매수 비율 (None이면 전액).
Returns:
매수에 사용 가능한 최대 원화.
"""
cash = max(float(cash_krw), 0.0)
pct = effective_buy_cash_pct(equity_krw, base_pct)
return cash * pct
def max_sell_coin_qty(
coin_qty: float,
*,
sell_pct: float | None = BASE_SELL_COIN_PCT,
) -> float:
"""1회 매도 최대 코인 수량 (보유 비율 cap + 클러스터 분할 전).
Args:
coin_qty: 보유 코인 수량.
sell_pct: 1회 매도 허용 비율 (None이면 전량).
Returns:
이번 매도 사이클에서 사용 가능한 최대 수량.
"""
qty = max(float(coin_qty), 0.0)
pct = effective_sell_coin_pct(sell_pct)
return qty * pct
def buy_sizing_metadata(
*,
base_buy_pct: float | None = BASE_BUY_CASH_PCT,
base_sell_pct: float | None = BASE_SELL_COIN_PCT,
) -> dict[str, Any]:
"""시뮬 결과·차트에 포함할 매수·매도 상한 메타."""
base_label = (
f" · 1억 미만 현금 {base_buy_pct * 100:.0f}%"
if base_buy_pct is not None
else ""
)
return {
"buy_sizing_rule": BUY_SIZING_RULE_LABEL + base_label,
"sell_sizing_rule": f"보유 코인 {base_sell_pct * 100:.0f}% 분할 매도"
if base_sell_pct is not None
else "전량 매도",
"base_buy_cash_pct": base_buy_pct,
"base_sell_coin_pct": base_sell_pct,
"buy_sizing_tiers": [
{"min_equity_krw": EQUITY_TIER_100M, "max_cash_pct": 0.10},
{"min_equity_krw": EQUITY_TIER_1B, "max_cash_pct": 0.05},
{"min_equity_krw": EQUITY_TIER_10B, "max_cash_pct": 0.01},
],
}

View File

@@ -6,7 +6,14 @@ from dataclasses import asdict, dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from typing import Any
from deepcoin.ground_truth.order_sizing import buy_sizing_metadata, max_buy_from_cash from bithumb.ground_truth.order_sizing import (
BASE_BUY_CASH_PCT,
BASE_SELL_COIN_PCT,
buy_sizing_metadata,
max_buy_from_cash,
max_sell_coin_qty,
)
from bithumb.ground_truth.sizing_rules import resolve_buy_cash_pct, resolve_sell_coin_pct
def _fill_price(signal_price: float, side: str, slippage_rate: float) -> float: def _fill_price(signal_price: float, side: str, slippage_rate: float) -> float:
@@ -177,6 +184,9 @@ def simulate_gt_signals_pnl(
min_order_krw: float = 5_000.0, min_order_krw: float = 5_000.0,
slippage_rate: float = 0.0, slippage_rate: float = 0.0,
daily_max_trades: int | None = None, daily_max_trades: int | None = None,
buy_cash_pct: float | None = BASE_BUY_CASH_PCT,
sell_coin_pct: float | None = BASE_SELL_COIN_PCT,
sizing_rules: dict[str, Any] | None = None,
sim_lookback_days: int = 365, sim_lookback_days: int = 365,
data_end: str | None = None, data_end: str | None = None,
last_mark_price: float | None = None, last_mark_price: float | None = None,
@@ -186,7 +196,8 @@ def simulate_gt_signals_pnl(
- 시뮬 기간: data_end 기준 최근 sim_lookback_days - 시뮬 기간: data_end 기준 최근 sim_lookback_days
- 연속 매수: 가용 원화를 매수 신호 수로 균등 분할 - 연속 매수: 가용 원화를 매수 신호 수로 균등 분할
- 총평가 1 현금 10% · 10 5% · 100 1% 상한 - 총평가 1 현금 10% · 10 5% · 100 1% 상한
- 연속 매도: 보유 코인을 매도 신호 수로 균등 분할 (상한 없음) - 1 미만: buy_cash_pct(기본 10%) 분할 매수 · sell_coin_pct(기본 10%) 분할 매도
- 연속 매도: 허용 수량을 매도 신호 수로 균등 분할
- 원화 부족 매수 스킵, 코인 없으면 매도 스킵 - 원화 부족 매수 스킵, 코인 없으면 매도 스킵
Args: Args:
@@ -251,7 +262,12 @@ def simulate_gt_signals_pnl(
for side, cluster in _cluster_signals(period_signals): for side, cluster in _cluster_signals(period_signals):
cluster_size = len(cluster) cluster_size = len(cluster)
if side == "buy": if side == "buy":
budget = cash effective_buy_pct = resolve_buy_cash_pct(
sizing_rules, cluster_size, buy_cash_pct
)
equity = cash + coin_qty * float(cluster[0]["price"])
cash_cap = max_buy_from_cash(equity, cash, base_pct=effective_buy_pct)
budget = min(cash, cash_cap)
per_buy = budget / cluster_size if cluster_size else 0.0 per_buy = budget / cluster_size if cluster_size else 0.0
for sig in cluster: for sig in cluster:
trade_id += 1 trade_id += 1
@@ -282,7 +298,7 @@ def simulate_gt_signals_pnl(
) )
continue continue
equity = cash + coin_qty * price equity = cash + coin_qty * price
cash_cap = max_buy_from_cash(equity, cash) cash_cap = max_buy_from_cash(equity, cash, base_pct=effective_buy_pct)
order_krw = min(per_buy, cash, cash_cap) order_krw = min(per_buy, cash, cash_cap)
if order_krw < min_order_krw: if order_krw < min_order_krw:
@@ -335,7 +351,10 @@ def simulate_gt_signals_pnl(
) )
) )
else: else:
budget_coin = coin_qty effective_sell_pct = resolve_sell_coin_pct(
sizing_rules, cluster_size, sell_coin_pct
)
budget_coin = max_sell_coin_qty(coin_qty, sell_pct=effective_sell_pct)
per_sell = budget_coin / cluster_size if cluster_size else 0.0 per_sell = budget_coin / cluster_size if cluster_size else 0.0
for sig in cluster: for sig in cluster:
trade_id += 1 trade_id += 1
@@ -436,7 +455,15 @@ def simulate_gt_signals_pnl(
"fee_rate": fee_rate, "fee_rate": fee_rate,
"slippage_rate": slippage_rate, "slippage_rate": slippage_rate,
"daily_max_trades": daily_max_trades, "daily_max_trades": daily_max_trades,
**buy_sizing_metadata(), **buy_sizing_metadata(base_buy_pct=buy_cash_pct, base_sell_pct=sell_coin_pct),
"sizing_rules_applied": sizing_rules is not None,
"learned_default_buy_cash_pct": (
sizing_rules.get("default_buy_cash_pct") if sizing_rules else None
),
"learned_default_sell_coin_pct": (
sizing_rules.get("default_sell_coin_pct") if sizing_rules else None
),
"learned_by_cluster": sizing_rules.get("by_cluster") if sizing_rules else None,
"sim_lookback_days": sim_lookback_days, "sim_lookback_days": sim_lookback_days,
"period_from": start_str, "period_from": start_str,
"period_to": end_dt.strftime("%Y-%m-%d %H:%M:%S"), "period_to": end_dt.strftime("%Y-%m-%d %H:%M:%S"),
@@ -468,7 +495,7 @@ def _empty_signal_pnl(
"total_pnl_krw": 0.0, "total_pnl_krw": 0.0,
"total_return_pct": 0.0, "total_return_pct": 0.0,
"fee_rate": fee_rate, "fee_rate": fee_rate,
**buy_sizing_metadata(), **buy_sizing_metadata(base_buy_pct=buy_cash_pct, base_sell_pct=sell_coin_pct),
"sim_lookback_days": sim_lookback_days, "sim_lookback_days": sim_lookback_days,
"period_from": period_from, "period_from": period_from,
"period_to": period_to, "period_to": period_to,

View File

@@ -6,7 +6,7 @@ import pandas as pd
from typing import Protocol from typing import Protocol
from deepcoin.ground_truth.zigzag import Pivot from bithumb.ground_truth.zigzag import Pivot
class _LegLike(Protocol): class _LegLike(Protocol):

View File

@@ -0,0 +1,97 @@
"""연속 매수·매도 클러스터 상태별 매수·매도 비율 규칙."""
from __future__ import annotations
import json
from datetime import datetime
from pathlib import Path
from typing import Any
CLUSTER_SIZE_KEYS: tuple[str, ...] = ("1", "2", "3+")
def cluster_size_key(cluster_size: int) -> str:
"""클러스터 크기를 규칙 lookup 키로 변환한다."""
size = max(int(cluster_size), 1)
if size >= 3:
return "3+"
return str(size)
def resolve_buy_cash_pct(
rules: dict[str, Any] | None,
cluster_size: int,
default: float | None,
) -> float | None:
"""클러스터 상태에 맞는 1회 매수 현금 비율을 반환한다."""
if not rules:
return default
by_side = (rules.get("by_cluster") or {}).get("buy") or {}
key = cluster_size_key(cluster_size)
if key in by_side:
return float(by_side[key])
fallback = rules.get("default_buy_cash_pct")
if fallback is not None:
return float(fallback)
return default
def resolve_sell_coin_pct(
rules: dict[str, Any] | None,
cluster_size: int,
default: float | None,
) -> float | None:
"""클러스터 상태에 맞는 1회 매도 코인 비율을 반환한다."""
if not rules:
return default
by_side = (rules.get("by_cluster") or {}).get("sell") or {}
key = cluster_size_key(cluster_size)
if key in by_side:
return float(by_side[key])
fallback = rules.get("default_sell_coin_pct")
if fallback is not None:
return float(fallback)
return default
def load_sizing_rules(path: Path) -> dict[str, Any] | None:
"""JSON 사이징 규칙을 로드한다. 파일이 없으면 None."""
if not path.exists():
return None
with path.open(encoding="utf-8") as fp:
data = json.load(fp)
return data if isinstance(data, dict) else None
def save_sizing_rules(rules: dict[str, Any], path: Path) -> Path:
"""사이징 규칙 JSON 저장."""
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as fp:
json.dump(rules, fp, ensure_ascii=False, indent=2)
return path
def empty_by_cluster() -> dict[str, dict[str, float]]:
"""빈 by_cluster 구조."""
return {"buy": {}, "sell": {}}
def merge_rules(
*,
default_buy: float,
default_sell: float,
by_cluster: dict[str, dict[str, float]] | None = None,
technique_id: str = "",
symbol: str = "",
tuning: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""튜닝 결과 dict를 표준 규칙 형식으로 만든다."""
return {
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"technique_id": technique_id,
"symbol": symbol,
"default_buy_cash_pct": round(float(default_buy), 4),
"default_sell_coin_pct": round(float(default_sell), 4),
"by_cluster": by_cluster or empty_by_cluster(),
"tuning": tuning or {},
}

View File

@@ -0,0 +1,273 @@
"""연속 매수·매도 클러스터별 사이징 비율 튜닝."""
from __future__ import annotations
from datetime import datetime, timedelta
from typing import Any
from bithumb.config import Settings
from bithumb.evaluation.causal_sim import normalize_signals_for_sim
from bithumb.ground_truth.pnl import _cluster_signals, simulate_gt_signals_pnl
from bithumb.ground_truth.sizing_rules import (
CLUSTER_SIZE_KEYS,
cluster_size_key,
merge_rules,
)
PCT_CANDIDATES: tuple[float, ...] = (
0.10,
0.15,
0.20,
0.25,
0.30,
0.40,
0.50,
0.60,
0.70,
0.80,
1.0,
)
def _parse_signal_dt(value: str) -> datetime:
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
def _sim_kwargs(settings: Settings, data_end: str, last_mark_price: float) -> dict[str, Any]:
return {
"initial_cash_krw": settings.gt_initial_cash_krw,
"fee_rate": settings.gt_trading_fee_rate,
"min_order_krw": settings.ops_min_order_krw,
"slippage_rate": settings.ops_slippage_rate,
"daily_max_trades": settings.ops_daily_max_trades,
"sim_lookback_days": settings.gt_sim_lookback_days,
"data_end": data_end,
"last_mark_price": last_mark_price,
}
def count_cluster_buckets(
signals: list[dict[str, Any]],
*,
sim_lookback_days: int,
data_end: str,
) -> dict[str, dict[str, int]]:
"""sim 기간 내 (side, cluster_size) 빈도."""
end_dt = _parse_signal_dt(data_end)
start_dt = end_dt - timedelta(days=sim_lookback_days)
period = [s for s in signals if _parse_signal_dt(s["datetime"]) >= start_dt]
counts: dict[str, dict[str, int]] = {"buy": {}, "sell": {}}
for side, cluster in _cluster_signals(period):
key = cluster_size_key(len(cluster))
bucket = counts.setdefault(side, {})
bucket[key] = bucket.get(key, 0) + 1
return counts
def _rules_for_sim(
default_buy: float,
default_sell: float,
by_cluster: dict[str, dict[str, float]],
) -> dict[str, Any]:
return merge_rules(
default_buy=default_buy,
default_sell=default_sell,
by_cluster=by_cluster,
)
def _run_sim(
signals: list[dict[str, Any]],
settings: Settings,
data_end: str,
last_mark_price: float,
*,
default_buy: float,
default_sell: float,
by_cluster: dict[str, dict[str, float]] | None = None,
) -> dict[str, Any]:
rules = _rules_for_sim(default_buy, default_sell, by_cluster or {"buy": {}, "sell": {}})
return simulate_gt_signals_pnl(
signals=signals,
buy_cash_pct=default_buy,
sell_coin_pct=default_sell,
sizing_rules=rules,
**_sim_kwargs(settings, data_end, last_mark_price),
)
def _best_pct(
signals: list[dict[str, Any]],
settings: Settings,
data_end: str,
last_mark_price: float,
*,
side: str,
fixed_buy: float,
fixed_sell: float,
by_cluster: dict[str, dict[str, float]],
bucket_key: str | None = None,
) -> tuple[float, float]:
"""한 축(side 또는 bucket)에 대해 최적 pct와 sim 수익률을 반환한다."""
best_pct = fixed_buy if side == "buy" else fixed_sell
best_return = float("-inf")
for pct in PCT_CANDIDATES:
buy = fixed_buy
sell = fixed_sell
cluster = {"buy": dict(by_cluster.get("buy", {})), "sell": dict(by_cluster.get("sell", {}))}
if bucket_key is not None:
cluster.setdefault(side, {})[bucket_key] = pct
else:
if side == "buy":
buy = pct
else:
sell = pct
sim = _run_sim(
signals,
settings,
data_end,
last_mark_price,
default_buy=buy,
default_sell=sell,
by_cluster=cluster,
)
ret = float(sim.get("total_return_pct") or 0)
if ret > best_return:
best_return = ret
best_pct = pct
return best_pct, best_return
def tune_sizing_rules(
settings: Settings,
signals: list[dict[str, Any]],
*,
data_end: str,
last_mark_price: float,
technique_id: str,
min_bucket_samples: int = 5,
) -> tuple[dict[str, Any], dict[str, Any]]:
"""타점 고정 상태에서 전역·클러스터별 매수·매도 비율을 탐색한다."""
normalized = normalize_signals_for_sim(signals)
base_buy = settings.ops_buy_cash_pct
base_sell = settings.ops_sell_coin_pct
by_cluster: dict[str, dict[str, float]] = {"buy": {}, "sell": {}}
history: list[dict[str, Any]] = []
buy_pct, buy_ret = _best_pct(
normalized,
settings,
data_end,
last_mark_price,
side="buy",
fixed_buy=base_buy,
fixed_sell=base_sell,
by_cluster=by_cluster,
)
history.append({"step": "global_buy", "buy_pct": buy_pct, "return_pct": buy_ret})
sell_pct, sell_ret = _best_pct(
normalized,
settings,
data_end,
last_mark_price,
side="sell",
fixed_buy=buy_pct,
fixed_sell=base_sell,
by_cluster=by_cluster,
)
history.append({"step": "global_sell", "sell_pct": sell_pct, "return_pct": sell_ret})
buy_pct, buy_ret = _best_pct(
normalized,
settings,
data_end,
last_mark_price,
side="buy",
fixed_buy=buy_pct,
fixed_sell=sell_pct,
by_cluster=by_cluster,
)
history.append({"step": "refine_buy", "buy_pct": buy_pct, "return_pct": buy_ret})
sell_pct, sell_ret = _best_pct(
normalized,
settings,
data_end,
last_mark_price,
side="sell",
fixed_buy=buy_pct,
fixed_sell=sell_pct,
by_cluster=by_cluster,
)
history.append({"step": "refine_sell", "sell_pct": sell_pct, "return_pct": sell_ret})
bucket_counts = count_cluster_buckets(
normalized,
sim_lookback_days=settings.gt_sim_lookback_days,
data_end=data_end,
)
for side in ("buy", "sell"):
for key in CLUSTER_SIZE_KEYS:
if bucket_counts.get(side, {}).get(key, 0) < min_bucket_samples:
continue
pct, ret = _best_pct(
normalized,
settings,
data_end,
last_mark_price,
side=side,
fixed_buy=buy_pct,
fixed_sell=sell_pct,
by_cluster=by_cluster,
bucket_key=key,
)
by_cluster.setdefault(side, {})[key] = pct
history.append(
{
"step": f"bucket_{side}_{key}",
"pct": pct,
"samples": bucket_counts[side][key],
"return_pct": ret,
}
)
final_sim = _run_sim(
normalized,
settings,
data_end,
last_mark_price,
default_buy=buy_pct,
default_sell=sell_pct,
by_cluster=by_cluster,
)
tuning_meta = {
"objective": "total_return_pct",
"pct_candidates": list(PCT_CANDIDATES),
"min_bucket_samples": min_bucket_samples,
"cluster_counts": bucket_counts,
"history": history,
"baseline_return_pct": _run_sim(
normalized,
settings,
data_end,
last_mark_price,
default_buy=base_buy,
default_sell=base_sell,
).get("total_return_pct"),
"final_return_pct": final_sim.get("total_return_pct"),
"final_buys_executed": final_sim.get("buys_executed"),
"final_sells_executed": final_sim.get("sells_executed"),
}
rules = merge_rules(
default_buy=buy_pct,
default_sell=sell_pct,
by_cluster=by_cluster,
technique_id=technique_id,
symbol=settings.symbol,
tuning=tuning_meta,
)
return rules, final_sim

View File

@@ -6,7 +6,7 @@ from dataclasses import dataclass
import pandas as pd import pandas as pd
from deepcoin.ground_truth.zigzag import Pivot, find_zigzag_pivots from bithumb.ground_truth.zigzag import Pivot, find_zigzag_pivots
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@@ -0,0 +1,23 @@
"""멀티 타임프레임(MTF) 인과 피처 추출."""
from bithumb.mtf.alignment import as_of_from_signal_bar, last_complete_bar_index
from bithumb.mtf.extractor import MtfFeatureExtractor, MtfSnapshot
from bithumb.mtf.filter import MtfSignalFilter, score_mtf_rules
from bithumb.mtf.rules import MtfRule, MtfRuleSet, derive_rules_from_report, load_mtf_rules, save_mtf_rules
from bithumb.mtf.store import MTF_INTERVALS, MultiTimeframeStore
__all__ = [
"MTF_INTERVALS",
"MultiTimeframeStore",
"MtfFeatureExtractor",
"MtfSnapshot",
"MtfSignalFilter",
"MtfRule",
"MtfRuleSet",
"derive_rules_from_report",
"load_mtf_rules",
"save_mtf_rules",
"score_mtf_rules",
"as_of_from_signal_bar",
"last_complete_bar_index",
]

View File

@@ -7,9 +7,9 @@ from typing import Any
import pandas as pd import pandas as pd
from deepcoin.mtf.alignment import as_of_from_signal_bar, resolve_bar_index from bithumb.mtf.alignment import as_of_from_signal_bar, resolve_bar_index
from deepcoin.mtf.features import snapshot_at_index from bithumb.mtf.features import snapshot_at_index
from deepcoin.mtf.store import MTF_INTERVALS, MultiTimeframeStore from bithumb.mtf.store import MTF_INTERVALS, MultiTimeframeStore
@dataclass @dataclass

View File

@@ -6,7 +6,7 @@ from typing import Any
import pandas as pd import pandas as pd
from deepcoin.techniques.indicators import atr, bollinger_bands, ema, macd, rsi from bithumb.techniques.indicators import atr, bollinger_bands, ema, macd, rsi
FEATURE_NAMES: tuple[str, ...] = ( FEATURE_NAMES: tuple[str, ...] = (
"close", "close",

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
from typing import Any from typing import Any
from deepcoin.mtf.trend_gate import HtfTrendGate from bithumb.mtf.trend_gate import HtfTrendGate
from deepcoin.mtf.extractor import MtfFeatureExtractor, MtfSnapshot from bithumb.mtf.extractor import MtfFeatureExtractor, MtfSnapshot
from deepcoin.mtf.rules import MtfRule, MtfRuleSet from bithumb.mtf.rules import MtfRule, MtfRuleSet
def evaluate_rule(rule: MtfRule, snapshot: MtfSnapshot) -> bool | None: def evaluate_rule(rule: MtfRule, snapshot: MtfSnapshot) -> bool | None:

View File

@@ -5,9 +5,9 @@ from __future__ import annotations
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from deepcoin.mtf.alignment import as_of_from_signal_bar from bithumb.mtf.alignment import as_of_from_signal_bar
from deepcoin.mtf.features import snapshot_at_index from bithumb.mtf.features import snapshot_at_index
from deepcoin.mtf.store import MultiTimeframeStore from bithumb.mtf.store import MultiTimeframeStore
def _vectorized_tf_indices( def _vectorized_tf_indices(

View File

@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, Literal from typing import Any, Literal
from deepcoin.evaluation.gt_align import GT_SIGNAL_TYPES from bithumb.evaluation.gt_align import GT_SIGNAL_TYPES
Operator = Literal["<=", ">="] Operator = Literal["<=", ">="]

View File

@@ -6,9 +6,9 @@ from pathlib import Path
import pandas as pd import pandas as pd
from deepcoin.data.candle_loader import load_candles from bithumb.data.candle_loader import load_candles
from deepcoin.data.intervals import DEFAULT_DOWNLOAD_INTERVALS, interval_label from bithumb.data.intervals import DEFAULT_DOWNLOAD_INTERVALS, interval_label
from deepcoin.mtf.features import compute_feature_frame from bithumb.mtf.features import compute_feature_frame
MTF_INTERVALS: tuple[int, ...] = tuple(DEFAULT_DOWNLOAD_INTERVALS) MTF_INTERVALS: tuple[int, ...] = tuple(DEFAULT_DOWNLOAD_INTERVALS)

View File

@@ -1,5 +1,5 @@
"""알림 채널 (텔레그램 등).""" """알림 채널 (텔레그램 등)."""
from deepcoin.notifications.telegram import TelegramNotifier, create_telegram_notifier from bithumb.notifications.telegram import TelegramNotifier, create_telegram_notifier
__all__ = ["TelegramNotifier", "create_telegram_notifier"] __all__ = ["TelegramNotifier", "create_telegram_notifier"]

View File

@@ -112,7 +112,7 @@ class TelegramNotifier:
equity = cash + coin_qty * price equity = cash + coin_qty * price
lines = [ lines = [
f"[DeepCoin] {side_label} 체결 ({mode_label})", f"[Bithumb] {side_label} 체결 ({mode_label})",
f"{coin_name} ({symbol}) | {technique_id}", f"{coin_name} ({symbol}) | {technique_id}",
f"시각: {datetime_str}", f"시각: {datetime_str}",
f"신호: {signal_type or side}", f"신호: {signal_type or side}",
@@ -145,13 +145,36 @@ class TelegramNotifier:
side_label = "매수" if side == "buy" else "매도" side_label = "매수" if side == "buy" else "매도"
mode_label = "LIVE" if mode == "live" else "PAPER" mode_label = "LIVE" if mode == "live" else "PAPER"
text = ( text = (
f"[DeepCoin] {side_label} 실패 ({mode_label})\n" f"[Bithumb] {side_label} 실패 ({mode_label})\n"
f"{symbol} | {technique_id}\n" f"{symbol} | {technique_id}\n"
f"시각: {datetime_str}\n" f"시각: {datetime_str}\n"
f"사유: {reason}" f"사유: {reason}"
) )
return self.send_message(text) return self.send_message(text)
def notify_ops_error(
self,
*,
mode: str,
symbol: str,
technique_id: str,
stage: str,
error: str,
detail: str = "",
) -> bool:
"""운영 tick·체결 등 예외 발생 알림 (프로세스는 계속 실행)."""
mode_label = "LIVE" if mode == "live" else "PAPER"
lines = [
f"[Bithumb] 운영 오류 ({mode_label})",
f"{symbol} | {technique_id}",
f"단계: {stage}",
f"원인: {error}",
]
if detail:
lines.append(detail)
lines.append("프로세스는 계속 실행됩니다.")
return self.send_message("\n".join(lines))
def _fmt_krw(value: float) -> str: def _fmt_krw(value: float) -> str:
"""원화 금액 포맷.""" """원화 금액 포맷."""

View File

@@ -0,0 +1,12 @@
"""현물 3단계 운영 — composite_v3 + MTF."""
from bithumb.operations.backtest import run_filtered_backtest, save_backtest_report
from bithumb.operations.runner import OperationsRunner
from bithumb.operations.signal_pipeline import run_signal_pipeline
__all__ = [
"OperationsRunner",
"run_filtered_backtest",
"run_signal_pipeline",
"save_backtest_report",
]

View File

@@ -7,10 +7,11 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from deepcoin.config import Settings from bithumb.config import Settings
from deepcoin.evaluation.causal_sim import normalize_signals_for_sim from bithumb.evaluation.causal_sim import normalize_signals_for_sim
from deepcoin.ground_truth.pnl import simulate_gt_signals_pnl from bithumb.ground_truth.pnl import simulate_gt_signals_pnl
from deepcoin.operations.signal_pipeline import run_signal_pipeline from bithumb.ground_truth.sizing_rules import load_sizing_rules
from bithumb.operations.signal_pipeline import run_signal_pipeline
def run_filtered_backtest(settings: Settings) -> dict[str, Any]: def run_filtered_backtest(settings: Settings) -> dict[str, Any]:
@@ -22,28 +23,25 @@ def run_filtered_backtest(settings: Settings) -> dict[str, Any]:
) )
kept = pipeline["kept"] kept = pipeline["kept"]
normalized = normalize_signals_for_sim(kept) normalized = normalize_signals_for_sim(kept)
sim = simulate_gt_signals_pnl( sizing_rules = load_sizing_rules(settings.ops_sizing_rules_json)
signals=normalized, sim_kwargs = {
initial_cash_krw=settings.gt_initial_cash_krw, "initial_cash_krw": settings.gt_initial_cash_krw,
fee_rate=settings.gt_trading_fee_rate, "fee_rate": settings.gt_trading_fee_rate,
min_order_krw=settings.ops_min_order_krw, "min_order_krw": settings.ops_min_order_krw,
slippage_rate=settings.ops_slippage_rate, "slippage_rate": settings.ops_slippage_rate,
daily_max_trades=settings.ops_daily_max_trades, "daily_max_trades": settings.ops_daily_max_trades,
sim_lookback_days=settings.gt_sim_lookback_days, "buy_cash_pct": settings.ops_buy_cash_pct,
data_end=pipeline["data_end"], "sell_coin_pct": settings.ops_sell_coin_pct,
last_mark_price=pipeline["last_price"], "sizing_rules": sizing_rules,
) "sim_lookback_days": settings.gt_sim_lookback_days,
"data_end": pipeline["data_end"],
"last_mark_price": pipeline["last_price"],
}
sim = simulate_gt_signals_pnl(signals=normalized, **sim_kwargs)
raw_sim = simulate_gt_signals_pnl( raw_sim = simulate_gt_signals_pnl(
signals=normalize_signals_for_sim(pipeline["scoped_raw_signals"]), signals=normalize_signals_for_sim(pipeline["scoped_raw_signals"]),
initial_cash_krw=settings.gt_initial_cash_krw, **sim_kwargs,
fee_rate=settings.gt_trading_fee_rate,
min_order_krw=settings.ops_min_order_krw,
slippage_rate=settings.ops_slippage_rate,
daily_max_trades=settings.ops_daily_max_trades,
sim_lookback_days=settings.gt_sim_lookback_days,
data_end=pipeline["data_end"],
last_mark_price=pipeline["last_price"],
) )
return { return {

View File

@@ -5,9 +5,9 @@ from __future__ import annotations
import logging import logging
from dataclasses import replace from dataclasses import replace
from deepcoin.config import Settings from bithumb.config import Settings
from deepcoin.data.candle_store import CandleStore from bithumb.data.candle_store import CandleStore
from deepcoin.data.downloader import CandleDownloader, DownloadResult from bithumb.data.downloader import CandleDownloader, DownloadResult
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -5,10 +5,10 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from deepcoin.config import Settings from bithumb.config import Settings
from deepcoin.data.intervals import interval_label from bithumb.data.intervals import interval_label
from deepcoin.ground_truth.chart import render_ground_truth_sim_chart from bithumb.ground_truth.chart import render_ground_truth_sim_chart
from deepcoin.operations.backtest import run_filtered_backtest from bithumb.operations.backtest import run_filtered_backtest
def _ops_chart_shell( def _ops_chart_shell(
@@ -19,6 +19,12 @@ def _ops_chart_shell(
"""운영 백테스트 차트용 GT 메타 셸을 구성한다.""" """운영 백테스트 차트용 GT 메타 셸을 구성한다."""
slip_pct = settings.ops_slippage_rate * 100 slip_pct = settings.ops_slippage_rate * 100
daily_cap = settings.ops_daily_max_trades daily_cap = settings.ops_daily_max_trades
sizing_note = ""
if sim_pnl.get("sizing_rules_applied"):
lb = sim_pnl.get("learned_default_buy_cash_pct")
ls = sim_pnl.get("learned_default_sell_coin_pct")
if lb is not None and ls is not None:
sizing_note = f" · 학습 비율 {float(lb) * 100:.0f}%/{float(ls) * 100:.0f}%"
return { return {
"meta": { "meta": {
"symbol": settings.symbol, "symbol": settings.symbol,
@@ -34,6 +40,7 @@ def _ops_chart_shell(
"ops_note": ( "ops_note": (
f"슬리피지 {slip_pct:.2f}% · 일 체결 상한 {daily_cap} · " f"슬리피지 {slip_pct:.2f}% · 일 체결 상한 {daily_cap} · "
f"MTF {'on' if pipeline.get('mtf_enabled') else 'off'}" f"MTF {'on' if pipeline.get('mtf_enabled') else 'off'}"
f"{sizing_note}"
), ),
"data_from": sim_pnl.get("period_from"), "data_from": sim_pnl.get("period_from"),
"data_to": sim_pnl.get("period_to"), "data_to": sim_pnl.get("period_to"),

View File

@@ -6,14 +6,16 @@ import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any from typing import Any
from deepcoin.api.bithumb_private import BithumbPrivateClient from bithumb.api.bithumb_private import BithumbPrivateClient
from deepcoin.config import Settings from bithumb.config import Settings
from deepcoin.operations.execution import fill_price from bithumb.ground_truth.sizing_rules import load_sizing_rules
from deepcoin.operations.trade_engine import ( from bithumb.operations.execution import fill_price
from bithumb.operations.trade_engine import (
TradeResult, TradeResult,
apply_trade_to_portfolio, apply_trade_to_portfolio,
compute_buy_order, compute_buy_order,
compute_sell_order, compute_sell_order,
spendable_cash_for_exchange_buy,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -38,6 +40,7 @@ class PaperExecutor(OrderExecutor):
def __init__(self, settings: Settings) -> None: def __init__(self, settings: Settings) -> None:
self.settings = settings self.settings = settings
self.sizing_rules = load_sizing_rules(settings.ops_sizing_rules_json)
def execute_signal( def execute_signal(
self, self,
@@ -66,6 +69,8 @@ class PaperExecutor(OrderExecutor):
fee_rate=fee_rate, fee_rate=fee_rate,
min_order_krw=min_order, min_order_krw=min_order,
cluster_size=cluster_size, cluster_size=cluster_size,
buy_cash_pct=self.settings.ops_buy_cash_pct,
sizing_rules=self.sizing_rules,
) )
else: else:
trade = compute_sell_order( trade = compute_sell_order(
@@ -74,6 +79,8 @@ class PaperExecutor(OrderExecutor):
fee_rate=fee_rate, fee_rate=fee_rate,
min_order_krw=min_order, min_order_krw=min_order,
cluster_size=cluster_size, cluster_size=cluster_size,
sell_coin_pct=self.settings.ops_sell_coin_pct,
sizing_rules=self.sizing_rules,
) )
if trade.executed: if trade.executed:
@@ -87,6 +94,7 @@ class LiveExecutor(OrderExecutor):
def __init__(self, settings: Settings, client: BithumbPrivateClient) -> None: def __init__(self, settings: Settings, client: BithumbPrivateClient) -> None:
self.settings = settings self.settings = settings
self.client = client self.client = client
self.sizing_rules = load_sizing_rules(settings.ops_sizing_rules_json)
def _sync_portfolio(self, portfolio: dict[str, Any]) -> None: def _sync_portfolio(self, portfolio: dict[str, Any]) -> None:
"""거래소 잔고로 포트폴리오를 동기화한다.""" """거래소 잔고로 포트폴리오를 동기화한다."""
@@ -116,27 +124,46 @@ class LiveExecutor(OrderExecutor):
coin = float(portfolio.get("coin_qty", 0)) coin = float(portfolio.get("coin_qty", 0))
if side == "buy": if side == "buy":
spendable = spendable_cash_for_exchange_buy(
cash,
self.settings.ops_exchange_fee_lock_rate,
self.settings.ops_buy_safety_buffer_krw,
)
trade = compute_buy_order( trade = compute_buy_order(
cash_krw=cash, cash_krw=spendable,
coin_qty=coin, coin_qty=coin,
price=price, price=price,
fee_rate=fee_rate, fee_rate=fee_rate,
min_order_krw=min_order, min_order_krw=min_order,
cluster_size=cluster_size, cluster_size=cluster_size,
buy_cash_pct=self.settings.ops_buy_cash_pct,
sizing_rules=self.sizing_rules,
) )
if not trade.executed: if not trade.executed:
if trade.expected_skip:
logger.info(
"live buy skip (%s): cash=%.0f spendable=%.0f coin=%.8f",
trade.skip_reason,
cash,
spendable,
coin,
)
return trade return trade
try: try:
resp = self.client.market_buy_krw(self.settings.market, trade.order_krw) resp = self.client.market_buy_krw(self.settings.market, trade.order_krw)
trade.api_response = resp trade.api_response = resp
self._sync_portfolio(portfolio) self._sync_portfolio(portfolio)
except Exception as exc: except Exception as exc:
logger.exception("live buy failed") logger.exception(
"live buy failed order_krw=%.0f cash=%.0f",
trade.order_krw,
cash,
)
return TradeResult( return TradeResult(
executed=False, executed=False,
side="buy", side="buy",
order_krw=0.0, order_krw=trade.order_krw,
order_coin=0.0, order_coin=trade.order_coin,
fee_krw=0.0, fee_krw=0.0,
price=price, price=price,
skip_reason=str(exc), skip_reason=str(exc),
@@ -149,20 +176,33 @@ class LiveExecutor(OrderExecutor):
fee_rate=fee_rate, fee_rate=fee_rate,
min_order_krw=min_order, min_order_krw=min_order,
cluster_size=cluster_size, cluster_size=cluster_size,
sell_coin_pct=self.settings.ops_sell_coin_pct,
sizing_rules=self.sizing_rules,
) )
if not trade.executed: if not trade.executed:
if trade.expected_skip:
logger.info(
"live sell skip (%s): %s=%.8f cash=%.0f",
trade.skip_reason,
self.settings.symbol,
coin,
cash,
)
return trade return trade
try: try:
resp = self.client.market_sell_volume(self.settings.market, trade.order_coin) resp = self.client.market_sell_volume(self.settings.market, trade.order_coin)
trade.api_response = resp trade.api_response = resp
self._sync_portfolio(portfolio) self._sync_portfolio(portfolio)
except Exception as exc: except Exception as exc:
logger.exception("live sell failed") logger.exception(
"live sell failed order_coin=%.8f",
trade.order_coin,
)
return TradeResult( return TradeResult(
executed=False, executed=False,
side="sell", side="sell",
order_krw=0.0, order_krw=trade.order_krw,
order_coin=0.0, order_coin=trade.order_coin,
fee_krw=0.0, fee_krw=0.0,
price=price, price=price,
skip_reason=str(exc), skip_reason=str(exc),

View File

@@ -0,0 +1,253 @@
"""live 운영 전 사전 점검 및 상태 초기화."""
from __future__ import annotations
import json
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any
from bithumb.api.bithumb_private import BithumbPrivateClient
from bithumb.config import Settings
from bithumb.notifications.telegram import create_telegram_notifier
from bithumb.operations.state_store import load_state, save_state
def create_bithumb_client(settings: Settings) -> BithumbPrivateClient:
"""설정 기반 빗썸 Private 클라이언트를 생성한다."""
return BithumbPrivateClient(
access_key=settings.bithumb_access_key,
secret_key=settings.bithumb_secret_key,
base_url=settings.api_url,
sleep_sec=settings.request_sleep_sec,
retries=settings.request_retries,
)
def sync_portfolio_from_exchange(
portfolio: dict[str, Any],
client: BithumbPrivateClient,
symbol: str,
*,
mode: str = "live",
) -> dict[str, float]:
"""거래소 잔고로 포트폴리오를 갱신한다.
Returns:
{"cash_krw", "coin_qty"} 스냅샷.
"""
krw_avail, krw_locked = client.get_balance("KRW")
coin_avail, coin_locked = client.get_balance(symbol)
portfolio["cash_krw"] = krw_avail
portfolio["coin_qty"] = coin_avail
portfolio["mode"] = mode
return {
"cash_krw": krw_avail,
"cash_locked_krw": krw_locked,
"coin_qty": coin_avail,
"coin_locked_qty": coin_locked,
}
def backup_state_file(state_path: Path) -> Path | None:
"""기존 운영 상태 JSON을 백업한다."""
if not state_path.exists():
return None
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup = state_path.with_name(f"{state_path.stem}.backup_{stamp}{state_path.suffix}")
shutil.copy2(state_path, backup)
return backup
def init_live_state(
settings: Settings,
*,
backup: bool = True,
preserve_bar_cursor: bool = True,
) -> dict[str, Any]:
"""paper 상태를 live로 전환하고 거래소 잔고를 반영한다."""
state_path = settings.ops_state_json
backup_path: Path | None = None
if backup and state_path.exists():
backup_path = backup_state_file(state_path)
state = load_state(state_path, initial_cash_krw=settings.gt_initial_cash_krw)
if not preserve_bar_cursor:
state["last_processed_bar_index"] = -1
state["last_processed_datetime"] = None
client = create_bithumb_client(settings)
balances = sync_portfolio_from_exchange(
state["portfolio"],
client,
settings.symbol,
mode="live",
)
state["live_initialized_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
state["live_init_backup"] = str(backup_path) if backup_path else None
save_state(state_path, state)
return {
"state_path": str(state_path),
"backup_path": str(backup_path) if backup_path else None,
"balances": balances,
"last_processed_bar_index": state.get("last_processed_bar_index", -1),
}
def run_preflight(settings: Settings) -> dict[str, Any]:
"""live 운영 전 점검 항목을 실행한다."""
checks: list[dict[str, Any]] = []
ok = True
def add(name: str, passed: bool, detail: str, *, required: bool = True) -> None:
nonlocal ok
if required and not passed:
ok = False
checks.append(
{
"name": name,
"passed": passed,
"required": required,
"detail": detail,
}
)
add(
"ops_mode_live",
settings.ops_mode == "live",
f"OPS_MODE={settings.ops_mode}",
)
add(
"api_keys",
bool(settings.bithumb_access_key and settings.bithumb_secret_key),
"BITHUMB_ACCESS_KEY / BITHUMB_SECRET_KEY 설정",
)
add(
"technique",
bool(settings.ops_technique_id),
f"OPS_TECHNIQUE_ID={settings.ops_technique_id}",
)
technique_path = settings.techniques_dir / f"{settings.ops_technique_id}.json"
add(
"technique_cache",
technique_path.exists(),
str(technique_path),
)
add(
"db_exists",
settings.db_path.exists(),
str(settings.db_path),
)
add(
"state_path_writable",
True,
str(settings.ops_state_json),
)
settings.ops_state_json.parent.mkdir(parents=True, exist_ok=True)
settings.ops_report_json.parent.mkdir(parents=True, exist_ok=True)
balances: dict[str, float] | None = None
if settings.bithumb_access_key and settings.bithumb_secret_key:
try:
client = create_bithumb_client(settings)
accounts = client.get_accounts()
add(
"api_connectivity",
isinstance(accounts, list),
f"accounts={len(accounts)}",
)
balances = sync_portfolio_from_exchange(
{"cash_krw": 0.0, "coin_qty": 0.0},
client,
settings.symbol,
)
add(
"krw_balance",
balances["cash_krw"] >= settings.ops_min_order_krw,
(
f"가용 {_fmt_krw(balances['cash_krw'])} "
f"(최소 주문 {_fmt_krw(settings.ops_min_order_krw)})"
),
)
add(
"coin_balance",
True,
(
f"{settings.symbol} {balances['coin_qty']:.8f} "
f"(locked {balances['coin_locked_qty']:.8f})"
),
required=False,
)
except Exception as exc:
add("api_connectivity", False, str(exc))
else:
add("api_connectivity", False, "API 키 없음")
telegram = create_telegram_notifier(
settings.telegram_bot_token,
settings.telegram_chat_id,
enabled=settings.ops_telegram_enabled,
)
telegram_ok = False
if telegram.is_active:
telegram_ok = telegram.send_message(
"[Bithumb] live 사전 점검 — 텔레그램 연결 OK"
)
add(
"telegram",
telegram_ok if telegram.is_active else True,
"활성" if telegram.is_active else "비활성(선택)",
required=telegram.is_active,
)
backtest_path = settings.ops_filtered_backtest_json
add(
"backtest_report",
backtest_path.exists(),
str(backtest_path),
required=False,
)
add(
"daily_max_trades",
settings.ops_daily_max_trades > 0,
str(settings.ops_daily_max_trades),
)
add(
"slippage_rate",
settings.ops_slippage_rate >= 0,
f"{settings.ops_slippage_rate * 100:.3f}%",
)
return {
"ok": ok,
"symbol": settings.symbol,
"market": settings.market,
"technique_id": settings.ops_technique_id,
"mode": settings.ops_mode,
"balances": balances,
"checks": checks,
}
def save_preflight_report(path: Path, report: dict[str, Any]) -> Path:
"""사전 점검 결과를 JSON으로 저장한다."""
path.parent.mkdir(parents=True, exist_ok=True)
payload = {
**report,
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
with path.open("w", encoding="utf-8") as fp:
json.dump(payload, fp, ensure_ascii=False, indent=2)
return path
def _fmt_krw(value: float) -> str:
"""원화 금액 포맷."""
return f"{round(value):,}"

View File

@@ -5,21 +5,24 @@ from __future__ import annotations
import json import json
import logging import logging
import time import time
import traceback
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from deepcoin.config import Settings from bithumb.config import Settings
from deepcoin.ground_truth.pnl import _cluster_signals from bithumb.ground_truth.pnl import _cluster_signals
from deepcoin.notifications.telegram import create_telegram_notifier from bithumb.notifications.telegram import create_telegram_notifier
from deepcoin.operations.candle_sync import sync_ops_candles from bithumb.operations.candle_sync import sync_ops_candles
from deepcoin.operations.executor import create_executor from bithumb.operations.executor import LiveExecutor, create_executor
from deepcoin.operations.signal_pipeline import ( from bithumb.operations.live_bootstrap import sync_portfolio_from_exchange
from bithumb.operations.signal_pipeline import (
filter_signals_for_ops, filter_signals_for_ops,
generate_raw_signals, generate_raw_signals,
load_ops_candles, load_ops_candles,
) )
from deepcoin.operations.state_store import load_state, reset_daily_trade_count, save_state from bithumb.operations.state_store import load_state, reset_daily_trade_count, save_state
from bithumb.operations.trade_engine import TradeResult
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -91,9 +94,30 @@ class OperationsRunner:
initial_cash_krw=settings.gt_initial_cash_krw, initial_cash_krw=settings.gt_initial_cash_krw,
) )
self.state["portfolio"]["mode"] = settings.ops_mode self.state["portfolio"]["mode"] = settings.ops_mode
if settings.ops_mode == "live" and isinstance(self.executor, LiveExecutor):
try:
sync_portfolio_from_exchange(
self.state["portfolio"],
self.executor.client,
settings.symbol,
mode="live",
)
save_state(settings.ops_state_json, self.state)
except Exception as exc:
logger.exception("live 초기 잔고 동기화 실패")
self._notify_ops_error("init_sync", exc)
def tick(self, *, sync_candles: bool | None = None) -> dict[str, Any]: def tick(self, *, sync_candles: bool | None = None) -> dict[str, Any]:
"""신호 확인 및 체결 1회.""" """신호 확인 및 체결 1회. 예외 발생 시 텔레그램 알림 후 error 리포트 반환."""
try:
return self._tick_impl(sync_candles=sync_candles)
except Exception as exc:
logger.exception("운영 tick 실패")
self._notify_ops_error("tick", exc)
return self._build_error_report(exc, stage="tick")
def _tick_impl(self, *, sync_candles: bool | None = None) -> dict[str, Any]:
"""tick 본체."""
do_sync = sync_candles if sync_candles is not None else self.settings.ops_sync_candles do_sync = sync_candles if sync_candles is not None else self.settings.ops_sync_candles
candle_sync_results: list[Any] = [] candle_sync_results: list[Any] = []
if do_sync: if do_sync:
@@ -109,6 +133,18 @@ class OperationsRunner:
kept = filtered["kept"] kept = filtered["kept"]
reset_daily_trade_count(self.state) reset_daily_trade_count(self.state)
if self.settings.ops_mode == "live" and isinstance(self.executor, LiveExecutor):
try:
sync_portfolio_from_exchange(
self.state["portfolio"],
self.executor.client,
self.settings.symbol,
mode="live",
)
except Exception as exc:
logger.exception("tick 잔고 동기화 실패")
self._notify_ops_error("portfolio_sync", exc)
last_bar = int(self.state.get("last_processed_bar_index", -1)) last_bar = int(self.state.get("last_processed_bar_index", -1))
target_bars = _pending_bar_indices(kept, last_bar, latest_bar) target_bars = _pending_bar_indices(kept, last_bar, latest_bar)
@@ -128,11 +164,32 @@ class OperationsRunner:
(k for k in kept if k["datetime"] == sig["datetime"]), (k for k in kept if k["datetime"] == sig["datetime"]),
sig, sig,
) )
try:
trade = self.executor.execute_signal( trade = self.executor.execute_signal(
full_sig, full_sig,
self.state["portfolio"], self.state["portfolio"],
cluster_size=cluster_size, cluster_size=cluster_size,
) )
except Exception as exc:
logger.exception(
"체결 실행 예외 side=%s datetime=%s",
full_sig.get("side"),
full_sig.get("datetime"),
)
self._notify_ops_error(
"execute",
exc,
context=f"side={full_sig.get('side')} dt={full_sig.get('datetime')}",
)
trade = TradeResult(
executed=False,
side=str(full_sig.get("side", "")),
order_krw=0.0,
order_coin=0.0,
fee_krw=0.0,
price=float(full_sig.get("price", 0)),
skip_reason=str(exc),
)
record = { record = {
"datetime": full_sig["datetime"], "datetime": full_sig["datetime"],
"side": full_sig["side"], "side": full_sig["side"],
@@ -154,6 +211,7 @@ class OperationsRunner:
elif ( elif (
self.settings.ops_mode == "live" self.settings.ops_mode == "live"
and trade.skip_reason and trade.skip_reason
and not trade.expected_skip
and self.telegram.is_active and self.telegram.is_active
): ):
self.telegram.notify_trade_failure( self.telegram.notify_trade_failure(
@@ -213,6 +271,50 @@ class OperationsRunner:
self._save_report(report) self._save_report(report)
return report return report
def _notify_ops_error(
self,
stage: str,
exc: Exception,
*,
context: str = "",
) -> None:
"""운영 예외를 텔레그램으로 알린다."""
if not self.telegram.is_active:
return
tb_tail = traceback.format_exc(limit=4).strip()
detail_parts = [p for p in (context, tb_tail) if p]
self.telegram.notify_ops_error(
mode=self.settings.ops_mode,
symbol=self.settings.symbol,
technique_id=self.settings.ops_technique_id,
stage=stage,
error=str(exc),
detail="\n".join(detail_parts)[:800],
)
def _build_error_report(self, exc: Exception, *, stage: str) -> dict[str, Any]:
"""tick 실패 시 저장·표시용 리포트."""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.state["last_run_at"] = now
report = {
"generated_at": now,
"mode": self.settings.ops_mode,
"technique_id": self.settings.ops_technique_id,
"error": True,
"error_stage": stage,
"error_message": str(exc),
"executions": [],
"portfolio": self.state.get("portfolio", {}),
"trades_today_count": self.state.get("trades_today_count", 0),
"last_processed_bar_index": self.state.get("last_processed_bar_index", -1),
}
try:
save_state(self.settings.ops_state_json, self.state)
self._save_report(report)
except Exception:
logger.exception("error 리포트 저장 실패")
return report
def _notify_trade( def _notify_trade(
self, self,
signal: dict[str, Any], signal: dict[str, Any],
@@ -243,5 +345,9 @@ class OperationsRunner:
"""최신 운영 리포트 저장.""" """최신 운영 리포트 저장."""
path = self.settings.ops_report_json path = self.settings.ops_report_json
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
try:
with path.open("w", encoding="utf-8") as fp: with path.open("w", encoding="utf-8") as fp:
json.dump(report, fp, ensure_ascii=False, indent=2) json.dump(report, fp, ensure_ascii=False, indent=2)
except OSError as exc:
logger.exception("ops_report 저장 실패: %s", path)
self._notify_ops_error("save_report", exc, context=str(path))

View File

@@ -10,17 +10,17 @@ from typing import Any
import pandas as pd import pandas as pd
from deepcoin.config import Settings from bithumb.config import Settings
from deepcoin.data.candle_loader import load_candles from bithumb.data.candle_loader import load_candles
from deepcoin.mtf.extractor import MtfFeatureExtractor from bithumb.mtf.extractor import MtfFeatureExtractor
from deepcoin.mtf.filter import MtfSignalFilter from bithumb.mtf.filter import MtfSignalFilter
from deepcoin.mtf.rules import load_or_derive_mtf_rules from bithumb.mtf.rules import load_or_derive_mtf_rules
from deepcoin.mtf.store import MultiTimeframeStore from bithumb.mtf.store import MultiTimeframeStore
from deepcoin.mtf.trend_gate import HtfTrendGate from bithumb.mtf.trend_gate import HtfTrendGate
from deepcoin.operations.signal_type import enrich_signal_types from bithumb.operations.signal_type import enrich_signal_types
from deepcoin.techniques.base import TechniqueParams, TechniqueResult from bithumb.techniques.base import TechniqueParams, TechniqueResult
from deepcoin.techniques.registry import get_technique from bithumb.techniques.registry import get_technique
from deepcoin.techniques.runner import load_technique_result, run_technique, save_technique_result from bithumb.techniques.runner import load_technique_result, run_technique, save_technique_result
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -2,10 +2,12 @@
from __future__ import annotations from __future__ import annotations
import math
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from deepcoin.ground_truth.order_sizing import max_buy_from_cash from bithumb.ground_truth.order_sizing import max_buy_from_cash, max_sell_coin_qty
from bithumb.ground_truth.sizing_rules import resolve_buy_cash_pct, resolve_sell_coin_pct
@dataclass @dataclass
@@ -19,6 +21,7 @@ class TradeResult:
fee_krw: float fee_krw: float
price: float price: float
skip_reason: str = "" skip_reason: str = ""
expected_skip: bool = False
api_response: dict[str, Any] | None = None api_response: dict[str, Any] | None = None
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
@@ -31,10 +34,32 @@ class TradeResult:
"fee_krw": round(self.fee_krw, 2), "fee_krw": round(self.fee_krw, 2),
"price": self.price, "price": self.price,
"skip_reason": self.skip_reason, "skip_reason": self.skip_reason,
"expected_skip": self.expected_skip,
"api_response": self.api_response, "api_response": self.api_response,
} }
def spendable_cash_for_exchange_buy(
cash_krw: float,
fee_lock_rate: float = 0.0025,
safety_buffer_krw: float = 1000.0,
) -> float:
"""거래소 시장가 매수 시 주문 가능 원화 (수수료 lock + floor + 안전 여유).
빗썸은 주문금액 + 예약수수료를 KRW에서 동시에 lock하므로
가용 원화 전액을 price로 넣으면 insufficient_funds(400) 발생한다.
``floor(가용/(1+lock)) - safety_buffer_krw`` 주문 상한을 계산한다.
"""
cash = max(float(cash_krw), 0.0)
lock = max(float(fee_lock_rate), 0.0)
buffer = max(float(safety_buffer_krw), 0.0)
if lock <= 0:
spendable = cash
else:
spendable = cash / (1.0 + lock)
return max(0.0, math.floor(spendable) - buffer)
def compute_buy_order( def compute_buy_order(
*, *,
cash_krw: float, cash_krw: float,
@@ -43,13 +68,16 @@ def compute_buy_order(
fee_rate: float, fee_rate: float,
min_order_krw: float, min_order_krw: float,
cluster_size: int = 1, cluster_size: int = 1,
buy_cash_pct: float | None = None,
sizing_rules: dict | None = None,
) -> TradeResult: ) -> TradeResult:
"""매수 주문 금액·수량을 계산한다.""" """매수 주문 금액·수량을 계산한다."""
cash = max(float(cash_krw), 0.0) cash = max(float(cash_krw), 0.0)
equity = cash + float(coin_qty) * price equity = cash + float(coin_qty) * price
cash_cap = max_buy_from_cash(equity, cash) effective_pct = resolve_buy_cash_pct(sizing_rules, cluster_size, buy_cash_pct)
per_buy = cash / cluster_size if cluster_size > 0 else cash cash_cap = max_buy_from_cash(equity, cash, base_pct=effective_pct)
order_krw = min(per_buy, cash, cash_cap) per_buy = cash_cap / cluster_size if cluster_size > 0 else cash_cap
order_krw = math.floor(min(per_buy, cash, cash_cap))
if order_krw < min_order_krw: if order_krw < min_order_krw:
return TradeResult( return TradeResult(
@@ -60,6 +88,7 @@ def compute_buy_order(
fee_krw=0.0, fee_krw=0.0,
price=price, price=price,
skip_reason="원화 부족 또는 최소 주문 미만", skip_reason="원화 부족 또는 최소 주문 미만",
expected_skip=True,
) )
fee = order_krw * fee_rate fee = order_krw * fee_rate
@@ -81,13 +110,29 @@ def compute_sell_order(
fee_rate: float, fee_rate: float,
min_order_krw: float, min_order_krw: float,
cluster_size: int = 1, cluster_size: int = 1,
sell_coin_pct: float | None = None,
sizing_rules: dict | None = None,
) -> TradeResult: ) -> TradeResult:
"""매도 주문 수량을 계산한다.""" """매도 주문 수량을 계산한다."""
qty = max(float(coin_qty), 0.0) qty = max(float(coin_qty), 0.0)
per_sell = qty / cluster_size if cluster_size > 0 else qty effective_pct = resolve_sell_coin_pct(sizing_rules, cluster_size, sell_coin_pct)
order_coin = per_sell sellable = max_sell_coin_qty(qty, sell_pct=effective_pct)
per_sell = sellable / cluster_size if cluster_size > 0 else sellable
order_coin = min(per_sell, qty)
order_krw = order_coin * price order_krw = order_coin * price
if qty <= 0:
return TradeResult(
executed=False,
side="sell",
order_krw=0.0,
order_coin=0.0,
fee_krw=0.0,
price=price,
skip_reason="보유 코인 없음",
expected_skip=True,
)
if order_coin <= 0 or order_krw < min_order_krw: if order_coin <= 0 or order_krw < min_order_krw:
return TradeResult( return TradeResult(
executed=False, executed=False,
@@ -96,7 +141,8 @@ def compute_sell_order(
order_coin=0.0, order_coin=0.0,
fee_krw=0.0, fee_krw=0.0,
price=price, price=price,
skip_reason="코인 부족 또는 최소 주문 미만", skip_reason="매도 가능 수량 부족(최소 주문 미만)",
expected_skip=True,
) )
fee = order_krw * fee_rate fee = order_krw * fee_rate

View File

@@ -1,13 +1,13 @@
"""2단계: Ground Truth 정합 매매 기법.""" """2단계: Ground Truth 정합 매매 기법."""
from deepcoin.techniques.registry import ( from bithumb.techniques.registry import (
get_all_techniques, get_all_techniques,
get_composite_techniques, get_composite_techniques,
get_single_techniques, get_single_techniques,
list_technique_ids, list_technique_ids,
techniques_by_category, techniques_by_category,
) )
from deepcoin.techniques.runner import run_all_techniques, run_technique from bithumb.techniques.runner import run_all_techniques, run_technique
__all__ = [ __all__ = [
"get_all_techniques", "get_all_techniques",

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.helpers import make_signal from bithumb.techniques.helpers import make_signal
from deepcoin.techniques.indicators import adx from bithumb.techniques.indicators import adx
class AdxTrendTechnique(BaseTechnique): class AdxTrendTechnique(BaseTechnique):

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.helpers import make_signal from bithumb.techniques.helpers import make_signal
from deepcoin.techniques.indicators import atr, ema from bithumb.techniques.indicators import atr, ema
class AtrChannelTechnique(BaseTechnique): class AtrChannelTechnique(BaseTechnique):

View File

@@ -4,8 +4,8 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.indicators import bollinger_bands, ema from bithumb.techniques.indicators import bollinger_bands, ema
class BbReversalTechnique(BaseTechnique): class BbReversalTechnique(BaseTechnique):

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.helpers import make_signal from bithumb.techniques.helpers import make_signal
from deepcoin.techniques.indicators import bollinger_bands from bithumb.techniques.indicators import bollinger_bands
class BbSqueezeBreakoutTechnique(BaseTechnique): class BbSqueezeBreakoutTechnique(BaseTechnique):

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.helpers import make_signal, safe_float from bithumb.techniques.helpers import make_signal, safe_float
from deepcoin.techniques.indicators import cci from bithumb.techniques.indicators import cci
class CciExtremeTechnique(BaseTechnique): class CciExtremeTechnique(BaseTechnique):

View File

@@ -6,8 +6,8 @@ from dataclasses import dataclass
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.indicators import ema from bithumb.techniques.indicators import ema
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@@ -4,18 +4,18 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.bb_squeeze_breakout import BbSqueezeBreakoutTechnique from bithumb.techniques.bb_squeeze_breakout import BbSqueezeBreakoutTechnique
from deepcoin.techniques.composite_base import ( from bithumb.techniques.composite_base import (
cluster_events, cluster_events,
collect_weighted_events, collect_weighted_events,
score_clusters_to_signals, score_clusters_to_signals,
) )
from deepcoin.techniques.donchian import DonchianTechnique from bithumb.techniques.donchian import DonchianTechnique
from deepcoin.techniques.keltner_breakout import KeltnerBreakoutTechnique from bithumb.techniques.keltner_breakout import KeltnerBreakoutTechnique
from deepcoin.techniques.macd_cross import MacdCrossTechnique from bithumb.techniques.macd_cross import MacdCrossTechnique
from deepcoin.techniques.range_breakout import RangeBreakoutTechnique from bithumb.techniques.range_breakout import RangeBreakoutTechnique
from deepcoin.techniques.volume_breakout import VolumeBreakoutTechnique from bithumb.techniques.volume_breakout import VolumeBreakoutTechnique
_SUB = [ _SUB = [
DonchianTechnique(), DonchianTechnique(),

View File

@@ -4,17 +4,17 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.composite_base import ( from bithumb.techniques.composite_base import (
cluster_events, cluster_events,
collect_weighted_events, collect_weighted_events,
score_clusters_to_signals, score_clusters_to_signals,
) )
from deepcoin.techniques.macd_cross import MacdCrossTechnique from bithumb.techniques.macd_cross import MacdCrossTechnique
from deepcoin.techniques.macd_divergence import MacdDivergenceTechnique from bithumb.techniques.macd_divergence import MacdDivergenceTechnique
from deepcoin.techniques.obv_divergence import ObvDivergenceTechnique from bithumb.techniques.obv_divergence import ObvDivergenceTechnique
from deepcoin.techniques.rsi_divergence import RsiDivergenceTechnique from bithumb.techniques.rsi_divergence import RsiDivergenceTechnique
from deepcoin.techniques.rsi_swing import RsiSwingTechnique from bithumb.techniques.rsi_swing import RsiSwingTechnique
_SUB = [ _SUB = [
RsiDivergenceTechnique(), RsiDivergenceTechnique(),

View File

@@ -4,15 +4,15 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.composite_base import ( from bithumb.techniques.composite_base import (
cluster_events, cluster_events,
collect_weighted_events, collect_weighted_events,
score_clusters_to_signals, score_clusters_to_signals,
) )
def _build_full_sub_techniques() -> list[BaseTechnique]: def _build_full_sub_techniques() -> list[BaseTechnique]:
"""복합 제외 단일 기법 목록을 반환한다.""" """복합 제외 단일 기법 목록을 반환한다."""
from deepcoin.techniques.registry import get_single_techniques from bithumb.techniques.registry import get_single_techniques
return get_single_techniques() return get_single_techniques()

View File

@@ -4,18 +4,18 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.bb_reversal import BbReversalTechnique from bithumb.techniques.bb_reversal import BbReversalTechnique
from deepcoin.techniques.composite_base import ( from bithumb.techniques.composite_base import (
cluster_events, cluster_events,
collect_weighted_events, collect_weighted_events,
score_clusters_to_signals, score_clusters_to_signals,
) )
from deepcoin.techniques.ema_pullback import EmaPullbackTechnique from bithumb.techniques.ema_pullback import EmaPullbackTechnique
from deepcoin.techniques.fib_pullback import FibPullbackTechnique from bithumb.techniques.fib_pullback import FibPullbackTechnique
from deepcoin.techniques.keltner_reversal import KeltnerReversalTechnique from bithumb.techniques.keltner_reversal import KeltnerReversalTechnique
from deepcoin.techniques.local_extrema import LocalExtremaTechnique from bithumb.techniques.local_extrema import LocalExtremaTechnique
from deepcoin.techniques.support_bounce import SupportBounceTechnique from bithumb.techniques.support_bounce import SupportBounceTechnique
_SUB = [ _SUB = [
EmaPullbackTechnique(), EmaPullbackTechnique(),

View File

@@ -4,19 +4,19 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.composite_base import ( from bithumb.techniques.composite_base import (
cluster_events, cluster_events,
collect_weighted_events, collect_weighted_events,
score_clusters_to_signals, score_clusters_to_signals,
) )
from deepcoin.techniques.donchian import DonchianTechnique from bithumb.techniques.donchian import DonchianTechnique
from deepcoin.techniques.fractal_swing import FractalSwingTechnique from bithumb.techniques.fractal_swing import FractalSwingTechnique
from deepcoin.techniques.local_extrema import LocalExtremaTechnique from bithumb.techniques.local_extrema import LocalExtremaTechnique
from deepcoin.techniques.minor_swing import MinorSwingTechnique from bithumb.techniques.minor_swing import MinorSwingTechnique
from deepcoin.techniques.pivot_swing import PivotSwingTechnique from bithumb.techniques.pivot_swing import PivotSwingTechnique
from deepcoin.techniques.swing_failure import SwingFailureTechnique from bithumb.techniques.swing_failure import SwingFailureTechnique
from deepcoin.techniques.zigzag_causal import ZigzagCausalTechnique from bithumb.techniques.zigzag_causal import ZigzagCausalTechnique
_SUB = [ _SUB = [
ZigzagCausalTechnique(), ZigzagCausalTechnique(),

View File

@@ -4,29 +4,29 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.bb_reversal import BbReversalTechnique from bithumb.techniques.bb_reversal import BbReversalTechnique
from deepcoin.techniques.composite_base import ( from bithumb.techniques.composite_base import (
cluster_events, cluster_events,
collect_weighted_events, collect_weighted_events,
score_clusters_to_signals, score_clusters_to_signals,
) )
from deepcoin.techniques.donchian import DonchianTechnique from bithumb.techniques.donchian import DonchianTechnique
from deepcoin.techniques.ema_pullback import EmaPullbackTechnique from bithumb.techniques.ema_pullback import EmaPullbackTechnique
from deepcoin.techniques.fib_pullback import FibPullbackTechnique from bithumb.techniques.fib_pullback import FibPullbackTechnique
from deepcoin.techniques.fractal_swing import FractalSwingTechnique from bithumb.techniques.fractal_swing import FractalSwingTechnique
from deepcoin.techniques.keltner_breakout import KeltnerBreakoutTechnique from bithumb.techniques.keltner_breakout import KeltnerBreakoutTechnique
from deepcoin.techniques.local_extrema import LocalExtremaTechnique from bithumb.techniques.local_extrema import LocalExtremaTechnique
from deepcoin.techniques.macd_cross import MacdCrossTechnique from bithumb.techniques.macd_cross import MacdCrossTechnique
from deepcoin.techniques.macd_divergence import MacdDivergenceTechnique from bithumb.techniques.macd_divergence import MacdDivergenceTechnique
from deepcoin.techniques.minor_swing import MinorSwingTechnique from bithumb.techniques.minor_swing import MinorSwingTechnique
from deepcoin.techniques.obv_divergence import ObvDivergenceTechnique from bithumb.techniques.obv_divergence import ObvDivergenceTechnique
from deepcoin.techniques.pivot_swing import PivotSwingTechnique from bithumb.techniques.pivot_swing import PivotSwingTechnique
from deepcoin.techniques.range_breakout import RangeBreakoutTechnique from bithumb.techniques.range_breakout import RangeBreakoutTechnique
from deepcoin.techniques.rsi_divergence import RsiDivergenceTechnique from bithumb.techniques.rsi_divergence import RsiDivergenceTechnique
from deepcoin.techniques.rsi_swing import RsiSwingTechnique from bithumb.techniques.rsi_swing import RsiSwingTechnique
from deepcoin.techniques.support_bounce import SupportBounceTechnique from bithumb.techniques.support_bounce import SupportBounceTechnique
from deepcoin.techniques.zigzag_causal import ZigzagCausalTechnique from bithumb.techniques.zigzag_causal import ZigzagCausalTechnique
_TECHNIQUE_WEIGHTS: dict[str, tuple[float, float]] = { _TECHNIQUE_WEIGHTS: dict[str, tuple[float, float]] = {
"zigzag_causal": (2.5, 2.5), "zigzag_causal": (2.5, 2.5),

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
class DonchianTechnique(BaseTechnique): class DonchianTechnique(BaseTechnique):

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.helpers import make_signal from bithumb.techniques.helpers import make_signal
from deepcoin.techniques.indicators import ema from bithumb.techniques.indicators import ema
class EmaPullbackTechnique(BaseTechnique): class EmaPullbackTechnique(BaseTechnique):

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.helpers import find_confirmed_pivots, make_signal from bithumb.techniques.helpers import find_confirmed_pivots, make_signal
from deepcoin.techniques.indicators import ema from bithumb.techniques.indicators import ema
class FibPullbackTechnique(BaseTechnique): class FibPullbackTechnique(BaseTechnique):

View File

@@ -4,8 +4,8 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.helpers import dedupe_signals, find_fractal_pivots, make_signal from bithumb.techniques.helpers import dedupe_signals, find_fractal_pivots, make_signal
class FractalSwingTechnique(BaseTechnique): class FractalSwingTechnique(BaseTechnique):

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import TechniqueSignal from bithumb.techniques.base import TechniqueSignal
def safe_float(value: object) -> float | None: def safe_float(value: object) -> float | None:

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.helpers import make_signal from bithumb.techniques.helpers import make_signal
from deepcoin.techniques.indicators import ichimoku from bithumb.techniques.indicators import ichimoku
class IchimokuTrendTechnique(BaseTechnique): class IchimokuTrendTechnique(BaseTechnique):

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.helpers import make_signal from bithumb.techniques.helpers import make_signal
from deepcoin.techniques.indicators import keltner_channels from bithumb.techniques.indicators import keltner_channels
class KeltnerBreakoutTechnique(BaseTechnique): class KeltnerBreakoutTechnique(BaseTechnique):

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
import pandas as pd import pandas as pd
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
from deepcoin.techniques.helpers import make_signal from bithumb.techniques.helpers import make_signal
from deepcoin.techniques.indicators import keltner_channels from bithumb.techniques.indicators import keltner_channels
class KeltnerReversalTechnique(BaseTechnique): class KeltnerReversalTechnique(BaseTechnique):

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from deepcoin.techniques.base import TechniqueSignal from bithumb.techniques.base import TechniqueSignal
def signals_to_legs( def signals_to_legs(

Some files were not shown because too many files have changed in this diff Show More