refactor: 프로젝트명 bithumb으로 변경 및 futures 파이프라인 제거
deepcoin 패키지를 bithumb으로 rename하고, 3단계 live 운영·사이징 튜닝·텔레그램 알림을 통합한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
23
.env.example
23
.env.example
@@ -1,4 +1,4 @@
|
||||
# DeepCoin — .env.example (비밀값 없음). 복사: cp .env.example .env
|
||||
# Bithumb — .env.example (비밀값 없음). 복사: cp .env.example .env
|
||||
|
||||
# --- 빗썸 API (캔들 수집은 Public API, 키 선택) ---
|
||||
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_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 ---
|
||||
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
|
||||
@@ -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
|
||||
|
||||
# --- 현물 3단계: 운영 (기본 paper) ---
|
||||
# live 전환: OPS_MODE=live → bash scripts/3_run_fractal_live.sh
|
||||
OPS_MODE=paper
|
||||
OPS_TECHNIQUE_ID=fractal_swing
|
||||
OPS_MTF_ENABLED=false
|
||||
OPS_TREND_GATE_ENABLED=false
|
||||
OPS_DAILY_MAX_TRADES=100
|
||||
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
|
||||
# 빗썸 시장가 매수 시 주문금액+예약수수료 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_SYNC_CANDLES=true
|
||||
# 비우면 DOWNLOAD_INTERVALS 전체 증분 sync
|
||||
@@ -94,9 +94,10 @@ OPS_SIGNAL_TAIL_BARS=800
|
||||
OPS_STATE_JSON=data/spot/operations/fractal_ops_state.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
|
||||
# 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
|
||||
|
||||
# 폴더 구조: data|docs / {common, spot, futures}
|
||||
# 폴더 구조: data|docs / {common, spot}
|
||||
# common — coins.db 등 공유 리소스
|
||||
# spot — 현물 GT·기법·분석·운영
|
||||
# futures — 선물 GT·분석·운영
|
||||
|
||||
45
README.md
45
README.md
@@ -1,15 +1,15 @@
|
||||
# DeepCoin
|
||||
# Bithumb
|
||||
|
||||
빗썸 KRW 마켓 암호화폐 캔들 수집 및 **현물**·**선물** 매매 전략 파이프라인.
|
||||
빗썸 KRW 마켓 암호화폐 캔들 수집 및 **현물** 매매 전략 파이프라인.
|
||||
|
||||
- **기본 축:** 3분봉 현물 BTC, 최근 **10년** 캔들 (`DOWNLOAD_DAYS=3650`)
|
||||
- **데이터·문서 분류:** `common` (공유) · `spot` (현물) · `futures` (선물)
|
||||
- **데이터·문서 분류:** `common` (공유) · `spot` (현물)
|
||||
- **현재 운영 전략:** `fractal_swing` + MTF off — paper/live tick 운영 구현 완료
|
||||
|
||||
## 주요 기능
|
||||
|
||||
- 빗썸 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)**
|
||||
- 운영 tick: 캔들 증분 sync, 신호 tail 갱신, 슬리피지·일 체결 상한, 텔레그램 체결 알림
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
## 설치
|
||||
|
||||
```bash
|
||||
cd DeepCoin
|
||||
cd Bithumb
|
||||
conda activate ncue # 또는 xavis
|
||||
pip install -r requirements.txt
|
||||
cp .env.example .env # API 키·텔레그램 등 로컬 설정
|
||||
@@ -56,7 +56,6 @@ OPS_DAILY_MAX_TRADES=100
|
||||
| **spot 1단계** | GT 타점 완벽 추종 sim 상한선 | GT 자체가 사후 | 불가 |
|
||||
| **spot 2단계** | 39종 인과 기법 평가·MTF 규칙 | 미사용 | 불가 |
|
||||
| **spot 3단계** | paper/live tick 운영 | 미사용 | **가능** |
|
||||
| **futures 0단계** | 현물 GT → 선물 롱·숏 마커 | — | — |
|
||||
|
||||
### 현물 3단계 운영 아키텍처 (fractal_swing)
|
||||
|
||||
@@ -93,8 +92,8 @@ flowchart TD
|
||||
## 폴더 구조
|
||||
|
||||
```text
|
||||
DeepCoin/
|
||||
├── src/deepcoin/
|
||||
Bithumb/
|
||||
├── src/bithumb/
|
||||
│ ├── api/ # 빗썸 Public·Private REST
|
||||
│ ├── data/ # 캔들 수집·DB·로더
|
||||
│ ├── ground_truth/ # GT 타점·sim·차트
|
||||
@@ -112,16 +111,14 @@ DeepCoin/
|
||||
│ │ ├── techniques/ # 2단계 기법 결과 (fractal_swing.json 등)
|
||||
│ │ ├── mtf/ # mtf_rules_v3.json
|
||||
│ │ └── operations/ # fractal_ops_state.json
|
||||
│ └── futures/ground_truth/ # 선물 GT JSON
|
||||
│
|
||||
└── docs/
|
||||
├── live/ # 운영 백테스트 매매 차트 (index.html)
|
||||
├── spot/
|
||||
│ ├── 0_ground_truth/ # GT 차트 HTML
|
||||
│ ├── 1_simulation/ # 1단계 sim 차트
|
||||
│ ├── 2_analysis/ # 2단계 리포트·설계 가이드
|
||||
│ └── 3_operations/ # 운영·백테스트 JSON 리포트
|
||||
└── futures/0_ground_truth/ # 선물 GT 차트
|
||||
└── spot/
|
||||
├── 0_ground_truth/ # GT 차트 HTML
|
||||
├── 1_simulation/ # 1단계 sim 차트
|
||||
├── 2_analysis/ # 2단계 리포트·설계 가이드
|
||||
└── 3_operations/ # 운영·백테스트 JSON 리포트
|
||||
```
|
||||
|
||||
테이블명: `{SYMBOL}_{인터벌분}` (예: `BTC_3`, `BTC_1440`). 인터벌: 분봉=분 숫자, 일=`1440`, 주=`10080`, 월=`43200`.
|
||||
@@ -136,7 +133,6 @@ flowchart LR
|
||||
B --> C[1_ground_truth_sim]
|
||||
C --> D[2_run_stage2_all]
|
||||
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/` |
|
||||
| 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/` |
|
||||
| — | 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
|
||||
bash scripts/2_run_stage2_all.sh
|
||||
|
||||
# futures 0단계
|
||||
python scripts/0_ground_truth_futures.py --tier all
|
||||
|
||||
# spot 3단계 — fractal_swing 운영
|
||||
python scripts/3_run_filtered_backtest.py # 운영 조건 3년 sim 검증
|
||||
python scripts/3_render_live_chart.py # docs/live 매매 차트
|
||||
@@ -274,12 +266,6 @@ OPS_TREND_GATE_ENABLED=true
|
||||
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종)
|
||||
|
||||
`src/deepcoin/techniques/` — 단일 33 + 복합 6, 미래 데이터 미사용.
|
||||
`src/bithumb/techniques/` — 단일 33 + 복합 6, 미래 데이터 미사용.
|
||||
|
||||
| ID | 기법 | 유형 |
|
||||
|----|------|------|
|
||||
@@ -412,18 +398,17 @@ OPS_DAILY_MAX_TRADES=20
|
||||
| common | 캔들 수집·증분 sync | 구현됨 |
|
||||
| spot | 0~2단계 (GT·기법·MTF) | 구현됨 |
|
||||
| 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: `docs/live/` 운영 백테스트 매매 차트 (`3_render_live_chart.py`)
|
||||
- 2026-06-13: fractal_swing live 운영 — 슬리피지·일 체결 상한·전 TF 증분 sync·신호 tail 갱신
|
||||
- 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: 2단계 인과 기법 분석 파이프라인 완료
|
||||
- 2026-06-08: Ground Truth v1/v2/v3
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>DeepCoin Live — 운영 백테스트 차트</title>
|
||||
<title>Bithumb Live — 운영 백테스트 차트</title>
|
||||
<style>
|
||||
body { font-family: "Malgun Gothic", Arial, sans-serif; margin: 32px; color: #333; background: #f5f5f5; }
|
||||
h1 { font-size: 22px; margin-bottom: 8px; }
|
||||
@@ -15,23 +15,23 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DeepCoin Live — 운영 백테스트</h1>
|
||||
<h1>Bithumb Live — 운영 백테스트</h1>
|
||||
<p class="meta">
|
||||
BTC · 프랙탈 스윙 (fractal_swing)<br>
|
||||
sim 기간: 최근 1095일 ·
|
||||
슬리피지 0.05% ·
|
||||
일 체결 상한 100 ·
|
||||
MTF off
|
||||
MTF off<br>학습 비율: 매수 100% · 매도 100% (클러스터별 규칙 적용)
|
||||
</p>
|
||||
<div class="card">
|
||||
<div>3년 수익률 (운영 규칙 sim)</div>
|
||||
<div class="stat">+1874019.75%</div>
|
||||
<div class="stat">+1885460.27%</div>
|
||||
<p>
|
||||
<a href="fractal_swing_ops_chart.html">매수·매도 타점 차트 열기</a>
|
||||
</p>
|
||||
<ul>
|
||||
<li>매수 53,521 / 매도 53,449 체결</li>
|
||||
<li>초기 200,000원 → 최종 3,748,239,499원</li>
|
||||
<li>매수 53,519 / 매도 53,444 체결</li>
|
||||
<li>초기 200,000원 → 최종 3,771,120,549원</li>
|
||||
<li>차트: B=매수 S=매도 마커, 이전/다음 타점 탐색, 기간 줌</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 현물 2단계 설계 가이드
|
||||
|
||||
> DeepCoin 현물 파이프라인 2단계(인과 기법 분석)의 목적, 구조, 설계 근거를 정리한 문서입니다.
|
||||
> Bithumb 현물 파이프라인 2단계(인과 기법 분석)의 목적, 구조, 설계 근거를 정리한 문서입니다.
|
||||
> 작성 기준: 2026-06-12 · 기본 TF: 3분봉 · GT: v3
|
||||
|
||||
---
|
||||
@@ -57,7 +57,7 @@
|
||||
### 파이프라인 실행
|
||||
|
||||
```bash
|
||||
cd DeepCoin
|
||||
cd Bithumb
|
||||
export PYTHONPATH=src
|
||||
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)
|
||||
|
||||
**TF별 피처** (`src/deepcoin/mtf/features.py`)
|
||||
**TF별 피처** (`src/bithumb/mtf/features.py`)
|
||||
|
||||
| 피처 | 용도 |
|
||||
|------|------|
|
||||
@@ -224,7 +224,7 @@ flowchart TD
|
||||
- 주봉 하락 추세 3분 돌파 매수 → 가짜 돌파
|
||||
- 월봉 과매도 3분 매도 → 바닥 청산
|
||||
|
||||
등의 문제가 발생합니다. DeepCoin은 이를 **MTF 레이어**로 보완합니다.
|
||||
등의 문제가 발생합니다. Bithumb은 이를 **MTF 레이어**로 보완합니다.
|
||||
|
||||
| 상황 | MTF 해석 | 의도 |
|
||||
|------|----------|------|
|
||||
@@ -299,12 +299,12 @@ flowchart TD
|
||||
|
||||
| 모듈 | 경로 |
|
||||
|------|------|
|
||||
| 기법 실행 | `scripts/2_run_techniques.py`, `src/deepcoin/techniques/runner.py` |
|
||||
| GT 정합 | `src/deepcoin/evaluation/gt_align.py` |
|
||||
| MTF 피처 | `src/deepcoin/mtf/features.py`, `extractor.py`, `store.py` |
|
||||
| MTF 규칙 | `src/deepcoin/mtf/rules.py` |
|
||||
| MTF 필터 | `src/deepcoin/mtf/filter.py`, `trend_gate.py` |
|
||||
| 통합 기법 | `src/deepcoin/techniques/composite_v3.py` |
|
||||
| 기법 실행 | `scripts/2_run_techniques.py`, `src/bithumb/techniques/runner.py` |
|
||||
| GT 정합 | `src/bithumb/evaluation/gt_align.py` |
|
||||
| MTF 피처 | `src/bithumb/mtf/features.py`, `extractor.py`, `store.py` |
|
||||
| MTF 규칙 | `src/bithumb/mtf/rules.py` |
|
||||
| MTF 필터 | `src/bithumb/mtf/filter.py`, `trend_gate.py` |
|
||||
| 통합 기법 | `src/bithumb/techniques/composite_v3.py` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 현물 2단계 최종 정리 — 결과 해석 및 운영 권고
|
||||
|
||||
> DeepCoin 현물 파이프라인 2단계(인과 기법 분석) 완료 후 종합 정리 문서
|
||||
> Bithumb 현물 파이프라인 2단계(인과 기법 분석) 완료 후 종합 정리 문서
|
||||
> 작성 기준: 2026-06-12 · 데이터: BTC · 3분봉 · GT v3 · 분석 기간 3650일 · sim 기간 최근 3년(1095일)
|
||||
|
||||
---
|
||||
|
||||
@@ -47,7 +47,7 @@ paper / live 체결 (구간별 매수 상한 동일)
|
||||
| 일괄 | `3_run_stage3_all.sh` | 3-1 + 3-2 paper |
|
||||
|
||||
```bash
|
||||
cd DeepCoin
|
||||
cd Bithumb
|
||||
export PYTHONPATH=src
|
||||
|
||||
# MTF 필터 백테스트
|
||||
@@ -87,12 +87,12 @@ bash scripts/3_run_stage3_all.sh
|
||||
|
||||
| 모듈 | 경로 |
|
||||
|------|------|
|
||||
| 신호 파이프라인 | `src/deepcoin/operations/signal_pipeline.py` |
|
||||
| signal_type 추론 | `src/deepcoin/operations/signal_type.py` |
|
||||
| 체결 엔진 | `src/deepcoin/operations/trade_engine.py` |
|
||||
| paper/live | `src/deepcoin/operations/executor.py` |
|
||||
| 러너 | `src/deepcoin/operations/runner.py` |
|
||||
| 빗썸 Private | `src/deepcoin/api/bithumb_private.py` |
|
||||
| 신호 파이프라인 | `src/bithumb/operations/signal_pipeline.py` |
|
||||
| signal_type 추론 | `src/bithumb/operations/signal_type.py` |
|
||||
| 체결 엔진 | `src/bithumb/operations/trade_engine.py` |
|
||||
| paper/live | `src/bithumb/operations/executor.py` |
|
||||
| 러너 | `src/bithumb/operations/runner.py` |
|
||||
| 빗썸 Private | `src/bithumb/api/bithumb_private.py` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ if str(SRC) not in sys.path:
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.data.candle_store import CandleStore
|
||||
from deepcoin.data.downloader import CandleDownloader
|
||||
from deepcoin.data.intervals import INTERVAL_1MIN, estimate_download_requests, interval_label
|
||||
from bithumb.config import load_settings
|
||||
from bithumb.data.candle_store import CandleStore
|
||||
from bithumb.data.downloader import CandleDownloader
|
||||
from bithumb.data.intervals import INTERVAL_1MIN, estimate_download_requests, interval_label
|
||||
|
||||
|
||||
def _configure_logging(verbose: bool) -> None:
|
||||
|
||||
@@ -15,10 +15,10 @@ 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.data.intervals import interval_label
|
||||
from deepcoin.ground_truth.chart import render_ground_truth_chart
|
||||
from deepcoin.ground_truth.ground_truth import GtParams, build_ground_truth, save_ground_truth
|
||||
from bithumb.config import Settings, load_settings
|
||||
from bithumb.data.intervals import interval_label
|
||||
from bithumb.ground_truth.chart import render_ground_truth_chart
|
||||
from bithumb.ground_truth.ground_truth import GtParams, build_ground_truth, save_ground_truth
|
||||
|
||||
TIER_DESCRIPTIONS = {
|
||||
"v1": "스윙만 (최소 매수·매도)",
|
||||
|
||||
@@ -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())
|
||||
@@ -16,12 +16,12 @@ 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.data.candle_loader import load_candles
|
||||
from deepcoin.data.intervals import interval_label
|
||||
from deepcoin.ground_truth.chart import render_ground_truth_sim_chart
|
||||
from deepcoin.ground_truth.ground_truth import GtParams, build_ground_truth, save_ground_truth
|
||||
from deepcoin.ground_truth.pnl import simulate_gt_signals_pnl
|
||||
from bithumb.config import Settings, load_settings
|
||||
from bithumb.data.candle_loader import load_candles
|
||||
from bithumb.data.intervals import interval_label
|
||||
from bithumb.ground_truth.chart import render_ground_truth_sim_chart
|
||||
from bithumb.ground_truth.ground_truth import GtParams, build_ground_truth, save_ground_truth
|
||||
from bithumb.ground_truth.pnl import simulate_gt_signals_pnl
|
||||
|
||||
TIER_DESCRIPTIONS = {
|
||||
"v1": "스윙만 (최소 매수·매도)",
|
||||
|
||||
96
scripts/1_tune_order_sizing.py
Normal file
96
scripts/1_tune_order_sizing.py
Normal 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())
|
||||
@@ -14,15 +14,15 @@ SRC = ROOT / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.data.candle_loader import load_candles
|
||||
from deepcoin.evaluation.causal_sim import (
|
||||
from bithumb.config import load_settings
|
||||
from bithumb.data.candle_loader import load_candles
|
||||
from bithumb.evaluation.causal_sim import (
|
||||
best_technique_chart_path,
|
||||
pick_best_technique_row,
|
||||
render_best_technique_comparison_chart,
|
||||
run_technique_causal_sim,
|
||||
)
|
||||
from deepcoin.techniques.runner import load_ground_truth, load_technique_result
|
||||
from bithumb.techniques.runner import load_ground_truth, load_technique_result
|
||||
|
||||
|
||||
def _configure_logging(verbose: bool) -> None:
|
||||
|
||||
@@ -14,10 +14,10 @@ SRC = ROOT / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.data.candle_loader import load_candles
|
||||
from deepcoin.data.intervals import interval_label
|
||||
from deepcoin.evaluation.causal_sim import (
|
||||
from bithumb.config import load_settings
|
||||
from bithumb.data.candle_loader import load_candles
|
||||
from bithumb.data.intervals import interval_label
|
||||
from bithumb.evaluation.causal_sim import (
|
||||
best_technique_chart_path,
|
||||
build_causal_sim_report,
|
||||
pick_best_technique_row,
|
||||
@@ -28,7 +28,7 @@ from deepcoin.evaluation.causal_sim import (
|
||||
save_causal_sim_report,
|
||||
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:
|
||||
|
||||
@@ -13,16 +13,16 @@ SRC = ROOT / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.evaluation.mtf_report import (
|
||||
from bithumb.config import load_settings
|
||||
from bithumb.evaluation.mtf_report import (
|
||||
build_mtf_correlation_report,
|
||||
render_mtf_html,
|
||||
save_mtf_report,
|
||||
)
|
||||
from deepcoin.mtf.extractor import MtfFeatureExtractor
|
||||
from deepcoin.mtf.rules import derive_rules_from_report, save_mtf_rules
|
||||
from deepcoin.mtf.store import MultiTimeframeStore
|
||||
from deepcoin.techniques.runner import load_ground_truth
|
||||
from bithumb.mtf.extractor import MtfFeatureExtractor
|
||||
from bithumb.mtf.rules import derive_rules_from_report, save_mtf_rules
|
||||
from bithumb.mtf.store import MultiTimeframeStore
|
||||
from bithumb.techniques.runner import load_ground_truth
|
||||
|
||||
|
||||
def _configure_logging(verbose: bool) -> None:
|
||||
|
||||
@@ -13,21 +13,21 @@ SRC = ROOT / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.data.intervals import interval_label
|
||||
from deepcoin.evaluation.report import (
|
||||
from bithumb.config import load_settings
|
||||
from bithumb.data.intervals import interval_label
|
||||
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,
|
||||
render_signal_type_html,
|
||||
save_signal_type_report,
|
||||
)
|
||||
from deepcoin.techniques.base import TechniqueParams
|
||||
from deepcoin.techniques.registry import list_technique_ids
|
||||
from deepcoin.techniques.runner import (
|
||||
from bithumb.techniques.base import TechniqueParams
|
||||
from bithumb.techniques.registry import list_technique_ids
|
||||
from bithumb.techniques.runner import (
|
||||
load_ground_truth,
|
||||
load_technique_results,
|
||||
run_all_techniques,
|
||||
|
||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
export PYTHONPATH=src
|
||||
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"
|
||||
|
||||
|
||||
@@ -13,16 +13,16 @@ SRC = ROOT / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.data.intervals import interval_label
|
||||
from deepcoin.evaluation.report import (
|
||||
from bithumb.config import load_settings
|
||||
from bithumb.data.intervals import interval_label
|
||||
from bithumb.evaluation.report import (
|
||||
build_comparison_report,
|
||||
render_comparison_html,
|
||||
save_comparison_report,
|
||||
)
|
||||
from deepcoin.techniques.base import TechniqueParams
|
||||
from deepcoin.techniques.registry import list_technique_ids
|
||||
from deepcoin.techniques.runner import (
|
||||
from bithumb.techniques.base import TechniqueParams
|
||||
from bithumb.techniques.registry import list_technique_ids
|
||||
from bithumb.techniques.runner import (
|
||||
load_ground_truth,
|
||||
run_all_techniques,
|
||||
save_technique_result,
|
||||
|
||||
90
scripts/3_init_live_state.py
Executable file
90
scripts/3_init_live_state.py
Executable 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
70
scripts/3_preflight_live.py
Executable 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())
|
||||
@@ -13,8 +13,8 @@ SRC = ROOT / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.operations.chart import render_ops_live_chart
|
||||
from bithumb.config import load_settings
|
||||
from bithumb.operations.chart import render_ops_live_chart
|
||||
|
||||
|
||||
def _configure_logging(verbose: bool) -> None:
|
||||
@@ -34,12 +34,21 @@ def _write_index_html(
|
||||
"""docs/live 인덱스 HTML을 생성한다."""
|
||||
sim = report["filtered_sim"]
|
||||
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)
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>DeepCoin Live — 운영 백테스트 차트</title>
|
||||
<title>Bithumb Live — 운영 백테스트 차트</title>
|
||||
<style>
|
||||
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 32px; color: #333; background: #f5f5f5; }}
|
||||
h1 {{ font-size: 22px; margin-bottom: 8px; }}
|
||||
@@ -52,13 +61,13 @@ def _write_index_html(
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DeepCoin Live — 운영 백테스트</h1>
|
||||
<h1>Bithumb Live — 운영 백테스트</h1>
|
||||
<p class="meta">
|
||||
{report.get("symbol", "BTC")} · {report.get("technique_name", "")} ({report.get("technique_id", "")})<br>
|
||||
sim 기간: 최근 {report.get("sim_lookback_days", 1095)}일 ·
|
||||
슬리피지 {report.get("slippage_rate", 0) * 100:.2f}% ·
|
||||
일 체결 상한 {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>
|
||||
<div class="card">
|
||||
<div>3년 수익률 (운영 규칙 sim)</div>
|
||||
|
||||
@@ -13,8 +13,8 @@ SRC = ROOT / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.operations.backtest import run_filtered_backtest, save_backtest_report
|
||||
from bithumb.config import load_settings
|
||||
from bithumb.operations.backtest import run_filtered_backtest, save_backtest_report
|
||||
|
||||
|
||||
def _configure_logging(verbose: bool) -> None:
|
||||
|
||||
26
scripts/3_run_fractal_live.sh
Executable file
26
scripts/3_run_fractal_live.sh
Executable 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
|
||||
@@ -14,10 +14,10 @@ SRC = ROOT / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.evaluation.causal_sim import normalize_signals_for_sim
|
||||
from deepcoin.ground_truth.pnl import simulate_gt_signals_pnl
|
||||
from deepcoin.operations.signal_pipeline import (
|
||||
from bithumb.config import load_settings
|
||||
from bithumb.evaluation.causal_sim import normalize_signals_for_sim
|
||||
from bithumb.ground_truth.pnl import simulate_gt_signals_pnl
|
||||
from bithumb.operations.signal_pipeline import (
|
||||
_signals_in_lookback,
|
||||
generate_raw_signals,
|
||||
load_ops_candles,
|
||||
@@ -64,6 +64,8 @@ def main() -> int:
|
||||
min_order_krw=settings.ops_min_order_krw,
|
||||
slippage_rate=sc["slippage_rate"],
|
||||
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,
|
||||
data_end=end,
|
||||
last_mark_price=price,
|
||||
|
||||
@@ -14,8 +14,8 @@ SRC = ROOT / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.operations.runner import OperationsRunner
|
||||
from bithumb.config import load_settings
|
||||
from bithumb.operations.runner import OperationsRunner
|
||||
|
||||
|
||||
def _configure_logging(verbose: bool) -> None:
|
||||
@@ -29,7 +29,7 @@ def _configure_logging(verbose: bool) -> None:
|
||||
|
||||
def main() -> int:
|
||||
"""CLI 진입점."""
|
||||
parser = argparse.ArgumentParser(description="3단계: DeepCoin 운영 tick")
|
||||
parser = argparse.ArgumentParser(description="3단계: Bithumb 운영 tick")
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=("paper", "live"),
|
||||
@@ -67,19 +67,37 @@ def main() -> int:
|
||||
sync = not args.no_sync
|
||||
|
||||
while True:
|
||||
try:
|
||||
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(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(
|
||||
f"최신 봉 후보: {report.get('latest_bar_candidates', 0)} · "
|
||||
f"필터 통과: {report['filtered_signals']} · "
|
||||
f"필터 통과: {report.get('filtered_signals', 0)} · "
|
||||
f"처리 bar: {report.get('pending_bars', [])}"
|
||||
)
|
||||
print(f"이번 체결: {len(report['executions'])}건")
|
||||
print(f"이번 체결: {len(report.get('executions', []))}건")
|
||||
print(
|
||||
f"포트폴리오: 현금 {port['cash_krw']:,.0f}원 · "
|
||||
f"코인 {port['coin_qty']:.8f} {settings.symbol}"
|
||||
f"포트폴리오: 현금 {port.get('cash_krw', 0):,.0f}원 · "
|
||||
f"코인 {port.get('coin_qty', 0):.8f} {settings.symbol}"
|
||||
)
|
||||
print(f"리포트: {settings.ops_report_json}")
|
||||
|
||||
|
||||
3
src/bithumb/__init__.py
Normal file
3
src/bithumb/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Bithumb — 빗썸 암호화폐 데이터 수집·분석."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -9,7 +9,7 @@ from typing import Any
|
||||
|
||||
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__)
|
||||
|
||||
@@ -3,16 +3,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
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:
|
||||
"""빗썸 v2.1 Private API 클라이언트."""
|
||||
|
||||
@@ -72,6 +98,18 @@ class BithumbPrivateClient:
|
||||
logger.warning("Rate limit 429 — %ss 대기", wait)
|
||||
time.sleep(wait)
|
||||
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()
|
||||
payload = response.json()
|
||||
time.sleep(self.sleep_sec)
|
||||
@@ -116,10 +154,13 @@ class BithumbPrivateClient:
|
||||
Returns:
|
||||
주문 응답 dict.
|
||||
"""
|
||||
order_krw = max(math.floor(float(krw_amount)), 0)
|
||||
if order_krw <= 0:
|
||||
raise ValueError("매수 원화 금액이 0 이하입니다.")
|
||||
params = {
|
||||
"market": market,
|
||||
"side": "bid",
|
||||
"price": str(int(krw_amount)),
|
||||
"price": str(order_krw),
|
||||
"ord_type": "price",
|
||||
}
|
||||
return self._request("POST", "/v1/orders", params=params)
|
||||
@@ -8,7 +8,7 @@ from pathlib import Path
|
||||
|
||||
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]
|
||||
|
||||
@@ -37,7 +37,7 @@ def _parse_int_list(raw: str) -> list[int]:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Settings:
|
||||
"""DeepCoin 실행 설정."""
|
||||
"""Bithumb 실행 설정."""
|
||||
|
||||
symbol: str
|
||||
coin_name: str
|
||||
@@ -48,7 +48,7 @@ class Settings:
|
||||
db_path: Path
|
||||
request_sleep_sec: float
|
||||
request_retries: int
|
||||
# 0단계: GT 타점 (현물·선물 공통 파라미터)
|
||||
# 0단계: GT 타점 (현물)
|
||||
gt_interval_min: int
|
||||
gt_lookback_days: int
|
||||
gt_zigzag_reversal_pct: float
|
||||
@@ -68,12 +68,6 @@ class Settings:
|
||||
ground_truth_chart_v1_file: Path
|
||||
ground_truth_chart_v2_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
|
||||
ground_truth_chart_sim_v1_file: Path
|
||||
ground_truth_chart_sim_v2_file: Path
|
||||
@@ -107,6 +101,11 @@ class Settings:
|
||||
ops_daily_max_trades: int
|
||||
ops_min_order_krw: 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_sync_candles: bool
|
||||
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(
|
||||
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(
|
||||
os.getenv(
|
||||
"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_min_order_krw=float(os.getenv("OPS_MIN_ORDER_KRW", "5000")),
|
||||
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_sync_candles=os.getenv("OPS_SYNC_CANDLES", "true").strip().lower()
|
||||
in ("1", "true", "yes", "on"),
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.data.candle_store import CandleStore
|
||||
from bithumb.data.candle_store import CandleStore
|
||||
|
||||
|
||||
def load_candles(
|
||||
@@ -8,7 +8,7 @@ from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.api.bithumb import parse_kst_datetime
|
||||
from bithumb.api.bithumb import parse_kst_datetime
|
||||
|
||||
|
||||
class CandleStore:
|
||||
@@ -7,9 +7,9 @@ from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from deepcoin.api.bithumb import BithumbCandleClient, parse_kst_datetime
|
||||
from deepcoin.config import Settings
|
||||
from deepcoin.data.candle_store import CandleStore
|
||||
from bithumb.api.bithumb import BithumbCandleClient, parse_kst_datetime
|
||||
from bithumb.config import Settings
|
||||
from bithumb.data.candle_store import CandleStore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Ground Truth 정합 평가."""
|
||||
|
||||
from deepcoin.evaluation.gt_align import align_with_ground_truth
|
||||
from deepcoin.evaluation.mtf_report import (
|
||||
from bithumb.evaluation.gt_align import align_with_ground_truth
|
||||
from bithumb.evaluation.mtf_report import (
|
||||
build_mtf_correlation_report,
|
||||
render_mtf_html,
|
||||
save_mtf_report,
|
||||
)
|
||||
from deepcoin.evaluation.report import build_comparison_report, render_comparison_html, save_comparison_report
|
||||
from deepcoin.evaluation.signal_type_report import (
|
||||
from bithumb.evaluation.report import build_comparison_report, render_comparison_html, save_comparison_report
|
||||
from bithumb.evaluation.signal_type_report import (
|
||||
build_signal_type_report,
|
||||
render_signal_type_html,
|
||||
save_signal_type_report,
|
||||
@@ -7,9 +7,9 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from deepcoin.ground_truth.chart import render_ground_truth_sim_chart
|
||||
from deepcoin.ground_truth.pnl import simulate_gt_signals_pnl
|
||||
from deepcoin.techniques.base import TechniqueResult
|
||||
from bithumb.ground_truth.chart import render_ground_truth_sim_chart
|
||||
from bithumb.ground_truth.pnl import simulate_gt_signals_pnl
|
||||
from bithumb.techniques.base import TechniqueResult
|
||||
|
||||
|
||||
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">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>DeepCoin 2단계 — 인과 sim</title>
|
||||
<title>Bithumb 2단계 — 인과 sim</title>
|
||||
<style>
|
||||
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; color: #333; background: #f5f5f5; }}
|
||||
h1 {{ font-size: 20px; margin-bottom: 8px; }}
|
||||
@@ -247,7 +247,7 @@ def align_with_ground_truth(
|
||||
|
||||
tech_return = 0.0
|
||||
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(
|
||||
technique_legs,
|
||||
@@ -11,9 +11,9 @@ from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.evaluation.gt_align import SIGNAL_TYPE_LABELS, SIGNAL_TYPE_SIDE
|
||||
from deepcoin.mtf.extractor import MtfFeatureExtractor, MtfSnapshot
|
||||
from deepcoin.mtf.features import FEATURE_NAMES
|
||||
from bithumb.evaluation.gt_align import SIGNAL_TYPE_LABELS, SIGNAL_TYPE_SIDE
|
||||
from bithumb.mtf.extractor import MtfFeatureExtractor, MtfSnapshot
|
||||
from bithumb.mtf.features import FEATURE_NAMES
|
||||
|
||||
NUMERIC_FEATURES: tuple[str, ...] = (
|
||||
"close_vs_ema60_pct",
|
||||
@@ -7,7 +7,7 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from deepcoin.techniques.base import TechniqueResult
|
||||
from bithumb.techniques.base import TechniqueResult
|
||||
|
||||
|
||||
def build_comparison_report(
|
||||
@@ -94,7 +94,7 @@ def render_comparison_html(report: dict[str, Any], html_path: Path) -> Path:
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>DeepCoin 2단계 — 인과 GT 정합</title>
|
||||
<title>Bithumb 2단계 — 인과 GT 정합</title>
|
||||
<style>
|
||||
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; color: #333; background: #f5f5f5; }}
|
||||
h1 {{ font-size: 20px; margin-bottom: 8px; }}
|
||||
@@ -106,7 +106,7 @@ def render_comparison_html(report: dict[str, Any], html_path: Path) -> Path:
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DeepCoin 2단계 — 인과 기법 Ground Truth 정합</h1>
|
||||
<h1>Bithumb 2단계 — 인과 기법 Ground Truth 정합</h1>
|
||||
<div class="meta">
|
||||
생성: {report.get('generated_at', '')} |
|
||||
{report.get('symbol', '')} |
|
||||
@@ -7,12 +7,12 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from deepcoin.evaluation.gt_align import (
|
||||
from bithumb.evaluation.gt_align import (
|
||||
SIGNAL_TYPE_LABELS,
|
||||
SIGNAL_TYPE_PRIMARY_TECHNIQUES,
|
||||
summarize_signal_type_matrix,
|
||||
)
|
||||
from deepcoin.techniques.base import TechniqueResult
|
||||
from bithumb.techniques.base import TechniqueResult
|
||||
|
||||
|
||||
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">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>DeepCoin — v3 신호 유형별 GT 정합</title>
|
||||
<title>Bithumb — v3 신호 유형별 GT 정합</title>
|
||||
<style>
|
||||
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; color: #333; background: #f5f5f5; }}
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DeepCoin — v3 신호 유형별 Ground Truth 정합</h1>
|
||||
<h1>Bithumb — v3 신호 유형별 Ground Truth 정합</h1>
|
||||
<div class="meta">
|
||||
생성: {report.get('generated_at', '')} |
|
||||
{report.get('symbol', '')} |
|
||||
@@ -7,7 +7,7 @@ from typing import Protocol
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.ground_truth.zigzag import Pivot
|
||||
from bithumb.ground_truth.zigzag import Pivot
|
||||
|
||||
|
||||
class _LegLike(Protocol):
|
||||
@@ -9,7 +9,7 @@ from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.data.candle_loader import load_candles
|
||||
from bithumb.data.candle_loader import load_candles
|
||||
|
||||
# 0이면 제한 없이 전체 봉 표시
|
||||
DEFAULT_MAX_CANDLES = 0
|
||||
@@ -254,7 +254,7 @@ _HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<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" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script>
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
@@ -284,7 +284,7 @@ __EXTRA_STYLES__
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1 id="title">DeepCoin Chart</h1>
|
||||
<h1 id="title">Bithumb Chart</h1>
|
||||
<div class="meta" id="meta"></div>
|
||||
</header>
|
||||
__EXTRA_BODY__
|
||||
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.indicators import macd, rsi
|
||||
from bithumb.techniques.indicators import macd, rsi
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -10,13 +10,13 @@ from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.data.candle_loader import load_candles
|
||||
from deepcoin.data.intervals import interval_label
|
||||
from deepcoin.ground_truth.pnl import simulate_gt_pnl
|
||||
from deepcoin.ground_truth.breakout import find_breakout_buy_pivots
|
||||
from deepcoin.ground_truth.divergence import find_divergence_signals
|
||||
from deepcoin.ground_truth.pullback import find_pullback_buy_pivots
|
||||
from deepcoin.ground_truth.zigzag import Pivot, find_zigzag_pivots
|
||||
from bithumb.data.candle_loader import load_candles
|
||||
from bithumb.data.intervals import interval_label
|
||||
from bithumb.ground_truth.pnl import simulate_gt_pnl
|
||||
from bithumb.ground_truth.breakout import find_breakout_buy_pivots
|
||||
from bithumb.ground_truth.divergence import find_divergence_signals
|
||||
from bithumb.ground_truth.pullback import find_pullback_buy_pivots
|
||||
from bithumb.ground_truth.zigzag import Pivot, find_zigzag_pivots
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
122
src/bithumb/ground_truth/order_sizing.py
Normal file
122
src/bithumb/ground_truth/order_sizing.py
Normal 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},
|
||||
],
|
||||
}
|
||||
@@ -6,7 +6,14 @@ from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timedelta
|
||||
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:
|
||||
@@ -177,6 +184,9 @@ def simulate_gt_signals_pnl(
|
||||
min_order_krw: float = 5_000.0,
|
||||
slippage_rate: float = 0.0,
|
||||
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,
|
||||
data_end: str | None = None,
|
||||
last_mark_price: float | None = None,
|
||||
@@ -186,7 +196,8 @@ def simulate_gt_signals_pnl(
|
||||
- 시뮬 기간: data_end 기준 최근 sim_lookback_days
|
||||
- 연속 매수: 가용 원화를 매수 신호 수로 균등 분할
|
||||
- 총평가 1억↑ 현금 10% · 10억↑ 5% · 100억↑ 1% 상한
|
||||
- 연속 매도: 보유 코인을 매도 신호 수로 균등 분할 (상한 없음)
|
||||
- 1억 미만: buy_cash_pct(기본 10%) 분할 매수 · sell_coin_pct(기본 10%) 분할 매도
|
||||
- 연속 매도: 허용 수량을 매도 신호 수로 균등 분할
|
||||
- 원화 부족 시 매수 스킵, 코인 없으면 매도 스킵
|
||||
|
||||
Args:
|
||||
@@ -251,7 +262,12 @@ def simulate_gt_signals_pnl(
|
||||
for side, cluster in _cluster_signals(period_signals):
|
||||
cluster_size = len(cluster)
|
||||
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
|
||||
for sig in cluster:
|
||||
trade_id += 1
|
||||
@@ -282,7 +298,7 @@ def simulate_gt_signals_pnl(
|
||||
)
|
||||
continue
|
||||
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)
|
||||
|
||||
if order_krw < min_order_krw:
|
||||
@@ -335,7 +351,10 @@ def simulate_gt_signals_pnl(
|
||||
)
|
||||
)
|
||||
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
|
||||
for sig in cluster:
|
||||
trade_id += 1
|
||||
@@ -436,7 +455,15 @@ def simulate_gt_signals_pnl(
|
||||
"fee_rate": fee_rate,
|
||||
"slippage_rate": slippage_rate,
|
||||
"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,
|
||||
"period_from": start_str,
|
||||
"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_return_pct": 0.0,
|
||||
"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,
|
||||
"period_from": period_from,
|
||||
"period_to": period_to,
|
||||
@@ -6,7 +6,7 @@ import pandas as pd
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from deepcoin.ground_truth.zigzag import Pivot
|
||||
from bithumb.ground_truth.zigzag import Pivot
|
||||
|
||||
|
||||
class _LegLike(Protocol):
|
||||
97
src/bithumb/ground_truth/sizing_rules.py
Normal file
97
src/bithumb/ground_truth/sizing_rules.py
Normal 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 {},
|
||||
}
|
||||
273
src/bithumb/ground_truth/sizing_tune.py
Normal file
273
src/bithumb/ground_truth/sizing_tune.py
Normal 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
|
||||
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
||||
|
||||
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)
|
||||
23
src/bithumb/mtf/__init__.py
Normal file
23
src/bithumb/mtf/__init__.py
Normal 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",
|
||||
]
|
||||
@@ -7,9 +7,9 @@ from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.mtf.alignment import as_of_from_signal_bar, resolve_bar_index
|
||||
from deepcoin.mtf.features import snapshot_at_index
|
||||
from deepcoin.mtf.store import MTF_INTERVALS, MultiTimeframeStore
|
||||
from bithumb.mtf.alignment import as_of_from_signal_bar, resolve_bar_index
|
||||
from bithumb.mtf.features import snapshot_at_index
|
||||
from bithumb.mtf.store import MTF_INTERVALS, MultiTimeframeStore
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
|
||||
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, ...] = (
|
||||
"close",
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from deepcoin.mtf.trend_gate import HtfTrendGate
|
||||
from deepcoin.mtf.extractor import MtfFeatureExtractor, MtfSnapshot
|
||||
from deepcoin.mtf.rules import MtfRule, MtfRuleSet
|
||||
from bithumb.mtf.trend_gate import HtfTrendGate
|
||||
from bithumb.mtf.extractor import MtfFeatureExtractor, MtfSnapshot
|
||||
from bithumb.mtf.rules import MtfRule, MtfRuleSet
|
||||
|
||||
|
||||
def evaluate_rule(rule: MtfRule, snapshot: MtfSnapshot) -> bool | None:
|
||||
@@ -5,9 +5,9 @@ from __future__ import annotations
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.mtf.alignment import as_of_from_signal_bar
|
||||
from deepcoin.mtf.features import snapshot_at_index
|
||||
from deepcoin.mtf.store import MultiTimeframeStore
|
||||
from bithumb.mtf.alignment import as_of_from_signal_bar
|
||||
from bithumb.mtf.features import snapshot_at_index
|
||||
from bithumb.mtf.store import MultiTimeframeStore
|
||||
|
||||
|
||||
def _vectorized_tf_indices(
|
||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
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["<=", ">="]
|
||||
|
||||
@@ -6,9 +6,9 @@ from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.data.candle_loader import load_candles
|
||||
from deepcoin.data.intervals import DEFAULT_DOWNLOAD_INTERVALS, interval_label
|
||||
from deepcoin.mtf.features import compute_feature_frame
|
||||
from bithumb.data.candle_loader import load_candles
|
||||
from bithumb.data.intervals import DEFAULT_DOWNLOAD_INTERVALS, interval_label
|
||||
from bithumb.mtf.features import compute_feature_frame
|
||||
|
||||
MTF_INTERVALS: tuple[int, ...] = tuple(DEFAULT_DOWNLOAD_INTERVALS)
|
||||
|
||||
@@ -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"]
|
||||
@@ -112,7 +112,7 @@ class TelegramNotifier:
|
||||
equity = cash + coin_qty * price
|
||||
|
||||
lines = [
|
||||
f"[DeepCoin] {side_label} 체결 ({mode_label})",
|
||||
f"[Bithumb] {side_label} 체결 ({mode_label})",
|
||||
f"{coin_name} ({symbol}) | {technique_id}",
|
||||
f"시각: {datetime_str}",
|
||||
f"신호: {signal_type or side}",
|
||||
@@ -145,13 +145,36 @@ class TelegramNotifier:
|
||||
side_label = "매수" if side == "buy" else "매도"
|
||||
mode_label = "LIVE" if mode == "live" else "PAPER"
|
||||
text = (
|
||||
f"[DeepCoin] {side_label} 실패 ({mode_label})\n"
|
||||
f"[Bithumb] {side_label} 실패 ({mode_label})\n"
|
||||
f"{symbol} | {technique_id}\n"
|
||||
f"시각: {datetime_str}\n"
|
||||
f"사유: {reason}"
|
||||
)
|
||||
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:
|
||||
"""원화 금액 포맷."""
|
||||
12
src/bithumb/operations/__init__.py
Normal file
12
src/bithumb/operations/__init__.py
Normal 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",
|
||||
]
|
||||
@@ -7,10 +7,11 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from deepcoin.config import Settings
|
||||
from deepcoin.evaluation.causal_sim import normalize_signals_for_sim
|
||||
from deepcoin.ground_truth.pnl import simulate_gt_signals_pnl
|
||||
from deepcoin.operations.signal_pipeline import run_signal_pipeline
|
||||
from bithumb.config import Settings
|
||||
from bithumb.evaluation.causal_sim import normalize_signals_for_sim
|
||||
from bithumb.ground_truth.pnl import simulate_gt_signals_pnl
|
||||
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]:
|
||||
@@ -22,28 +23,25 @@ def run_filtered_backtest(settings: Settings) -> dict[str, Any]:
|
||||
)
|
||||
kept = pipeline["kept"]
|
||||
normalized = normalize_signals_for_sim(kept)
|
||||
sim = simulate_gt_signals_pnl(
|
||||
signals=normalized,
|
||||
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=pipeline["data_end"],
|
||||
last_mark_price=pipeline["last_price"],
|
||||
)
|
||||
sizing_rules = load_sizing_rules(settings.ops_sizing_rules_json)
|
||||
sim_kwargs = {
|
||||
"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,
|
||||
"buy_cash_pct": settings.ops_buy_cash_pct,
|
||||
"sell_coin_pct": settings.ops_sell_coin_pct,
|
||||
"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(
|
||||
signals=normalize_signals_for_sim(pipeline["scoped_raw_signals"]),
|
||||
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=pipeline["data_end"],
|
||||
last_mark_price=pipeline["last_price"],
|
||||
**sim_kwargs,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -5,9 +5,9 @@ from __future__ import annotations
|
||||
import logging
|
||||
from dataclasses import replace
|
||||
|
||||
from deepcoin.config import Settings
|
||||
from deepcoin.data.candle_store import CandleStore
|
||||
from deepcoin.data.downloader import CandleDownloader, DownloadResult
|
||||
from bithumb.config import Settings
|
||||
from bithumb.data.candle_store import CandleStore
|
||||
from bithumb.data.downloader import CandleDownloader, DownloadResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -5,10 +5,10 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from deepcoin.config import Settings
|
||||
from deepcoin.data.intervals import interval_label
|
||||
from deepcoin.ground_truth.chart import render_ground_truth_sim_chart
|
||||
from deepcoin.operations.backtest import run_filtered_backtest
|
||||
from bithumb.config import Settings
|
||||
from bithumb.data.intervals import interval_label
|
||||
from bithumb.ground_truth.chart import render_ground_truth_sim_chart
|
||||
from bithumb.operations.backtest import run_filtered_backtest
|
||||
|
||||
|
||||
def _ops_chart_shell(
|
||||
@@ -19,6 +19,12 @@ def _ops_chart_shell(
|
||||
"""운영 백테스트 차트용 GT 메타 셸을 구성한다."""
|
||||
slip_pct = settings.ops_slippage_rate * 100
|
||||
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 {
|
||||
"meta": {
|
||||
"symbol": settings.symbol,
|
||||
@@ -34,6 +40,7 @@ def _ops_chart_shell(
|
||||
"ops_note": (
|
||||
f"슬리피지 {slip_pct:.2f}% · 일 체결 상한 {daily_cap} · "
|
||||
f"MTF {'on' if pipeline.get('mtf_enabled') else 'off'}"
|
||||
f"{sizing_note}"
|
||||
),
|
||||
"data_from": sim_pnl.get("period_from"),
|
||||
"data_to": sim_pnl.get("period_to"),
|
||||
@@ -6,14 +6,16 @@ import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
from deepcoin.api.bithumb_private import BithumbPrivateClient
|
||||
from deepcoin.config import Settings
|
||||
from deepcoin.operations.execution import fill_price
|
||||
from deepcoin.operations.trade_engine import (
|
||||
from bithumb.api.bithumb_private import BithumbPrivateClient
|
||||
from bithumb.config import Settings
|
||||
from bithumb.ground_truth.sizing_rules import load_sizing_rules
|
||||
from bithumb.operations.execution import fill_price
|
||||
from bithumb.operations.trade_engine import (
|
||||
TradeResult,
|
||||
apply_trade_to_portfolio,
|
||||
compute_buy_order,
|
||||
compute_sell_order,
|
||||
spendable_cash_for_exchange_buy,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,6 +40,7 @@ class PaperExecutor(OrderExecutor):
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
self.sizing_rules = load_sizing_rules(settings.ops_sizing_rules_json)
|
||||
|
||||
def execute_signal(
|
||||
self,
|
||||
@@ -66,6 +69,8 @@ class PaperExecutor(OrderExecutor):
|
||||
fee_rate=fee_rate,
|
||||
min_order_krw=min_order,
|
||||
cluster_size=cluster_size,
|
||||
buy_cash_pct=self.settings.ops_buy_cash_pct,
|
||||
sizing_rules=self.sizing_rules,
|
||||
)
|
||||
else:
|
||||
trade = compute_sell_order(
|
||||
@@ -74,6 +79,8 @@ class PaperExecutor(OrderExecutor):
|
||||
fee_rate=fee_rate,
|
||||
min_order_krw=min_order,
|
||||
cluster_size=cluster_size,
|
||||
sell_coin_pct=self.settings.ops_sell_coin_pct,
|
||||
sizing_rules=self.sizing_rules,
|
||||
)
|
||||
|
||||
if trade.executed:
|
||||
@@ -87,6 +94,7 @@ class LiveExecutor(OrderExecutor):
|
||||
def __init__(self, settings: Settings, client: BithumbPrivateClient) -> None:
|
||||
self.settings = settings
|
||||
self.client = client
|
||||
self.sizing_rules = load_sizing_rules(settings.ops_sizing_rules_json)
|
||||
|
||||
def _sync_portfolio(self, portfolio: dict[str, Any]) -> None:
|
||||
"""거래소 잔고로 포트폴리오를 동기화한다."""
|
||||
@@ -116,27 +124,46 @@ class LiveExecutor(OrderExecutor):
|
||||
coin = float(portfolio.get("coin_qty", 0))
|
||||
|
||||
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(
|
||||
cash_krw=cash,
|
||||
cash_krw=spendable,
|
||||
coin_qty=coin,
|
||||
price=price,
|
||||
fee_rate=fee_rate,
|
||||
min_order_krw=min_order,
|
||||
cluster_size=cluster_size,
|
||||
buy_cash_pct=self.settings.ops_buy_cash_pct,
|
||||
sizing_rules=self.sizing_rules,
|
||||
)
|
||||
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
|
||||
try:
|
||||
resp = self.client.market_buy_krw(self.settings.market, trade.order_krw)
|
||||
trade.api_response = resp
|
||||
self._sync_portfolio(portfolio)
|
||||
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(
|
||||
executed=False,
|
||||
side="buy",
|
||||
order_krw=0.0,
|
||||
order_coin=0.0,
|
||||
order_krw=trade.order_krw,
|
||||
order_coin=trade.order_coin,
|
||||
fee_krw=0.0,
|
||||
price=price,
|
||||
skip_reason=str(exc),
|
||||
@@ -149,20 +176,33 @@ class LiveExecutor(OrderExecutor):
|
||||
fee_rate=fee_rate,
|
||||
min_order_krw=min_order,
|
||||
cluster_size=cluster_size,
|
||||
sell_coin_pct=self.settings.ops_sell_coin_pct,
|
||||
sizing_rules=self.sizing_rules,
|
||||
)
|
||||
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
|
||||
try:
|
||||
resp = self.client.market_sell_volume(self.settings.market, trade.order_coin)
|
||||
trade.api_response = resp
|
||||
self._sync_portfolio(portfolio)
|
||||
except Exception as exc:
|
||||
logger.exception("live sell failed")
|
||||
logger.exception(
|
||||
"live sell failed order_coin=%.8f",
|
||||
trade.order_coin,
|
||||
)
|
||||
return TradeResult(
|
||||
executed=False,
|
||||
side="sell",
|
||||
order_krw=0.0,
|
||||
order_coin=0.0,
|
||||
order_krw=trade.order_krw,
|
||||
order_coin=trade.order_coin,
|
||||
fee_krw=0.0,
|
||||
price=price,
|
||||
skip_reason=str(exc),
|
||||
253
src/bithumb/operations/live_bootstrap.py
Normal file
253
src/bithumb/operations/live_bootstrap.py
Normal 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):,}원"
|
||||
@@ -5,21 +5,24 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from deepcoin.config import Settings
|
||||
from deepcoin.ground_truth.pnl import _cluster_signals
|
||||
from deepcoin.notifications.telegram import create_telegram_notifier
|
||||
from deepcoin.operations.candle_sync import sync_ops_candles
|
||||
from deepcoin.operations.executor import create_executor
|
||||
from deepcoin.operations.signal_pipeline import (
|
||||
from bithumb.config import Settings
|
||||
from bithumb.ground_truth.pnl import _cluster_signals
|
||||
from bithumb.notifications.telegram import create_telegram_notifier
|
||||
from bithumb.operations.candle_sync import sync_ops_candles
|
||||
from bithumb.operations.executor import LiveExecutor, create_executor
|
||||
from bithumb.operations.live_bootstrap import sync_portfolio_from_exchange
|
||||
from bithumb.operations.signal_pipeline import (
|
||||
filter_signals_for_ops,
|
||||
generate_raw_signals,
|
||||
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__)
|
||||
|
||||
@@ -91,9 +94,30 @@ class OperationsRunner:
|
||||
initial_cash_krw=settings.gt_initial_cash_krw,
|
||||
)
|
||||
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]:
|
||||
"""신호 확인 및 체결 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
|
||||
candle_sync_results: list[Any] = []
|
||||
if do_sync:
|
||||
@@ -109,6 +133,18 @@ class OperationsRunner:
|
||||
kept = filtered["kept"]
|
||||
|
||||
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))
|
||||
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"]),
|
||||
sig,
|
||||
)
|
||||
try:
|
||||
trade = self.executor.execute_signal(
|
||||
full_sig,
|
||||
self.state["portfolio"],
|
||||
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 = {
|
||||
"datetime": full_sig["datetime"],
|
||||
"side": full_sig["side"],
|
||||
@@ -154,6 +211,7 @@ class OperationsRunner:
|
||||
elif (
|
||||
self.settings.ops_mode == "live"
|
||||
and trade.skip_reason
|
||||
and not trade.expected_skip
|
||||
and self.telegram.is_active
|
||||
):
|
||||
self.telegram.notify_trade_failure(
|
||||
@@ -213,6 +271,50 @@ class OperationsRunner:
|
||||
self._save_report(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(
|
||||
self,
|
||||
signal: dict[str, Any],
|
||||
@@ -243,5 +345,9 @@ class OperationsRunner:
|
||||
"""최신 운영 리포트 저장."""
|
||||
path = self.settings.ops_report_json
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
with path.open("w", encoding="utf-8") as fp:
|
||||
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))
|
||||
@@ -10,17 +10,17 @@ from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.config import Settings
|
||||
from deepcoin.data.candle_loader import load_candles
|
||||
from deepcoin.mtf.extractor import MtfFeatureExtractor
|
||||
from deepcoin.mtf.filter import MtfSignalFilter
|
||||
from deepcoin.mtf.rules import load_or_derive_mtf_rules
|
||||
from deepcoin.mtf.store import MultiTimeframeStore
|
||||
from deepcoin.mtf.trend_gate import HtfTrendGate
|
||||
from deepcoin.operations.signal_type import enrich_signal_types
|
||||
from deepcoin.techniques.base import TechniqueParams, TechniqueResult
|
||||
from deepcoin.techniques.registry import get_technique
|
||||
from deepcoin.techniques.runner import load_technique_result, run_technique, save_technique_result
|
||||
from bithumb.config import Settings
|
||||
from bithumb.data.candle_loader import load_candles
|
||||
from bithumb.mtf.extractor import MtfFeatureExtractor
|
||||
from bithumb.mtf.filter import MtfSignalFilter
|
||||
from bithumb.mtf.rules import load_or_derive_mtf_rules
|
||||
from bithumb.mtf.store import MultiTimeframeStore
|
||||
from bithumb.mtf.trend_gate import HtfTrendGate
|
||||
from bithumb.operations.signal_type import enrich_signal_types
|
||||
from bithumb.techniques.base import TechniqueParams, TechniqueResult
|
||||
from bithumb.techniques.registry import get_technique
|
||||
from bithumb.techniques.runner import load_technique_result, run_technique, save_technique_result
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
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
|
||||
@@ -19,6 +21,7 @@ class TradeResult:
|
||||
fee_krw: float
|
||||
price: float
|
||||
skip_reason: str = ""
|
||||
expected_skip: bool = False
|
||||
api_response: dict[str, Any] | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
@@ -31,10 +34,32 @@ class TradeResult:
|
||||
"fee_krw": round(self.fee_krw, 2),
|
||||
"price": self.price,
|
||||
"skip_reason": self.skip_reason,
|
||||
"expected_skip": self.expected_skip,
|
||||
"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(
|
||||
*,
|
||||
cash_krw: float,
|
||||
@@ -43,13 +68,16 @@ def compute_buy_order(
|
||||
fee_rate: float,
|
||||
min_order_krw: float,
|
||||
cluster_size: int = 1,
|
||||
buy_cash_pct: float | None = None,
|
||||
sizing_rules: dict | None = None,
|
||||
) -> TradeResult:
|
||||
"""매수 주문 금액·수량을 계산한다."""
|
||||
cash = max(float(cash_krw), 0.0)
|
||||
equity = cash + float(coin_qty) * price
|
||||
cash_cap = max_buy_from_cash(equity, cash)
|
||||
per_buy = cash / cluster_size if cluster_size > 0 else cash
|
||||
order_krw = min(per_buy, cash, cash_cap)
|
||||
effective_pct = resolve_buy_cash_pct(sizing_rules, cluster_size, buy_cash_pct)
|
||||
cash_cap = max_buy_from_cash(equity, cash, base_pct=effective_pct)
|
||||
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:
|
||||
return TradeResult(
|
||||
@@ -60,6 +88,7 @@ def compute_buy_order(
|
||||
fee_krw=0.0,
|
||||
price=price,
|
||||
skip_reason="원화 부족 또는 최소 주문 미만",
|
||||
expected_skip=True,
|
||||
)
|
||||
|
||||
fee = order_krw * fee_rate
|
||||
@@ -81,13 +110,29 @@ def compute_sell_order(
|
||||
fee_rate: float,
|
||||
min_order_krw: float,
|
||||
cluster_size: int = 1,
|
||||
sell_coin_pct: float | None = None,
|
||||
sizing_rules: dict | None = None,
|
||||
) -> TradeResult:
|
||||
"""매도 주문 수량을 계산한다."""
|
||||
qty = max(float(coin_qty), 0.0)
|
||||
per_sell = qty / cluster_size if cluster_size > 0 else qty
|
||||
order_coin = per_sell
|
||||
effective_pct = resolve_sell_coin_pct(sizing_rules, cluster_size, sell_coin_pct)
|
||||
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
|
||||
|
||||
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:
|
||||
return TradeResult(
|
||||
executed=False,
|
||||
@@ -96,7 +141,8 @@ def compute_sell_order(
|
||||
order_coin=0.0,
|
||||
fee_krw=0.0,
|
||||
price=price,
|
||||
skip_reason="코인 부족 또는 최소 주문 미만",
|
||||
skip_reason="매도 가능 수량 부족(최소 주문 미만)",
|
||||
expected_skip=True,
|
||||
)
|
||||
|
||||
fee = order_krw * fee_rate
|
||||
@@ -1,13 +1,13 @@
|
||||
"""2단계: Ground Truth 정합 매매 기법."""
|
||||
|
||||
from deepcoin.techniques.registry import (
|
||||
from bithumb.techniques.registry import (
|
||||
get_all_techniques,
|
||||
get_composite_techniques,
|
||||
get_single_techniques,
|
||||
list_technique_ids,
|
||||
techniques_by_category,
|
||||
)
|
||||
from deepcoin.techniques.runner import run_all_techniques, run_technique
|
||||
from bithumb.techniques.runner import run_all_techniques, run_technique
|
||||
|
||||
__all__ = [
|
||||
"get_all_techniques",
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.helpers import make_signal
|
||||
from deepcoin.techniques.indicators import adx
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.helpers import make_signal
|
||||
from bithumb.techniques.indicators import adx
|
||||
|
||||
|
||||
class AdxTrendTechnique(BaseTechnique):
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.helpers import make_signal
|
||||
from deepcoin.techniques.indicators import atr, ema
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.helpers import make_signal
|
||||
from bithumb.techniques.indicators import atr, ema
|
||||
|
||||
|
||||
class AtrChannelTechnique(BaseTechnique):
|
||||
@@ -4,8 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.indicators import bollinger_bands, ema
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.indicators import bollinger_bands, ema
|
||||
|
||||
|
||||
class BbReversalTechnique(BaseTechnique):
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.helpers import make_signal
|
||||
from deepcoin.techniques.indicators import bollinger_bands
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.helpers import make_signal
|
||||
from bithumb.techniques.indicators import bollinger_bands
|
||||
|
||||
|
||||
class BbSqueezeBreakoutTechnique(BaseTechnique):
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.helpers import make_signal, safe_float
|
||||
from deepcoin.techniques.indicators import cci
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.helpers import make_signal, safe_float
|
||||
from bithumb.techniques.indicators import cci
|
||||
|
||||
|
||||
class CciExtremeTechnique(BaseTechnique):
|
||||
@@ -6,8 +6,8 @@ from dataclasses import dataclass
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.indicators import ema
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.indicators import ema
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -4,18 +4,18 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.bb_squeeze_breakout import BbSqueezeBreakoutTechnique
|
||||
from deepcoin.techniques.composite_base import (
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.bb_squeeze_breakout import BbSqueezeBreakoutTechnique
|
||||
from bithumb.techniques.composite_base import (
|
||||
cluster_events,
|
||||
collect_weighted_events,
|
||||
score_clusters_to_signals,
|
||||
)
|
||||
from deepcoin.techniques.donchian import DonchianTechnique
|
||||
from deepcoin.techniques.keltner_breakout import KeltnerBreakoutTechnique
|
||||
from deepcoin.techniques.macd_cross import MacdCrossTechnique
|
||||
from deepcoin.techniques.range_breakout import RangeBreakoutTechnique
|
||||
from deepcoin.techniques.volume_breakout import VolumeBreakoutTechnique
|
||||
from bithumb.techniques.donchian import DonchianTechnique
|
||||
from bithumb.techniques.keltner_breakout import KeltnerBreakoutTechnique
|
||||
from bithumb.techniques.macd_cross import MacdCrossTechnique
|
||||
from bithumb.techniques.range_breakout import RangeBreakoutTechnique
|
||||
from bithumb.techniques.volume_breakout import VolumeBreakoutTechnique
|
||||
|
||||
_SUB = [
|
||||
DonchianTechnique(),
|
||||
@@ -4,17 +4,17 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.composite_base import (
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.composite_base import (
|
||||
cluster_events,
|
||||
collect_weighted_events,
|
||||
score_clusters_to_signals,
|
||||
)
|
||||
from deepcoin.techniques.macd_cross import MacdCrossTechnique
|
||||
from deepcoin.techniques.macd_divergence import MacdDivergenceTechnique
|
||||
from deepcoin.techniques.obv_divergence import ObvDivergenceTechnique
|
||||
from deepcoin.techniques.rsi_divergence import RsiDivergenceTechnique
|
||||
from deepcoin.techniques.rsi_swing import RsiSwingTechnique
|
||||
from bithumb.techniques.macd_cross import MacdCrossTechnique
|
||||
from bithumb.techniques.macd_divergence import MacdDivergenceTechnique
|
||||
from bithumb.techniques.obv_divergence import ObvDivergenceTechnique
|
||||
from bithumb.techniques.rsi_divergence import RsiDivergenceTechnique
|
||||
from bithumb.techniques.rsi_swing import RsiSwingTechnique
|
||||
|
||||
_SUB = [
|
||||
RsiDivergenceTechnique(),
|
||||
@@ -4,15 +4,15 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.composite_base import (
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.composite_base import (
|
||||
cluster_events,
|
||||
collect_weighted_events,
|
||||
score_clusters_to_signals,
|
||||
)
|
||||
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()
|
||||
|
||||
@@ -4,18 +4,18 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.bb_reversal import BbReversalTechnique
|
||||
from deepcoin.techniques.composite_base import (
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.bb_reversal import BbReversalTechnique
|
||||
from bithumb.techniques.composite_base import (
|
||||
cluster_events,
|
||||
collect_weighted_events,
|
||||
score_clusters_to_signals,
|
||||
)
|
||||
from deepcoin.techniques.ema_pullback import EmaPullbackTechnique
|
||||
from deepcoin.techniques.fib_pullback import FibPullbackTechnique
|
||||
from deepcoin.techniques.keltner_reversal import KeltnerReversalTechnique
|
||||
from deepcoin.techniques.local_extrema import LocalExtremaTechnique
|
||||
from deepcoin.techniques.support_bounce import SupportBounceTechnique
|
||||
from bithumb.techniques.ema_pullback import EmaPullbackTechnique
|
||||
from bithumb.techniques.fib_pullback import FibPullbackTechnique
|
||||
from bithumb.techniques.keltner_reversal import KeltnerReversalTechnique
|
||||
from bithumb.techniques.local_extrema import LocalExtremaTechnique
|
||||
from bithumb.techniques.support_bounce import SupportBounceTechnique
|
||||
|
||||
_SUB = [
|
||||
EmaPullbackTechnique(),
|
||||
@@ -4,19 +4,19 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.composite_base import (
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.composite_base import (
|
||||
cluster_events,
|
||||
collect_weighted_events,
|
||||
score_clusters_to_signals,
|
||||
)
|
||||
from deepcoin.techniques.donchian import DonchianTechnique
|
||||
from deepcoin.techniques.fractal_swing import FractalSwingTechnique
|
||||
from deepcoin.techniques.local_extrema import LocalExtremaTechnique
|
||||
from deepcoin.techniques.minor_swing import MinorSwingTechnique
|
||||
from deepcoin.techniques.pivot_swing import PivotSwingTechnique
|
||||
from deepcoin.techniques.swing_failure import SwingFailureTechnique
|
||||
from deepcoin.techniques.zigzag_causal import ZigzagCausalTechnique
|
||||
from bithumb.techniques.donchian import DonchianTechnique
|
||||
from bithumb.techniques.fractal_swing import FractalSwingTechnique
|
||||
from bithumb.techniques.local_extrema import LocalExtremaTechnique
|
||||
from bithumb.techniques.minor_swing import MinorSwingTechnique
|
||||
from bithumb.techniques.pivot_swing import PivotSwingTechnique
|
||||
from bithumb.techniques.swing_failure import SwingFailureTechnique
|
||||
from bithumb.techniques.zigzag_causal import ZigzagCausalTechnique
|
||||
|
||||
_SUB = [
|
||||
ZigzagCausalTechnique(),
|
||||
@@ -4,29 +4,29 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.bb_reversal import BbReversalTechnique
|
||||
from deepcoin.techniques.composite_base import (
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.bb_reversal import BbReversalTechnique
|
||||
from bithumb.techniques.composite_base import (
|
||||
cluster_events,
|
||||
collect_weighted_events,
|
||||
score_clusters_to_signals,
|
||||
)
|
||||
from deepcoin.techniques.donchian import DonchianTechnique
|
||||
from deepcoin.techniques.ema_pullback import EmaPullbackTechnique
|
||||
from deepcoin.techniques.fib_pullback import FibPullbackTechnique
|
||||
from deepcoin.techniques.fractal_swing import FractalSwingTechnique
|
||||
from deepcoin.techniques.keltner_breakout import KeltnerBreakoutTechnique
|
||||
from deepcoin.techniques.local_extrema import LocalExtremaTechnique
|
||||
from deepcoin.techniques.macd_cross import MacdCrossTechnique
|
||||
from deepcoin.techniques.macd_divergence import MacdDivergenceTechnique
|
||||
from deepcoin.techniques.minor_swing import MinorSwingTechnique
|
||||
from deepcoin.techniques.obv_divergence import ObvDivergenceTechnique
|
||||
from deepcoin.techniques.pivot_swing import PivotSwingTechnique
|
||||
from deepcoin.techniques.range_breakout import RangeBreakoutTechnique
|
||||
from deepcoin.techniques.rsi_divergence import RsiDivergenceTechnique
|
||||
from deepcoin.techniques.rsi_swing import RsiSwingTechnique
|
||||
from deepcoin.techniques.support_bounce import SupportBounceTechnique
|
||||
from deepcoin.techniques.zigzag_causal import ZigzagCausalTechnique
|
||||
from bithumb.techniques.donchian import DonchianTechnique
|
||||
from bithumb.techniques.ema_pullback import EmaPullbackTechnique
|
||||
from bithumb.techniques.fib_pullback import FibPullbackTechnique
|
||||
from bithumb.techniques.fractal_swing import FractalSwingTechnique
|
||||
from bithumb.techniques.keltner_breakout import KeltnerBreakoutTechnique
|
||||
from bithumb.techniques.local_extrema import LocalExtremaTechnique
|
||||
from bithumb.techniques.macd_cross import MacdCrossTechnique
|
||||
from bithumb.techniques.macd_divergence import MacdDivergenceTechnique
|
||||
from bithumb.techniques.minor_swing import MinorSwingTechnique
|
||||
from bithumb.techniques.obv_divergence import ObvDivergenceTechnique
|
||||
from bithumb.techniques.pivot_swing import PivotSwingTechnique
|
||||
from bithumb.techniques.range_breakout import RangeBreakoutTechnique
|
||||
from bithumb.techniques.rsi_divergence import RsiDivergenceTechnique
|
||||
from bithumb.techniques.rsi_swing import RsiSwingTechnique
|
||||
from bithumb.techniques.support_bounce import SupportBounceTechnique
|
||||
from bithumb.techniques.zigzag_causal import ZigzagCausalTechnique
|
||||
|
||||
_TECHNIQUE_WEIGHTS: dict[str, tuple[float, float]] = {
|
||||
"zigzag_causal": (2.5, 2.5),
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
|
||||
|
||||
class DonchianTechnique(BaseTechnique):
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.helpers import make_signal
|
||||
from deepcoin.techniques.indicators import ema
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.helpers import make_signal
|
||||
from bithumb.techniques.indicators import ema
|
||||
|
||||
|
||||
class EmaPullbackTechnique(BaseTechnique):
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.helpers import find_confirmed_pivots, make_signal
|
||||
from deepcoin.techniques.indicators import ema
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.helpers import find_confirmed_pivots, make_signal
|
||||
from bithumb.techniques.indicators import ema
|
||||
|
||||
|
||||
class FibPullbackTechnique(BaseTechnique):
|
||||
@@ -4,8 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.helpers import dedupe_signals, find_fractal_pivots, make_signal
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.helpers import dedupe_signals, find_fractal_pivots, make_signal
|
||||
|
||||
|
||||
class FractalSwingTechnique(BaseTechnique):
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import TechniqueSignal
|
||||
from bithumb.techniques.base import TechniqueSignal
|
||||
|
||||
|
||||
def safe_float(value: object) -> float | None:
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.helpers import make_signal
|
||||
from deepcoin.techniques.indicators import ichimoku
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.helpers import make_signal
|
||||
from bithumb.techniques.indicators import ichimoku
|
||||
|
||||
|
||||
class IchimokuTrendTechnique(BaseTechnique):
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.helpers import make_signal
|
||||
from deepcoin.techniques.indicators import keltner_channels
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.helpers import make_signal
|
||||
from bithumb.techniques.indicators import keltner_channels
|
||||
|
||||
|
||||
class KeltnerBreakoutTechnique(BaseTechnique):
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from deepcoin.techniques.helpers import make_signal
|
||||
from deepcoin.techniques.indicators import keltner_channels
|
||||
from bithumb.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal
|
||||
from bithumb.techniques.helpers import make_signal
|
||||
from bithumb.techniques.indicators import keltner_channels
|
||||
|
||||
|
||||
class KeltnerReversalTechnique(BaseTechnique):
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from deepcoin.techniques.base import TechniqueSignal
|
||||
from bithumb.techniques.base import TechniqueSignal
|
||||
|
||||
|
||||
def signals_to_legs(
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user