diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..0f243d7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "superpowers@claude-plugins-official": true + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..68a08fe --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,83 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +CoinTrader is a Python asyncio-based automated cryptocurrency trading bot for Binance Futures. It trades XRPUSDT on 15-minute candles, using BTC/ETH as correlation features. The system has 5 layers: Data (WebSocket streams) → Signal (technical indicators) → ML Filter (ONNX/LightGBM) → Execution & Risk → Event/Alert (Discord). + +## Common Commands + +```bash +# venv +source .venv/bin/activate + +# Run the bot +python main.py + +# Run full test suite +bash scripts/run_tests.sh + +# Run filtered tests +bash scripts/run_tests.sh -k "bot" + +# Run pytest directly +pytest tests/ -v --tb=short + +# ML training pipeline (LightGBM default) +bash scripts/train_and_deploy.sh + +# MLX GPU training (macOS Apple Silicon) +bash scripts/train_and_deploy.sh mlx + +# Hyperparameter tuning (50 trials, 5-fold walk-forward) +python scripts/tune_hyperparams.py + +# Fetch historical data +python scripts/fetch_history.py --symbols XRPUSDT BTCUSDT ETHUSDT --interval 15m --days 365 + +# Deploy models to production +bash scripts/deploy_model.sh +``` + +## Architecture + +**Entry point**: `main.py` → creates `Config` (dataclass from env vars) → runs `TradingBot` + +**5-layer data flow on each 15m candle close:** +1. `src/data_stream.py` — Combined WebSocket for XRP/BTC/ETH, deque buffers (200 candles each) +2. `src/indicators.py` — RSI, MACD, BB, EMA, StochRSI, ATR; weighted signal aggregation → LONG/SHORT/HOLD +3. `src/ml_filter.py` + `src/ml_features.py` — 23-feature extraction, ONNX priority > LightGBM fallback, threshold ≥ 0.60 +4. `src/exchange.py` + `src/risk_manager.py` — Dynamic margin, MARKET orders with SL/TP, daily loss limit (5%) +5. `src/user_data_stream.py` + `src/notifier.py` — Real-time TP/SL detection via WebSocket, Discord webhooks + +**Parallel execution**: `user_data_stream` runs independently via `asyncio.gather()` alongside candle processing. + +## Key Patterns + +- **Async-first**: All I/O operations use `async/await`; parallel tasks via `asyncio.gather()` +- **Reverse signal re-entry**: While holding LONG, if SHORT signal appears → close position, cancel SL/TP, open SHORT. `_is_reentering` flag prevents race conditions with User Data Stream +- **ML hot reload**: `ml_filter.check_and_reload()` compares file mtime on every candle, reloads model without restart +- **Active Config pattern**: Best hyperparams stored in `models/active_lgbm_params.json`, must be manually approved before retraining +- **Graceful degradation**: Missing model → all signals pass; API failure → use fallback values (0.0 for OI/funding) +- **Walk-forward validation**: Time-series CV with undersampling (1:1 class balance, preserving time order) +- **Label generation**: Binary labels based on 24-candle (6h) lookahead — check SL hit first (conservative), then TP + +## Testing + +- All external APIs (Binance, Discord) are mocked with `unittest.mock.AsyncMock` +- Async tests use `@pytest.mark.asyncio` +- 14 test files, 80+ test cases covering all layers +- Testing is done in actual terminal, not IDE sandbox + +## Configuration + +Environment variables via `.env` file (see `.env.example`). Key vars: `BINANCE_API_KEY`, `BINANCE_API_SECRET`, `SYMBOL` (default XRPUSDT), `LEVERAGE`, `DISCORD_WEBHOOK_URL`, `MARGIN_MAX_RATIO`, `MARGIN_MIN_RATIO`, `NO_ML_FILTER`. + +`src/config.py` uses `@dataclass` with `__post_init__` to load and validate all env vars. + +## Deployment + +- **Docker**: `Dockerfile` (Python 3.12-slim) + `docker-compose.yml` +- **CI/CD**: Jenkins pipeline (Gitea → Docker registry → LXC production server) +- Models stored in `models/`, data cache in `data/`, logs in `logs/` diff --git a/docs/plans/2026-03-02-adx-filter-design.md b/docs/plans/2026-03-02-adx-filter-design.md new file mode 100644 index 0000000..863f549 --- /dev/null +++ b/docs/plans/2026-03-02-adx-filter-design.md @@ -0,0 +1,150 @@ +# ADX 횡보장 필터 구현 계획 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** ADX < 25일 때 get_signal()에서 즉시 HOLD를 반환하여 횡보장 진입을 차단한다. + +**Architecture:** `calculate_all()`에서 `pandas_ta.adx()`로 ADX 컬럼을 추가하고, `get_signal()`에서 가중치 계산 전 ADX < 25이면 early-return HOLD. NaN(초기 캔들)은 기존 로직으로 폴백. + +**Tech Stack:** pandas-ta (이미 사용 중), pytest + +--- + +### Task 1: ADX 계산 테스트 추가 + +**Files:** +- Test: `tests/test_indicators.py` + +**Step 1: Write the failing test** + +```python +def test_adx_column_exists(sample_df): + """calculate_all()이 adx 컬럼을 생성하는지 확인.""" + ind = Indicators(sample_df) + df = ind.calculate_all() + assert "adx" in df.columns + valid = df["adx"].dropna() + assert (valid >= 0).all() +``` + +`tests/test_indicators.py`에 위 테스트 함수를 추가한다. + +**Step 2: Run test to verify it fails** + +Run: `pytest tests/test_indicators.py::test_adx_column_exists -v` +Expected: FAIL — `"adx" not in df.columns` + +--- + +### Task 2: calculate_all()에 ADX 계산 추가 + +**Files:** +- Modify: `src/indicators.py:46-48` (vol_ma20 계산 바로 앞에 추가) + +**Step 3: Write minimal implementation** + +`calculate_all()`의 Stochastic RSI 계산 뒤, `vol_ma20` 계산 앞에 추가: + +```python + # ADX (14) — 횡보장 필터 + adx_df = ta.adx(df["high"], df["low"], df["close"], length=14) + df["adx"] = adx_df["ADX_14"] +``` + +**Step 4: Run test to verify it passes** + +Run: `pytest tests/test_indicators.py::test_adx_column_exists -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/indicators.py tests/test_indicators.py +git commit -m "feat: add ADX calculation to indicators" +``` + +--- + +### Task 3: ADX 필터 테스트 추가 (차단 케이스) + +**Files:** +- Test: `tests/test_indicators.py` + +**Step 6: Write the failing test** + +```python +def test_adx_filter_blocks_low_adx(sample_df): + """ADX < 25일 때 가중치와 무관하게 HOLD를 반환해야 한다.""" + ind = Indicators(sample_df) + df = ind.calculate_all() + # ADX를 강제로 낮은 값으로 설정 + df["adx"] = 15.0 + signal = ind.get_signal(df) + assert signal == "HOLD" +``` + +**Step 7: Run test to verify it fails** + +Run: `pytest tests/test_indicators.py::test_adx_filter_blocks_low_adx -v` +Expected: FAIL — signal이 LONG 또는 SHORT 반환 (ADX 필터 미구현) + +--- + +### Task 4: ADX 필터 테스트 추가 (NaN 폴백 케이스) + +**Files:** +- Test: `tests/test_indicators.py` + +**Step 8: Write the failing test** + +```python +def test_adx_nan_falls_through(sample_df): + """ADX가 NaN(초기 캔들)이면 기존 가중치 로직으로 폴백해야 한다.""" + ind = Indicators(sample_df) + df = ind.calculate_all() + df["adx"] = float("nan") + signal = ind.get_signal(df) + # NaN이면 차단하지 않고 기존 로직 실행 → LONG/SHORT/HOLD 중 하나 + assert signal in ("LONG", "SHORT", "HOLD") +``` + +**Step 9: Run test to verify it passes (이 테스트는 현재도 통과)** + +Run: `pytest tests/test_indicators.py::test_adx_nan_falls_through -v` +Expected: PASS (ADX 컬럼이 무시되므로 기존 로직 그대로) + +--- + +### Task 5: get_signal()에 ADX early-return 구현 + +**Files:** +- Modify: `src/indicators.py:51-56` (get_signal 메서드 시작부) + +**Step 10: Write minimal implementation** + +`get_signal()` 메서드의 `last = df.iloc[-1]` 바로 다음에 추가: + +```python + # ADX 횡보장 필터: ADX < 25이면 추세 부재로 판단하여 진입 차단 + adx = last.get("adx", None) + if adx is not None and not pd.isna(adx) and adx < 25: + logger.debug(f"ADX 필터: {adx:.1f} < 25 — HOLD") + return "HOLD" +``` + +**Step 11: Run all ADX-related tests** + +Run: `pytest tests/test_indicators.py -k "adx" -v` +Expected: 3 tests PASS + +**Step 12: Run full test suite to check for regressions** + +Run: `pytest tests/ -v --tb=short` +Expected: All tests PASS + +**Step 13: Commit** + +```bash +git add src/indicators.py tests/test_indicators.py +git commit -m "feat: add ADX filter to block sideways market entries" +```