feat(weekly-report): implement weekly report generation with live trade data and performance tracking

- Added functionality to fetch live trade data from the dashboard API.
- Implemented weekly report generation that includes backtest results, live trade statistics, and performance trends.
- Enhanced error handling for API requests and improved logging for better traceability.
- Updated tests to cover new features and ensure reliability of the report generation process.
This commit is contained in:
21in7
2026-03-07 01:13:03 +09:00
parent 6a6740d708
commit 2a767c35d4
11 changed files with 466 additions and 243 deletions

View File

@@ -13,5 +13,6 @@ ATR_TP_MULT=2.0
SIGNAL_THRESHOLD=3 SIGNAL_THRESHOLD=3
ADX_THRESHOLD=25 ADX_THRESHOLD=25
VOL_MULTIPLIER=2.5 VOL_MULTIPLIER=2.5
DASHBOARD_API_URL=http://10.1.10.24:8000
BINANCE_TESTNET_API_KEY= BINANCE_TESTNET_API_KEY=
BINANCE_TESTNET_API_SECRET= BINANCE_TESTNET_API_SECRET=

View File

@@ -47,7 +47,7 @@ flowchart TD
subgraph 실시간봇["실시간 봇 (bot.py — asyncio)"] subgraph 실시간봇["실시간 봇 (bot.py — asyncio)"]
DS["data_stream.py<br/>MultiSymbolStream (심볼별)<br/>캔들 버퍼 (deque 200개)"] DS["data_stream.py<br/>MultiSymbolStream (심볼별)<br/>캔들 버퍼 (deque 200개)"]
IND["indicators.py<br/>기술 지표 계산<br/>RSI·MACD·BB·EMA·StochRSI·ATR·ADX"] IND["indicators.py<br/>기술 지표 계산<br/>RSI·MACD·BB·EMA·StochRSI·ATR·ADX"]
MF["ml_features.py<br/>23개 피처 추출<br/>(XRP 13 + BTC/ETH 8 + OI/FR 2)"] MF["ml_features.py<br/>26개 피처 추출<br/>(XRP 13 + BTC/ETH 8 + OI/FR 2 + OI파생 2 + ADX 1)"]
ML["ml_filter.py<br/>MLFilter<br/>ONNX 우선 / LightGBM 폴백<br/>확률 ≥ 0.60 시 진입 허용"] ML["ml_filter.py<br/>MLFilter<br/>ONNX 우선 / LightGBM 폴백<br/>확률 ≥ 0.60 시 진입 허용"]
RM["risk_manager.py<br/>RiskManager (공유 싱글턴)<br/>일일 손실 5% 한도<br/>동적 증거금 비율<br/>동일 방향 제한"] RM["risk_manager.py<br/>RiskManager (공유 싱글턴)<br/>일일 손실 5% 한도<br/>동적 증거금 비율<br/>동일 방향 제한"]
EX["exchange.py<br/>BinanceFuturesClient<br/>주문·레버리지·잔고 API"] EX["exchange.py<br/>BinanceFuturesClient<br/>주문·레버리지·잔고 API"]
@@ -183,9 +183,10 @@ flowchart TD
EMA 정배열 (9 > 21 > 50) → +1 EMA 정배열 (9 > 21 > 50) → +1
StochRSI K < 20 and K > D → +1 StochRSI K < 20 and K > D → +1
진입 조건: 점수 ≥ 3 AND (거래량 급증 OR 점수 ≥ 4) 진입 조건: 점수 ≥ SIGNAL_THRESHOLD(기본 3) AND (거래량 ≥ 20MA × VOL_MULTIPLIER(기본 2.5) OR 점수 ≥ SIGNAL_THRESHOLD + 1)
SL = 진입가 - ATR × 1.5 SL = 진입가 - ATR × ATR_SL_MULT (기본 2.0)
TP = 진입가 + ATR × 3.0 (리스크:리워드 = 1:2) TP = 진입가 + ATR × ATR_TP_MULT (기본 2.0)
※ SL/TP/신호임계값/ADX/거래량배수 모두 환경변수로 설정 가능
``` ```
숏 신호는 롱의 대칭 조건으로 계산됩니다. 숏 신호는 롱의 대칭 조건으로 계산됩니다.
@@ -206,7 +207,7 @@ ONNX (MLX 신경망) → LightGBM → 폴백(항상 허용)
모델 파일이 없으면 모든 신호를 허용합니다. 봇 재시작 없이 모델 파일을 교체하면 다음 캔들 마감 시 자동으로 핫리로드됩니다(`mtime` 감지). 모델 파일이 없으면 모든 신호를 허용합니다. 봇 재시작 없이 모델 파일을 교체하면 다음 캔들 마감 시 자동으로 핫리로드됩니다(`mtime` 감지).
**23개 ML 피처:** **26개 ML 피처:**
``` ```
XRP 기술 지표 (13개): XRP 기술 지표 (13개):
@@ -222,6 +223,13 @@ BTC/ETH 상관관계 (8개):
시장 미시구조 (2개): 시장 미시구조 (2개):
oi_change ← 이전 캔들 대비 미결제약정 변화율 oi_change ← 이전 캔들 대비 미결제약정 변화율
funding_rate ← 현재 펀딩비 funding_rate ← 현재 펀딩비
OI 파생 피처 (2개):
oi_change_ma5 ← OI 변화율 5캔들 이동평균 (스마트머니 추세)
oi_price_spread ← OI 변화율 - 가격 변화율 (OI-가격 괴리도)
추세 강도 (1개):
adx ← ADX 값 (ML 모델이 횡보/추세 판단에 활용)
``` ```
`oi_change``funding_rate`는 캔들 마감마다 Binance REST API로 실시간 조회합니다. API 실패 시 `0.0`으로 폴백하여 봇이 멈추지 않습니다. `oi_change``funding_rate`는 캔들 마감마다 Binance REST API로 실시간 조회합니다. API 실패 시 `0.0`으로 폴백하여 봇이 멈추지 않습니다.
@@ -230,9 +238,13 @@ BTC/ETH 상관관계 (8개):
```python ```python
proba = model.predict_proba(features)[0][1] # 성공 확률 proba = model.predict_proba(features)[0][1] # 성공 확률
return proba >= 0.60 # 임계값 60% return proba >= 0.55 # 임계값 55% (ML_THRESHOLD 환경변수)
``` ```
**ML 필터 현황 — 현재 비활성화 상태:**
프로덕션에서 `NO_ML_FILTER=true`로 ML 필터를 비활성화하고 있습니다. Walk-Forward 검증 결과 각 폴드 학습 세트에 유효 신호가 약 27건으로, LightGBM이 의미 있는 패턴을 학습하기엔 표본이 절대적으로 부족합니다. 모든 입력에 동일한 확률(~0.55)을 출력하여 필터링 효과가 없었습니다. 전략 파라미터 스윕에서 ADX 필터(≥25) + 거래량 배수(2.5) 조합만으로 PF 1.57~2.39를 달성하여, 충분한 트레이드 데이터가 축적될 때까지 ML 없이 운영합니다.
--- ---
### Layer 4: Execution & Risk Layer ### Layer 4: Execution & Risk Layer
@@ -444,14 +456,33 @@ if onnx_changed or lgbm_changed:
매 캔들 마감(15분)마다 모델 파일의 `mtime`을 확인합니다. 변경이 감지되면 즉시 리로드합니다. 매 캔들 마감(15분)마다 모델 파일의 `mtime`을 확인합니다. 변경이 감지되면 즉시 리로드합니다.
### 3.3 레이블 생성 방식 ### 3.3 주간 전략 모니터링
`scripts/weekly_report.py`가 매주 자동으로 전략 성능을 측정하고 Discord로 리포트를 전송합니다.
```
[매주 일요일 크론탭]
[1/6] 데이터 수집 (fetch_history.py × 3심볼, 최근 35일 Upsert)
[2/6] Walk-Forward 백테스트 (심볼별 → 합산 PF/승률/MDD)
[3/6] 실전 봇 로그 파싱 (이번 주 진입/청산 기록)
[4/6] 추이 분석 (이전 results/weekly/*.json에서 PF/승률/MDD 추이)
[5/6] ML 재도전 체크 (누적 트레이드 ≥ 150, PF < 1.0, PF 3주 하락 → 2/3 충족 시 권장)
[6/6] PF < 1.0이면 파라미터 스윕 실행 → 상위 3개 대안 제시
→ Discord 알림 + results/weekly/report_YYYY-MM-DD.json 저장
```
**전략 파라미터 스윕**: 성능 저하 감지 시 324개 파라미터 조합(SL/TP/ADX/신호임계값/거래량배수)을 자동 탐색하여 현재보다 높은 PF의 대안을 제시합니다. 자동 적용되지 않으며, 사람이 검토 후 승인해야 합니다.
### 3.4 레이블 생성 방식
학습 데이터의 레이블은 **미래 6시간(24캔들) 룩어헤드**로 생성됩니다. 학습 데이터의 레이블은 **미래 6시간(24캔들) 룩어헤드**로 생성됩니다.
``` ```
신호 발생 시점 기준: 신호 발생 시점 기준:
SL = 진입가 - ATR × 1.5 SL = 진입가 - ATR × ATR_SL_MULT (기본 2.0)
TP = 진입가 + ATR × 3.0 TP = 진입가 + ATR × ATR_TP_MULT (기본 2.0)
향후 24캔들 동안: 향후 24캔들 동안:
- 저가가 SL에 먼저 닿으면 → label = 0 (실패) - 저가가 SL에 먼저 닿으면 → label = 0 (실패)
@@ -496,7 +527,7 @@ sequenceDiagram
alt 신호 = LONG 또는 SHORT, 포지션 없음 alt 신호 = LONG 또는 SHORT, 포지션 없음
BOT->>MF: build_features(df, signal, btc_df, eth_df, oi_change, funding_rate) BOT->>MF: build_features(df, signal, btc_df, eth_df, oi_change, funding_rate)
MF-->>BOT: features (23개 피처 Series) MF-->>BOT: features (26개 피처 Series)
BOT->>ML: should_enter(features) BOT->>ML: should_enter(features)
ML-->>BOT: proba=0.73 ≥ 0.60 → True ML-->>BOT: proba=0.73 ≥ 0.60 → True
@@ -568,7 +599,7 @@ sequenceDiagram
### 5.1 테스트 파일 구성 ### 5.1 테스트 파일 구성
`tests/` 폴더에 14개 테스트 파일, 총 **99개의 테스트 케이스**가 작성되어 있습니다. `tests/` 폴더에 15개 테스트 파일, 총 **135개의 테스트 케이스**가 작성되어 있습니다.
```bash ```bash
pytest tests/ -v # 전체 실행 pytest tests/ -v # 전체 실행
@@ -581,7 +612,7 @@ bash scripts/run_tests.sh # 래퍼 스크립트 실행
|------------|----------|:------------:|--------------| |------------|----------|:------------:|--------------|
| `test_bot.py` | `src/bot.py` | 11 | 반대 시그널 재진입 흐름, ML 차단 시 재진입 스킵, OI/펀딩비 피처 전달, OI 변화율 계산 | | `test_bot.py` | `src/bot.py` | 11 | 반대 시그널 재진입 흐름, ML 차단 시 재진입 스킵, OI/펀딩비 피처 전달, OI 변화율 계산 |
| `test_indicators.py` | `src/indicators.py` | 7 | RSI 범위(0~100), MACD 컬럼 존재, 볼린저 밴드 상하단 대소관계, 신호 반환값 유효성, ADX 컬럼 존재, ADX<25 횡보장 차단, ADX NaN 폴스루 | | `test_indicators.py` | `src/indicators.py` | 7 | RSI 범위(0~100), MACD 컬럼 존재, 볼린저 밴드 상하단 대소관계, 신호 반환값 유효성, ADX 컬럼 존재, ADX<25 횡보장 차단, ADX NaN 폴스루 |
| `test_ml_features.py` | `src/ml_features.py` | 11 | 23개 피처 수, BTC/ETH 포함 시 피처 수, RS 분모 0 처리, NaN 없음, side 인코딩, OI/펀딩비 파라미터 반영 | | `test_ml_features.py` | `src/ml_features.py` | 11 | 26개 피처 수, BTC/ETH 포함 시 피처 수, RS 분모 0 처리, NaN 없음, side 인코딩, OI/펀딩비 파라미터 반영 |
| `test_ml_filter.py` | `src/ml_filter.py` | 5 | 모델 없을 때 폴백 허용, 임계값 이상/미만 판단, 핫리로드 후 상태 변화 | | `test_ml_filter.py` | `src/ml_filter.py` | 5 | 모델 없을 때 폴백 허용, 임계값 이상/미만 판단, 핫리로드 후 상태 변화 |
| `test_risk_manager.py` | `src/risk_manager.py` | 13 | 일일 손실 한도 초과 차단, 최대 포지션 수 제한, 동일 방향 제한, 심볼 중복 차단, 비동기 포지션 등록/해제, 동적 증거금 비율 상한/하한 클램핑 | | `test_risk_manager.py` | `src/risk_manager.py` | 13 | 일일 손실 한도 초과 차단, 최대 포지션 수 제한, 동일 방향 제한, 심볼 중복 차단, 비동기 포지션 등록/해제, 동적 증거금 비율 상한/하한 클램핑 |
| `test_exchange.py` | `src/exchange.py` | 8 | 수량 계산(기본/최소명목금액/잔고0), OI·펀딩비 조회 정상/오류 시 반환값 | | `test_exchange.py` | `src/exchange.py` | 8 | 수량 계산(기본/최소명목금액/잔고0), OI·펀딩비 조회 정상/오류 시 반환값 |
@@ -591,6 +622,7 @@ bash scripts/run_tests.sh # 래퍼 스크립트 실행
| `test_mlx_filter.py` | `src/mlx_filter.py` | 5 | GPU 디바이스 확인, 학습 전 예측 형태, 학습 후 유효 확률, NaN 피처 처리, 저장/로드 후 동일 예측 | | `test_mlx_filter.py` | `src/mlx_filter.py` | 5 | GPU 디바이스 확인, 학습 전 예측 형태, 학습 후 유효 확률, NaN 피처 처리, 저장/로드 후 동일 예측 |
| `test_fetch_history.py` | `scripts/fetch_history.py` | 5 | OI=0 구간 Upsert, 신규 행 추가, 기존 비0값 보존, 파일 없을 때 신규 반환, 타임스탬프 오름차순 정렬 | | `test_fetch_history.py` | `scripts/fetch_history.py` | 5 | OI=0 구간 Upsert, 신규 행 추가, 기존 비0값 보존, 파일 없을 때 신규 반환, 타임스탬프 오름차순 정렬 |
| `test_config.py` | `src/config.py` | 6 | 환경변수 로드, 동적 증거금 파라미터 로드, `symbols` 리스트, `correlation_symbols`, `max_same_direction`, SYMBOL→symbols 폴백 | | `test_config.py` | `src/config.py` | 6 | 환경변수 로드, 동적 증거금 파라미터 로드, `symbols` 리스트, `correlation_symbols`, `max_same_direction`, SYMBOL→symbols 폴백 |
| `test_weekly_report.py` | `scripts/weekly_report.py` | 14 | 데이터 수집 subprocess 호출, WF 백테스트 실행, 로그 파싱(진입/청산), 추이 로드(PF 하락 감지), ML 트리거 체크, 성능 저하 스윕, Discord 포맷/전송, JSON 저장 |
> `test_mlx_filter.py`는 Apple Silicon(`mlx` 패키지)이 없는 환경에서 자동 스킵됩니다. > `test_mlx_filter.py`는 Apple Silicon(`mlx` 패키지)이 없는 환경에서 자동 스킵됩니다.
@@ -603,7 +635,7 @@ bash scripts/run_tests.sh # 래퍼 스크립트 실행
| 기술 지표 계산 (RSI/MACD/BB/EMA/StochRSI/ADX) | ✅ | ✅ | `test_indicators` + `test_ml_features` + `test_dataset_builder` | | 기술 지표 계산 (RSI/MACD/BB/EMA/StochRSI/ADX) | ✅ | ✅ | `test_indicators` + `test_ml_features` + `test_dataset_builder` |
| 신호 생성 (가중치 합산) | ✅ | ✅ | `test_indicators` + `test_dataset_builder` | | 신호 생성 (가중치 합산) | ✅ | ✅ | `test_indicators` + `test_dataset_builder` |
| ADX 횡보장 필터 (ADX < 25 차단) | ✅ | ✅ | `test_indicators` + `test_dataset_builder` (`_calc_signals` 실제 호출) | | ADX 횡보장 필터 (ADX < 25 차단) | ✅ | ✅ | `test_indicators` + `test_dataset_builder` (`_calc_signals` 실제 호출) |
| ML 피처 추출 (23개) | ✅ | ✅ | `test_ml_features` + `test_dataset_builder` (`_calc_features_vectorized` 실제 호출) | | ML 피처 추출 (26개) | ✅ | ✅ | `test_ml_features` + `test_dataset_builder` (`_calc_features_vectorized` 실제 호출) |
| ML 필터 추론 (임계값 판단) | ✅ | — | `test_ml_filter` | | ML 필터 추론 (임계값 판단) | ✅ | — | `test_ml_filter` |
| MLX 신경망 학습/저장/로드 | ✅ | — | `test_mlx_filter` (Apple Silicon 전용) | | MLX 신경망 학습/저장/로드 | ✅ | — | `test_mlx_filter` (Apple Silicon 전용) |
| 레이블 생성 (SL/TP 룩어헤드) | ✅ | ✅ | `test_label_builder` + `test_dataset_builder` (전체 파이프라인 실제 호출) | | 레이블 생성 (SL/TP 룩어헤드) | ✅ | ✅ | `test_label_builder` + `test_dataset_builder` (전체 파이프라인 실제 호출) |
@@ -619,6 +651,7 @@ bash scripts/run_tests.sh # 래퍼 스크립트 실행
| OI 변화율 계산 (API 실패 폴백) | ✅ | ✅ | `test_bot` (`process_candle` → OI 조회 → `_calc_oi_change` 흐름) | | OI 변화율 계산 (API 실패 폴백) | ✅ | ✅ | `test_bot` (`process_candle` → OI 조회 → `_calc_oi_change` 흐름) |
| 캔들 버퍼 관리 및 프리로드 | ✅ | — | `test_data_stream` | | 캔들 버퍼 관리 및 프리로드 | ✅ | — | `test_data_stream` |
| Parquet Upsert (OI=0 보충) | ✅ | — | `test_fetch_history` | | Parquet Upsert (OI=0 보충) | ✅ | — | `test_fetch_history` |
| 주간 리포트 (백테스트+로그+추이+스윕) | ✅ | ✅ | `test_weekly_report` (14개 테스트: 데이터 수집, 백테스트, 로그 파싱, 추이, ML 트리거, 스윕, 포맷, 전송, JSON 저장) |
| User Data Stream TP/SL 감지 | ❌ | — | 미작성 (실제 WebSocket 의존) | | User Data Stream TP/SL 감지 | ❌ | — | 미작성 (실제 WebSocket 의존) |
| Discord 알림 전송 | ❌ | — | 미작성 (외부 웹훅 의존) | | Discord 알림 전송 | ❌ | — | 미작성 (외부 웹훅 의존) |
| CI/CD 파이프라인 | ❌ | — | Jenkins 환경 의존 | | CI/CD 파이프라인 | ❌ | — | Jenkins 환경 의존 |
@@ -650,7 +683,7 @@ bash scripts/run_tests.sh # 래퍼 스크립트 실행
| `src/config.py` | — | 환경변수 기반 설정 (`symbols` 리스트, `correlation_symbols`) | | `src/config.py` | — | 환경변수 기반 설정 (`symbols` 리스트, `correlation_symbols`) |
| `src/data_stream.py` | Data | Combined WebSocket 캔들 수신·버퍼 관리 | | `src/data_stream.py` | Data | Combined WebSocket 캔들 수신·버퍼 관리 |
| `src/indicators.py` | Signal | 기술 지표 계산 및 복합 신호 생성 | | `src/indicators.py` | Signal | 기술 지표 계산 및 복합 신호 생성 |
| `src/ml_features.py` | ML Filter | 23개 ML 피처 추출 | | `src/ml_features.py` | ML Filter | 26개 ML 피처 추출 |
| `src/ml_filter.py` | ML Filter | ONNX/LightGBM 모델 로드·추론·핫리로드 | | `src/ml_filter.py` | ML Filter | ONNX/LightGBM 모델 로드·추론·핫리로드 |
| `src/mlx_filter.py` | ML Filter | Apple Silicon GPU 학습 + ONNX export | | `src/mlx_filter.py` | ML Filter | Apple Silicon GPU 학습 + ONNX export |
| `src/exchange.py` | Execution | Binance Futures REST API 클라이언트 (심볼별 독립) | | `src/exchange.py` | Execution | Binance Futures REST API 클라이언트 (심볼별 독립) |
@@ -666,4 +699,8 @@ bash scripts/run_tests.sh # 래퍼 스크립트 실행
| `scripts/tune_hyperparams.py` | MLOps | Optuna 하이퍼파라미터 탐색 (`--symbol` 지원) | | `scripts/tune_hyperparams.py` | MLOps | Optuna 하이퍼파라미터 탐색 (`--symbol` 지원) |
| `scripts/train_and_deploy.sh` | MLOps | 전체 파이프라인 (`--symbol` / `--all` 지원) | | `scripts/train_and_deploy.sh` | MLOps | 전체 파이프라인 (`--symbol` / `--all` 지원) |
| `scripts/deploy_model.sh` | MLOps | 모델 파일 LXC 서버 전송 (`--symbol` 지원) | | `scripts/deploy_model.sh` | MLOps | 모델 파일 LXC 서버 전송 (`--symbol` 지원) |
| `scripts/strategy_sweep.py` | MLOps | 전략 파라미터 그리드 스윕 (324개 조합) |
| `scripts/weekly_report.py` | MLOps | 주간 전략 리포트 (백테스트+로그+추이+스윕+Discord) |
| `scripts/run_backtest.py` | MLOps | 단일 백테스트 CLI |
| `src/backtester.py` | MLOps | 백테스트 엔진 (단일 + Walk-Forward) |
| `models/{symbol}/active_lgbm_params.json` | MLOps | 심볼별 승인된 LightGBM 파라미터 (Active Config) | | `models/{symbol}/active_lgbm_params.json` | MLOps | 심볼별 승인된 LightGBM 파라미터 (Active Config) |

104
README.md
View File

@@ -12,10 +12,13 @@ Binance Futures 자동매매 봇. 복합 기술 지표와 ML 필터(LightGBM / M
- **ML 필터 (ONNX 우선 / LightGBM 폴백)**: 기술 지표 신호를 한 번 더 검증하여 오진입 차단. 우선순위: ONNX > LightGBM > 폴백(항상 허용) - **ML 필터 (ONNX 우선 / LightGBM 폴백)**: 기술 지표 신호를 한 번 더 검증하여 오진입 차단. 우선순위: ONNX > LightGBM > 폴백(항상 허용)
- **모델 핫리로드**: 캔들마다 모델 파일 mtime을 감지해 변경 시 자동 리로드 (봇 재시작 불필요) - **모델 핫리로드**: 캔들마다 모델 파일 mtime을 감지해 변경 시 자동 리로드 (봇 재시작 불필요)
- **멀티심볼 스트림**: XRP/BTC/ETH 3개 심볼을 단일 Combined WebSocket으로 수신, BTC·ETH 상관관계 피처 활용 - **멀티심볼 스트림**: XRP/BTC/ETH 3개 심볼을 단일 Combined WebSocket으로 수신, BTC·ETH 상관관계 피처 활용
- **23개 ML 피처**: XRP 기술 지표 13개 + BTC/ETH 수익률·상대강도 8개 + OI 변화율·펀딩비 2개 (캔들 마감 시 실시간 조회, 실패 시 0으로 폴백) - **26개 ML 피처**: XRP 기술 지표 13개 + BTC/ETH 수익률·상대강도 8개 + OI 변화율·펀딩비 2개 + OI 파생 피처 2개(oi_change_ma5, oi_price_spread) + ADX 1개 (캔들 마감 시 실시간 조회, 실패 시 0으로 폴백)
- **점진적 OI 데이터 축적 (Upsert)**: 바이낸스 OI 히스토리 API는 최근 30일치만 제공. `fetch_history.py` 실행 시 기존 parquet의 `oi_change/funding_rate=0` 구간을 신규 값으로 채워 학습 데이터 품질을 점진적으로 개선 - **점진적 OI 데이터 축적 (Upsert)**: 바이낸스 OI 히스토리 API는 최근 30일치만 제공. `fetch_history.py` 실행 시 기존 parquet의 `oi_change/funding_rate=0` 구간을 신규 값으로 채워 학습 데이터 품질을 점진적으로 개선
- **실시간 OI/펀딩비 조회**: 캔들 마감마다 `get_open_interest()` / `get_funding_rate()`를 비동기 병렬 조회하여 ML 피처에 전달. 이전 캔들 대비 OI 변화율로 변환하여 train-serve skew 해소 - **실시간 OI/펀딩비 조회**: 캔들 마감마다 `get_open_interest()` / `get_funding_rate()`를 비동기 병렬 조회하여 ML 피처에 전달. 이전 캔들 대비 OI 변화율로 변환하여 train-serve skew 해소
- **ATR 기반 손절/익절**: 변동성에 따라 동적으로 SL/TP 계산 (1.5× / 3.0× ATR) - **ATR 기반 손절/익절**: 변동성에 따라 동적으로 SL/TP 계산 (기본 2.0× / 2.0× ATR, 환경변수로 설정 가능)
- **전략 파라미터 스윕**: 324개 파라미터 조합(SL/TP/ADX/신호임계값/거래량배수)을 Walk-Forward 백테스트로 체계적 탐색, 수익 구간 자동 발견
- **주간 전략 리포트**: 매주 자동으로 백테스트 성능 측정, 실전 로그 파싱, 추이 추적, ML 재학습 시점 판단, 성능 저하 시 대안 파라미터 스윕, Discord 알림
- **ML 필터 비활성화 모드**: `NO_ML_FILTER=true` 설정 시 ML 모델 로드 없이 기술 지표 신호만으로 운영 (현재 프로덕션 기본값 — 아래 "ML 필터 현황" 참고)
- **Algo Order API 지원**: 계정 설정에 따라 STOP_MARKET/TAKE_PROFIT_MARKET 주문을 `/fapi/v1/algoOrder` 엔드포인트로 자동 전송 (오류 코드 -4120 대응) - **Algo Order API 지원**: 계정 설정에 따라 STOP_MARKET/TAKE_PROFIT_MARKET 주문을 `/fapi/v1/algoOrder` 엔드포인트로 자동 전송 (오류 코드 -4120 대응)
- **동적 증거금 비율**: 잔고 증가에 따라 선형 감소 (최대 50% → 최소 20%) - **동적 증거금 비율**: 잔고 증가에 따라 선형 감소 (최대 50% → 최소 20%)
- **반대 시그널 재진입**: 보유 포지션과 반대 신호 발생 시 즉시 청산 후 ML 필터 통과 시 반대 방향 재진입 - **반대 시그널 재진입**: 보유 포지션과 반대 신호 발생 시 즉시 청산 후 ML 필터 통과 시 반대 방향 재진입
@@ -56,6 +59,9 @@ cointrader/
│ ├── train_mlx_model.py # MLX 신경망 학습 (Apple Silicon GPU) │ ├── train_mlx_model.py # MLX 신경망 학습 (Apple Silicon GPU)
│ ├── train_and_deploy.sh # 전체 파이프라인 (--symbol / --all 지원) │ ├── train_and_deploy.sh # 전체 파이프라인 (--symbol / --all 지원)
│ ├── tune_hyperparams.py # Optuna 하이퍼파라미터 자동 탐색 (--symbol 지원) │ ├── tune_hyperparams.py # Optuna 하이퍼파라미터 자동 탐색 (--symbol 지원)
│ ├── strategy_sweep.py # 전략 파라미터 그리드 스윕 (324개 조합)
│ ├── weekly_report.py # 주간 전략 리포트 (백테스트+로그+추이+Discord)
│ ├── run_backtest.py # 단일 백테스트 CLI
│ ├── deploy_model.sh # 모델 파일 LXC 서버 전송 (--symbol 지원) │ ├── deploy_model.sh # 모델 파일 LXC 서버 전송 (--symbol 지원)
│ └── run_tests.sh # 전체 테스트 실행 │ └── run_tests.sh # 전체 테스트 실행
├── dashboard/ ├── dashboard/
@@ -69,6 +75,8 @@ cointrader/
│ ├── xrpusdt/ # data/xrpusdt/combined_15m.parquet │ ├── xrpusdt/ # data/xrpusdt/combined_15m.parquet
│ ├── trxusdt/ # data/trxusdt/combined_15m.parquet │ ├── trxusdt/ # data/trxusdt/combined_15m.parquet
│ └── dogeusdt/ # data/dogeusdt/combined_15m.parquet │ └── dogeusdt/ # data/dogeusdt/combined_15m.parquet
├── results/
│ └── weekly/ # 주간 리포트 JSON 저장
├── logs/ # 로그 파일 ├── logs/ # 로그 파일
├── docs/plans/ # 설계 문서 및 구현 플랜 ├── docs/plans/ # 설계 문서 및 구현 플랜
├── tests/ # 테스트 코드 ├── tests/ # 테스트 코드
@@ -225,9 +233,10 @@ MLX로 학습한 모델은 ONNX 포맷으로 export되어 Linux 서버에서 `on
| Stochastic RSI | < 20 + K>D | > 80 + K<D | 1 | | Stochastic RSI | < 20 + K>D | > 80 + K<D | 1 |
| 거래량 | 20MA × 1.5 이상 시 신호 강화 | — | 보조 | | 거래량 | 20MA × 1.5 이상 시 신호 강화 | — | 보조 |
**진입 조건**: 가중치 합계 ≥ 3 + (거래량 급증 또는 가중치 합계 ≥ 4) **진입 조건**: 가중치 합계 ≥ `SIGNAL_THRESHOLD`(기본 3) + (거래량 ≥ 20MA × `VOL_MULTIPLIER`(기본 2.5) 또는 가중치 합계 ≥ `SIGNAL_THRESHOLD` + 1)
**손절/익절**: ATR × 1.5 / ATR × 3.0 (리스크:리워드 = 1:2) **ADX 필터**: ADX < `ADX_THRESHOLD`(기본 25) 시 횡보장으로 판단, 진입 차단
**ML 필터**: 예측 확률 ≥ 0.60 이어야 최종 진입 **손절/익절**: ATR × `ATR_SL_MULT`(기본 2.0) / ATR × `ATR_TP_MULT`(기본 2.0) — 환경변수로 설정 가능
**ML 필터**: 예측 확률 ≥ 0.55 이어야 최종 진입 (현재 `NO_ML_FILTER=true`로 비활성화 — 아래 참고)
### 반대 시그널 재진입 ### 반대 시그널 재진입
@@ -235,6 +244,82 @@ MLX로 학습한 모델은 ONNX 포맷으로 export되어 Linux 서버에서 `on
1. 기존 포지션 즉시 청산 (미체결 SL/TP 주문 취소 포함) 1. 기존 포지션 즉시 청산 (미체결 SL/TP 주문 취소 포함)
2. ML 필터 통과 시 반대 방향으로 즉시 재진입 2. ML 필터 통과 시 반대 방향으로 즉시 재진입
### ML 필터 현황 — 왜 현재 ML을 사용하지 않는가
현재 프로덕션 봇은 `NO_ML_FILTER=true`로 ML 필터를 **비활성화**한 상태로 운영 중입니다.
**비활성화 사유:**
1. **학습 데이터 부족**: Walk-Forward 검증(학습 3개월, 테스트 1개월) 시 각 폴드의 학습 세트에서 유효 신호가 약 27건에 불과. LightGBM이 의미 있는 패턴을 학습하기엔 표본 수가 절대적으로 부족.
2. **예측 무차별**: 학습된 모델이 모든 입력에 대해 거의 동일한 확률(~0.55)을 출력하여 필터링 효과가 사실상 없음. 모든 신호를 차단하거나 모든 신호를 통과시키는 극단적 동작.
3. **전략 파라미터 스윕 결과**: ADX 필터(≥25)와 거래량 배수(2.5)를 적용한 기본 기술 지표 전략만으로 PF 1.57~2.39를 달성. ML 없이도 수익성 확보 가능.
**ML 재활성화 조건 (주간 리포트에서 자동 체크):**
- 누적 트레이드 ≥ 150건 (충분한 학습 데이터 확보)
- 현재 PF < 1.0 (기술 지표만으로 수익성 저하)
- PF 3주 연속 하락 추세
3개 조건 중 2개 이상 충족 시 `scripts/weekly_report.py`가 Discord로 ML 재학습 권장 알림을 전송합니다.
---
## 전략 파라미터 스윕
기술 지표 전략의 최적 파라미터를 Walk-Forward 백테스트로 탐색합니다.
```bash
# 전체 스윕 (324개 조합, ~30분)
python scripts/strategy_sweep.py --symbols XRPUSDT --train-months 3 --test-months 1
# 결과 확인
cat results/sweep_*.json | python -m json.tool | head -50
```
5개 파라미터 × 3~4개 값 = 324개 조합을 순차 테스트:
| 파라미터 | 값 | 설명 |
|---------|------|------|
| `ATR_SL_MULT` | 1.0, 1.5, 2.0 | 손절 ATR 배수 |
| `ATR_TP_MULT` | 2.0, 3.0, 4.0 | 익절 ATR 배수 |
| `SIGNAL_THRESHOLD` | 3, 4, 5 | 최소 가중치 점수 |
| `ADX_THRESHOLD` | 0, 20, 25, 30 | ADX 필터 (0=비활성) |
| `VOL_MULTIPLIER` | 1.5, 2.0, 2.5 | 거래량 급증 배수 |
> **핵심 발견**: ADX ≥ 25 필터가 가장 영향력 있는 단일 파라미터. 상위 10개 결과 모두 ADX ≥ 25를 사용하며, 횡보장 노이즈 신호를 효과적으로 필터링.
---
## 주간 전략 리포트
매주 자동으로 전략 성능을 측정하고 Discord로 리포트를 전송합니다.
```bash
# 수동 실행 (데이터 수집 스킵)
python scripts/weekly_report.py --skip-fetch
# 전체 실행 (데이터 수집 포함)
python scripts/weekly_report.py
# 특정 날짜 리포트
python scripts/weekly_report.py --date 2026-03-07
```
**리포트 내용:**
- Walk-Forward 백테스트 성능 (심볼별 PF/승률/MDD)
- 실전 트레이드 로그 파싱 (이번 주 거래 수/순수익/승률)
- 성능 추이 (최근 4주 PF/승률/MDD 변화)
- ML 재도전 체크리스트 (3개 조건 자동 판단)
- PF < 1.0 시 파라미터 스윕 대안 제시
**크론탭 설정 (프로덕션 서버):**
```bash
# 매주 일요일 새벽 3시 KST
0 18 * * 6 cd /app && python scripts/weekly_report.py >> logs/cron.log 2>&1
```
리포트 결과는 `results/weekly/report_YYYY-MM-DD.json`에 저장됩니다.
--- ---
## CI/CD ## CI/CD
@@ -321,8 +406,13 @@ pytest tests/ -v
| `MARGIN_MAX_RATIO` | `0.50` | 최대 증거금 비율 (잔고 대비 50%) | | `MARGIN_MAX_RATIO` | `0.50` | 최대 증거금 비율 (잔고 대비 50%) |
| `MARGIN_MIN_RATIO` | `0.20` | 최소 증거금 비율 (잔고 대비 20%) | | `MARGIN_MIN_RATIO` | `0.20` | 최소 증거금 비율 (잔고 대비 20%) |
| `MARGIN_DECAY_RATE` | `0.0006` | 잔고 증가 시 증거금 비율 감소 속도 | | `MARGIN_DECAY_RATE` | `0.0006` | 잔고 증가 시 증거금 비율 감소 속도 |
| `NO_ML_FILTER` | | `true`/`1`/`yes` 설정 시 ML 필터 완전 비활성화 — 모델 로드 없이 모든 신호 허용 | | `NO_ML_FILTER` | `true` | `true`/`1`/`yes` 설정 시 ML 필터 완전 비활성화 — 모델 로드 없이 모든 신호 허용 (현재 프로덕션 기본값) |
| `ML_THRESHOLD` | `0.55` | ML 필터 예측 확률 임계값 — 이 값 이상이어야 진입 허용 (기본값 0.55) | | `ML_THRESHOLD` | `0.55` | ML 필터 예측 확률 임계값 — 이 값 이상이어야 진입 허용 |
| `ATR_SL_MULT` | `2.0` | 손절 ATR 배수 (진입가 ± ATR × 이 값) |
| `ATR_TP_MULT` | `2.0` | 익절 ATR 배수 (진입가 ± ATR × 이 값) |
| `SIGNAL_THRESHOLD` | `3` | 진입을 위한 최소 가중치 지표 점수 |
| `ADX_THRESHOLD` | `25` | ADX 횡보장 필터 (이 값 미만이면 진입 차단, 0=비활성) |
| `VOL_MULTIPLIER` | `2.5` | 거래량 급증 감지 배수 (20MA × 이 값 이상 시 급증 판정) |
--- ---

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,86 +1,85 @@
# Strategy Parameter Sweep Plan # 전략 파라미터 스윕 계획
**Date**: 2026-03-06 **날짜**: 2026-03-06
**Status**: Completed **상태**: 완료
## Goal ## 목표
Find profitable parameter combinations for the base technical indicator strategy (ML OFF) using walk-forward backtesting, targeting PF >= 1.0 as foundation for ML redesign. Walk-Forward 백테스트를 활용하여 기본 기술 지표 전략(ML OFF)의 수익성 높은 파라미터 조합을 탐색하고, PF >= 1.0을 ML 재설계의 기반으로 확보한다.
## Background ## 배경
Walk-forward backtest revealed the current XRP strategy is unprofitable (PF 0.71, -641 PnL). The strategy parameter sweep systematically tests 324 combinations of 5 parameters to find profitable regimes. Walk-Forward 백테스트 결과 현재 XRP 전략이 비수익적(PF 0.71, -641 PnL)으로 확인되었다. 전략 파라미터 스윕은 5개 파라미터의 324개 조합을 체계적으로 테스트하여 수익 구간을 탐색한다.
## Parameters Swept ## 스윕 파라미터
| Parameter | Values | Description | | 파라미터 | | 설명 |
| ------------------- | ------------- | ----------------------------------------- | | ------------------- | ------------- | ------------------------------------------- |
| `atr_sl_mult` | 1.0, 1.5, 2.0 | Stop-loss ATR multiplier | | `atr_sl_mult` | 1.0, 1.5, 2.0 | 손절 ATR 배수 |
| `atr_tp_mult` | 2.0, 3.0, 4.0 | Take-profit ATR multiplier | | `atr_tp_mult` | 2.0, 3.0, 4.0 | 익절 ATR 배수 |
| `signal_threshold` | 3, 4, 5 | Min weighted indicator score for entry | | `signal_threshold` | 3, 4, 5 | 진입을 위한 최소 가중치 지표 점수 |
| `adx_threshold` | 0, 20, 25, 30 | ADX filter (0=disabled, N=require ADX>=N) | | `adx_threshold` | 0, 20, 25, 30 | ADX 필터 (0=비활성, N=ADX>=N 필요) |
| `volume_multiplier` | 1.5, 2.0, 2.5 | Volume surge detection multiplier | | `volume_multiplier` | 1.5, 2.0, 2.5 | 거래량 급증 감지 배수 |
Total combinations: 3 x 3 x 3 x 4 x 3 = **324** 총 조합: 3 x 3 x 3 x 4 x 3 = **324**
## Implementation ## 구현
### Files Modified ### 수정된 파일
- `src/indicators.py``get_signal()` accepts `signal_threshold`, `adx_threshold`, `volume_multiplier` params - `src/indicators.py``get_signal()` `signal_threshold`, `adx_threshold`, `volume_multiplier` 파라미터 추가
- `src/dataset_builder.py``_calc_signals()` accepts same params for vectorized computation - `src/dataset_builder.py``_calc_signals()`에 동일 파라미터를 받아 벡터화 계산에 적용
- `src/backtester.py``BacktestConfig` includes strategy params; `WalkForwardBacktester` propagates them to test folds - `src/backtester.py``BacktestConfig`에 전략 파라미터 포함; `WalkForwardBacktester`가 테스트 폴드에 전파
### Files Created ### 신규 생성 파일
- `scripts/strategy_sweep.py`CLI tool for parameter grid sweep - `scripts/strategy_sweep.py`파라미터 그리드 스윕 CLI 도구
### Bug Fix ### 버그 수정
- `WalkForwardBacktester` was not passing `signal_threshold`, `adx_threshold`, `volume_multiplier`, or `use_ml` to fold `BacktestConfig`. All signal params were silently using defaults, making ADX/volume/threshold sweeps have zero effect. - `WalkForwardBacktester` `signal_threshold`, `adx_threshold`, `volume_multiplier`, `use_ml`을 폴드 `BacktestConfig`에 전달하지 않는 버그 수정. 모든 신호 파라미터가 기본값으로 적용되어 ADX/거래량/임계값 스윕이 효과 없이 실행되고 있었음.
## Results (XRPUSDT, Walk-Forward 3/1) ## 결과 (XRPUSDT, Walk-Forward 3/1)
### Top 10 Combinations ### 상위 10개 조합
| Rank | SL×ATR | TP×ATR | Signal | ADX | Vol | Trades | WinRate | PF | MDD | PnL | Sharpe | | 순위 | SL×ATR | TP×ATR | 신호 | ADX | 거래량 | 거래 수 | 승률 | PF | MDD | PnL | 샤프 |
| ---- | ------ | ------ | ------ | --- | --- | ------ | ------- | ---- | ----- | ---- | ------ | | ---- | ------ | ------ | ---- | --- | ------ | ------- | ------- | ---- | ----- | ---- | ---- |
| 1 | 1.5 | 4.0 | 3 | 30 | 2.5 | 19 | 52.6% | 2.39 | 7.0% | +469 | 61.0 | | 1 | 1.5 | 4.0 | 3 | 30 | 2.5 | 19 | 52.6% | 2.39 | 7.0% | +469 | 61.0 |
| 2 | 1.5 | 2.0 | 3 | 30 | 2.5 | 19 | 68.4% | 2.23 | 6.5% | +282 | 61.2 | | 2 | 1.5 | 2.0 | 3 | 30 | 2.5 | 19 | 68.4% | 2.23 | 6.5% | +282 | 61.2 |
| 3 | 1.0 | 2.0 | 3 | 30 | 2.5 | 19 | 57.9% | 1.98 | 5.0% | +213 | 50.8 | | 3 | 1.0 | 2.0 | 3 | 30 | 2.5 | 19 | 57.9% | 1.98 | 5.0% | +213 | 50.8 |
| 4 | 1.0 | 4.0 | 3 | 30 | 2.5 | 19 | 36.8% | 1.80 | 7.7% | +248 | 37.1 | | 4 | 1.0 | 4.0 | 3 | 30 | 2.5 | 19 | 36.8% | 1.80 | 7.7% | +248 | 37.1 |
| 5 | 1.5 | 3.0 | 3 | 30 | 2.5 | 19 | 52.6% | 1.76 | 10.1% | +258 | 40.9 | | 5 | 1.5 | 3.0 | 3 | 30 | 2.5 | 19 | 52.6% | 1.76 | 10.1% | +258 | 40.9 |
| 6 | 1.5 | 4.0 | 3 | 25 | 2.5 | 28 | 42.9% | 1.75 | 13.1% | +381 | 36.8 | | 6 | 1.5 | 4.0 | 3 | 25 | 2.5 | 28 | 42.9% | 1.75 | 13.1% | +381 | 36.8 |
| 7 | 2.0 | 4.0 | 3 | 30 | 1.5 | 39 | 48.7% | 1.67 | 16.9% | +572 | 35.3 | | 7 | 2.0 | 4.0 | 3 | 30 | 1.5 | 39 | 48.7% | 1.67 | 16.9% | +572 | 35.3 |
| 8 | 1.0 | 2.0 | 3 | 25 | 2.5 | 28 | 50.0% | 1.64 | 5.8% | +205 | 35.7 | | 8 | 1.0 | 2.0 | 3 | 25 | 2.5 | 28 | 50.0% | 1.64 | 5.8% | +205 | 35.7 |
| 9 | 1.5 | 2.0 | 3 | 25 | 2.5 | 28 | 57.1% | 1.62 | 10.3% | +229 | 35.7 | | 9 | 1.5 | 2.0 | 3 | 25 | 2.5 | 28 | 57.1% | 1.62 | 10.3% | +229 | 35.7 |
| 10 | 2.0 | 2.0 | 3 | 25 | 2.5 | 27 | 66.7% | 1.57 | 12.0% | +217 | 33.3 | | 10 | 2.0 | 2.0 | 3 | 25 | 2.5 | 27 | 66.7% | 1.57 | 12.0% | +217 | 33.3 |
### Current Production (Rank 93/324) ### 현재 프로덕션 (324개 중 93위)
| SL×ATR | TP×ATR | Signal | ADX | Vol | Trades | WinRate | PF | MDD | PnL | | SL×ATR | TP×ATR | 신호 | ADX | 거래량 | 거래 수 | 승률 | PF | MDD | PnL |
| ------ | ------ | ------ | --- | --- | ------ | ------- | ---- | ----- | ---- | | ------ | ------ | ---- | --- | ------ | ------- | ------- | ---- | ----- | ---- |
| 1.5 | 3.0 | 3 | 0 | 1.5 | 118 | 30.5% | 0.71 | 65.9% | -641 | | 1.5 | 3.0 | 3 | 0 | 1.5 | 118 | 30.5% | 0.71 | 65.9% | -641 |
### Key Findings ### 핵심 발견 사항
1. **ADX filter is the single most impactful parameter.** All top 10 results use ADX >= 25, with ADX=30 dominating the top 5. This filters out sideways/ranging markets where signals are noise. 1. **ADX 필터가 가장 영향력 있는 단일 파라미터.** 상위 10개 결과 모두 ADX >= 25를 사용하며, 상위 5개는 ADX=30이 지배적. 횡보/박스권 시장에서 노이즈 신호를 필터링한다.
2. **Volume multiplier 2.5 dominates.** Higher volume thresholds ensure entries only on strong conviction (genuine breakouts vs. noise). 2. **거래량 배수 2.5가 지배적.** 높은 거래량 임계값은 진정한 돌파에서만 진입을 보장한다 (노이즈 대비 실질 돌파).
3. **Signal threshold 3 is optimal.** Higher thresholds (4, 5) produced too few trades or zero trades in most ADX-filtered regimes. 3. **신호 임계값 3이 최적.** 더 높은 임계값(4, 5)은 대부분의 ADX 필터링 구간에서 거래가 너무 적거나 0건이었다.
4. **SL/TP ratios matter less than entry filters.** The top results span all SL/TP combos, but all share ADX=25-30 + Vol=2.5. 4. **SL/TP 비율보다 진입 필터가 더 중요.** 상위 결과는 모든 SL/TP 조합에 걸쳐 있지만, 모두 ADX=25-30 + Vol=2.5를 공유한다.
5. **Trade count drops significantly with filters.** Top combos have 19-39 trades vs. 118 for current. Fewer but higher quality entries. 5. **필터 적용 시 거래 수가 크게 감소.** 상위 조합은 19-39건 vs 현재 118건. 적지만 높은 품질의 진입.
6. **41 combinations achieved PF >= 1.0** out of 324 total (12.7%). 6. **324개 중 41개 조합이 PF >= 1.0 달성** (12.7%).
## Recommended Next Steps ## 권장 다음 단계
1. **Update production defaults**: ADX=25, volume_multiplier=2.0 as a conservative choice (more trades than ADX=30)
2. **Validate on TRXUSDT and DOGEUSDT** to confirm ADX filter is not XRP-specific
3. **Retrain ML models** with updated strategy params — the ML filter should now have a profitable base to improve upon
4. **Fine-tune sweep** around the profitable zone: ADX [25-35], Vol [2.0-3.0]
1. **프로덕션 기본값 업데이트**: ADX=25, volume_multiplier=2.0을 보수적 선택으로 적용 (ADX=30보다 더 많은 거래 확보)
2. **TRXUSDT, DOGEUSDT에서 검증**: ADX 필터가 XRP에만 특화된 것이 아닌지 확인
3. **ML 모델 재학습**: 업데이트된 전략 파라미터로 — ML 필터가 수익성 있는 기반 위에서 개선 가능
4. **수익 구간 주변 세밀 스윕**: ADX [25-35], Vol [2.0-3.0]

View File

@@ -1,22 +1,22 @@
# Weekly Strategy Report Implementation Plan # 주간 전략 리포트 구현 계획
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Automatically measure strategy performance weekly, track trends, detect degradation, and send Discord reports. **Goal:** 매주 전략 성능을 자동 측정하고, 추이를 추적하며, 성능 저하를 감지하고, Discord 리포트를 전송한다.
**Architecture:** Single script `scripts/weekly_report.py` that orchestrates data fetch (subprocess), Walk-Forward backtest (import), log parsing (reuse `dashboard/api/log_parser.py`), trend analysis (read previous `results/weekly/*.json`), optional parameter sweep (import), and Discord notification (import `src/notifier.py`). No changes to production bot code. **Architecture:** 단일 스크립트 `scripts/weekly_report.py`가 데이터 수집(subprocess), Walk-Forward 백테스트(import), 로그 파싱(`dashboard/api/log_parser.py` 재사용), 추이 분석(기존 `results/weekly/*.json` 읽기), 선택적 파라미터 스윕(import), Discord 알림(`src/notifier.py` import)을 오케스트레이션한다. 프로덕션 봇 코드 변경 없음.
**Tech Stack:** Python 3.12, existing backtester/sweep/notifier/log_parser modules, subprocess for `fetch_history.py`, httpx for Discord. **Tech Stack:** Python 3.12, 기존 backtester/sweep/notifier/log_parser 모듈, `fetch_history.py` subprocess 호출, Discord용 httpx.
--- ---
### Task 1: Create weekly report core — data fetch + backtest ### Task 1: 주간 리포트 코어 — 데이터 수집 + 백테스트
**Files:** **Files:**
- Create: `scripts/weekly_report.py` - Create: `scripts/weekly_report.py`
- Test: `tests/test_weekly_report.py` - Test: `tests/test_weekly_report.py`
**Step 1: Write the failing test for `fetch_latest_data()`** **Step 1: `fetch_latest_data()` 실패 테스트 작성**
```python ```python
# tests/test_weekly_report.py # tests/test_weekly_report.py
@@ -45,15 +45,15 @@ def test_fetch_latest_data_calls_subprocess():
assert "35" in args_0 assert "35" in args_0
``` ```
**Step 2: Run test to verify it fails** **Step 2: 테스트 실행하여 실패 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py::test_fetch_latest_data_calls_subprocess -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py::test_fetch_latest_data_calls_subprocess -v`
Expected: FAIL — `ModuleNotFoundError: No module named 'scripts.weekly_report'` Expected: FAIL — `ModuleNotFoundError: No module named 'scripts.weekly_report'`
**Step 3: Write the failing test for `run_backtest()`** **Step 3: `run_backtest()` 실패 테스트 작성**
```python ```python
# tests/test_weekly_report.py (append) # tests/test_weekly_report.py (추가)
def test_run_backtest_returns_summary(): def test_run_backtest_returns_summary():
"""run_backtest가 심볼별 WF 백테스트를 실행하고 결과를 반환하는지 확인.""" """run_backtest가 심볼별 WF 백테스트를 실행하고 결과를 반환하는지 확인."""
from scripts.weekly_report import run_backtest from scripts.weekly_report import run_backtest
@@ -91,7 +91,7 @@ def test_run_backtest_returns_summary():
assert result["summary"]["total_trades"] == 27 assert result["summary"]["total_trades"] == 27
``` ```
**Step 4: Write minimal implementation** **Step 4: 최소 구현 작성**
```python ```python
#!/usr/bin/env python3 #!/usr/bin/env python3
@@ -167,30 +167,30 @@ def run_backtest(
return wf.run() return wf.run()
``` ```
**Step 5: Run tests to verify they pass** **Step 5: 테스트 실행하여 통과 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v`
Expected: 2 PASS Expected: 2 PASS
**Step 6: Commit** **Step 6: 커밋**
```bash ```bash
git add scripts/weekly_report.py tests/test_weekly_report.py git add scripts/weekly_report.py tests/test_weekly_report.py
git commit -m "feat(weekly-report): add data fetch and WF backtest core" git commit -m "feat(weekly-report): 데이터 수집 및 WF 백테스트 코어 추가"
``` ```
--- ---
### Task 2: Add live trade log parsing ### Task 2: 실전 트레이드 로그 파싱 추가
**Files:** **Files:**
- Modify: `scripts/weekly_report.py` - Modify: `scripts/weekly_report.py`
- Test: `tests/test_weekly_report.py` - Test: `tests/test_weekly_report.py`
**Step 1: Write the failing test** **Step 1: 실패 테스트 작성**
```python ```python
# tests/test_weekly_report.py (append) # tests/test_weekly_report.py (추가)
def test_parse_live_trades_extracts_entries(tmp_path): def test_parse_live_trades_extracts_entries(tmp_path):
"""봇 로그에서 진입/청산 패턴을 파싱하여 트레이드 리스트를 반환.""" """봇 로그에서 진입/청산 패턴을 파싱하여 트레이드 리스트를 반환."""
from scripts.weekly_report import parse_live_trades from scripts.weekly_report import parse_live_trades
@@ -218,14 +218,14 @@ def test_parse_live_trades_empty_log(tmp_path):
assert trades == [] assert trades == []
``` ```
**Step 2: Run test to verify it fails** **Step 2: 테스트 실행하여 실패 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py::test_parse_live_trades_extracts_entries -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py::test_parse_live_trades_extracts_entries -v`
Expected: FAIL — `ImportError: cannot import name 'parse_live_trades'` Expected: FAIL — `ImportError: cannot import name 'parse_live_trades'`
**Step 3: Write implementation** **Step 3: 구현 작성**
Append to `scripts/weekly_report.py`: `scripts/weekly_report.py`에 추가:
```python ```python
import re import re
@@ -289,30 +289,30 @@ def parse_live_trades(log_path: str, days: int = 7) -> list[dict]:
return closed_trades return closed_trades
``` ```
**Step 4: Run tests to verify they pass** **Step 4: 테스트 실행하여 통과 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v`
Expected: 4 PASS Expected: 4 PASS
**Step 5: Commit** **Step 5: 커밋**
```bash ```bash
git add scripts/weekly_report.py tests/test_weekly_report.py git add scripts/weekly_report.py tests/test_weekly_report.py
git commit -m "feat(weekly-report): add live trade log parser" git commit -m "feat(weekly-report): 실전 트레이드 로그 파서 추가"
``` ```
--- ---
### Task 3: Add trend tracking (read previous reports) ### Task 3: 추이 추적 (이전 리포트 읽기) 추가
**Files:** **Files:**
- Modify: `scripts/weekly_report.py` - Modify: `scripts/weekly_report.py`
- Test: `tests/test_weekly_report.py` - Test: `tests/test_weekly_report.py`
**Step 1: Write the failing test** **Step 1: 실패 테스트 작성**
```python ```python
# tests/test_weekly_report.py (append) # tests/test_weekly_report.py (추가)
def test_load_trend_reads_previous_reports(tmp_path): def test_load_trend_reads_previous_reports(tmp_path):
"""이전 주간 리포트를 읽어 PF/승률/MDD 추이를 반환.""" """이전 주간 리포트를 읽어 PF/승률/MDD 추이를 반환."""
from scripts.weekly_report import load_trend from scripts.weekly_report import load_trend
@@ -349,14 +349,14 @@ def test_load_trend_empty_dir(tmp_path):
assert trend["pf_declining_3w"] is False assert trend["pf_declining_3w"] is False
``` ```
**Step 2: Run test to verify it fails** **Step 2: 테스트 실행하여 실패 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py::test_load_trend_reads_previous_reports -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py::test_load_trend_reads_previous_reports -v`
Expected: FAIL Expected: FAIL
**Step 3: Write implementation** **Step 3: 구현 작성**
Append to `scripts/weekly_report.py`: `scripts/weekly_report.py`에 추가:
```python ```python
WEEKLY_DIR = Path("results/weekly") WEEKLY_DIR = Path("results/weekly")
@@ -396,30 +396,30 @@ def load_trend(report_dir: str, weeks: int = 4) -> dict:
} }
``` ```
**Step 4: Run tests to verify they pass** **Step 4: 테스트 실행하여 통과 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v`
Expected: 6 PASS Expected: 6 PASS
**Step 5: Commit** **Step 5: 커밋**
```bash ```bash
git add scripts/weekly_report.py tests/test_weekly_report.py git add scripts/weekly_report.py tests/test_weekly_report.py
git commit -m "feat(weekly-report): add trend tracking from previous reports" git commit -m "feat(weekly-report): 이전 리포트 추이 추적 추가"
``` ```
--- ---
### Task 4: Add ML re-trigger check + degradation sweep ### Task 4: ML 재트리거 체크 + 성능 저하 스윕 추가
**Files:** **Files:**
- Modify: `scripts/weekly_report.py` - Modify: `scripts/weekly_report.py`
- Test: `tests/test_weekly_report.py` - Test: `tests/test_weekly_report.py`
**Step 1: Write the failing tests** **Step 1: 실패 테스트 작성**
```python ```python
# tests/test_weekly_report.py (append) # tests/test_weekly_report.py (추가)
def test_check_ml_trigger_all_met(): def test_check_ml_trigger_all_met():
"""3개 조건 모두 충족 시 recommend=True.""" """3개 조건 모두 충족 시 recommend=True."""
from scripts.weekly_report import check_ml_trigger from scripts.weekly_report import check_ml_trigger
@@ -473,14 +473,14 @@ def test_run_degradation_sweep_called_when_pf_low():
assert alternatives[0]["summary"]["profit_factor"] >= alternatives[1]["summary"]["profit_factor"] assert alternatives[0]["summary"]["profit_factor"] >= alternatives[1]["summary"]["profit_factor"]
``` ```
**Step 2: Run tests to verify they fail** **Step 2: 테스트 실행하여 실패 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -k "ml_trigger or degradation" -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -k "ml_trigger or degradation" -v`
Expected: FAIL Expected: FAIL
**Step 3: Write implementation** **Step 3: 구현 작성**
Append to `scripts/weekly_report.py`: `scripts/weekly_report.py`에 추가:
```python ```python
from scripts.strategy_sweep import ( from scripts.strategy_sweep import (
@@ -538,30 +538,30 @@ def run_degradation_sweep(
return results[:top_n] return results[:top_n]
``` ```
**Step 4: Run tests to verify they pass** **Step 4: 테스트 실행하여 통과 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v`
Expected: 9 PASS Expected: 9 PASS
**Step 5: Commit** **Step 5: 커밋**
```bash ```bash
git add scripts/weekly_report.py tests/test_weekly_report.py git add scripts/weekly_report.py tests/test_weekly_report.py
git commit -m "feat(weekly-report): add ML trigger check and degradation sweep" git commit -m "feat(weekly-report): ML 트리거 체크 및 성능 저하 스윕 추가"
``` ```
--- ---
### Task 5: Add Discord report formatting + sending ### Task 5: Discord 리포트 포맷팅 + 전송 추가
**Files:** **Files:**
- Modify: `scripts/weekly_report.py` - Modify: `scripts/weekly_report.py`
- Test: `tests/test_weekly_report.py` - Test: `tests/test_weekly_report.py`
**Step 1: Write the failing test** **Step 1: 실패 테스트 작성**
```python ```python
# tests/test_weekly_report.py (append) # tests/test_weekly_report.py (추가)
def test_format_report_normal(): def test_format_report_normal():
"""정상 상태(PF >= 1.0) 리포트 포맷.""" """정상 상태(PF >= 1.0) 리포트 포맷."""
from scripts.weekly_report import format_report from scripts.weekly_report import format_report
@@ -621,7 +621,7 @@ def test_format_report_degraded():
text = format_report(report_data) text = format_report(report_data)
assert "0.87" in text assert "0.87" in text
assert "ML" in text assert "ML" in text
assert "1.15" in text # sweep alternative assert "1.15" in text # 스윕 대안
def test_send_report_uses_notifier(): def test_send_report_uses_notifier():
@@ -634,14 +634,14 @@ def test_send_report_uses_notifier():
instance._send.assert_called_once_with("test report content") instance._send.assert_called_once_with("test report content")
``` ```
**Step 2: Run tests to verify they fail** **Step 2: 테스트 실행하여 실패 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -k "format_report or send_report" -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -k "format_report or send_report" -v`
Expected: FAIL Expected: FAIL
**Step 3: Write implementation** **Step 3: 구현 작성**
Append to `scripts/weekly_report.py`: `scripts/weekly_report.py`에 추가:
```python ```python
import os import os
@@ -657,10 +657,10 @@ def format_report(data: dict) -> str:
status = "" status = ""
if pf < 1.0: if pf < 1.0:
status = " \U0001F6A8 손실 구간" status = " 🚨 손실 구간"
lines = [ lines = [
f"\U0001F4CA 주간 전략 리포트 ({d})", f"📊 주간 전략 리포트 ({d})",
"", "",
f"[현재 성능 — Walk-Forward 백테스트]", f"[현재 성능 — Walk-Forward 백테스트]",
f" 합산 PF: {pf_str} | 승률: {bt['win_rate']:.0f}% | MDD: {bt['max_drawdown_pct']:.0f}%{status}", f" 합산 PF: {pf_str} | 승률: {bt['win_rate']:.0f}% | MDD: {bt['max_drawdown_pct']:.0f}%{status}",
@@ -689,7 +689,7 @@ def format_report(data: dict) -> str:
trend = data["trend"] trend = data["trend"]
if trend["pf"]: if trend["pf"]:
pf_trend = "".join(f"{v:.2f}" for v in trend["pf"]) pf_trend = "".join(f"{v:.2f}" for v in trend["pf"])
warn = " \u26A0 하락 추세" if trend["pf_declining_3w"] else "" warn = " 하락 추세" if trend["pf_declining_3w"] else ""
lines += ["", f"[추이 (최근 {len(trend['pf'])}주)]", f" PF: {pf_trend}{warn}"] lines += ["", f"[추이 (최근 {len(trend['pf'])}주)]", f" PF: {pf_trend}{warn}"]
if trend["win_rate"]: if trend["win_rate"]:
wr_trend = "".join(f"{v:.0f}%" for v in trend["win_rate"]) wr_trend = "".join(f"{v:.0f}%" for v in trend["win_rate"])
@@ -709,7 +709,7 @@ def format_report(data: dict) -> str:
f" {'' if cond['pf_declining_3w'] else ''} PF 3주 연속 하락: {'예 ⚠' if cond['pf_declining_3w'] else '아니오'}", f" {'' if cond['pf_declining_3w'] else ''} PF 3주 연속 하락: {'예 ⚠' if cond['pf_declining_3w'] else '아니오'}",
] ]
if ml["recommend"]: if ml["recommend"]:
lines.append(f"\U0001F514 ML 재학습 권장! ({ml['met_count']}/3 충족)") lines.append(f"🔔 ML 재학습 권장! ({ml['met_count']}/3 충족)")
else: else:
lines.append(f" → ML 재도전 시점: 아직 아님 ({ml['met_count']}/3 충족)") lines.append(f" → ML 재도전 시점: 아직 아님 ({ml['met_count']}/3 충족)")
@@ -725,7 +725,7 @@ def format_report(data: dict) -> str:
diff = apf - pf diff = apf - pf
lines.append(f" 대안 {i+1}: {_param_str(alt['params'])} → PF {apf_str} ({diff:+.2f})") lines.append(f" 대안 {i+1}: {_param_str(alt['params'])} → PF {apf_str} ({diff:+.2f})")
lines.append("") lines.append("")
lines.append(" \u26A0 자동 적용되지 않음. 검토 후 승인 필요.") lines.append(" 자동 적용되지 않음. 검토 후 승인 필요.")
elif pf >= 1.0: elif pf >= 1.0:
lines += ["", "[파라미터 스윕]", " 현재 파라미터가 최적 — 스윕 불필요"] lines += ["", "[파라미터 스윕]", " 현재 파라미터가 최적 — 스윕 불필요"]
@@ -748,30 +748,30 @@ def send_report(content: str, webhook_url: str | None = None) -> None:
logger.info("Discord 리포트 전송 완료") logger.info("Discord 리포트 전송 완료")
``` ```
**Step 4: Run tests to verify they pass** **Step 4: 테스트 실행하여 통과 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v`
Expected: 12 PASS Expected: 12 PASS
**Step 5: Commit** **Step 5: 커밋**
```bash ```bash
git add scripts/weekly_report.py tests/test_weekly_report.py git add scripts/weekly_report.py tests/test_weekly_report.py
git commit -m "feat(weekly-report): add Discord report formatting and sending" git commit -m "feat(weekly-report): Discord 리포트 포맷팅 및 전송 추가"
``` ```
--- ---
### Task 6: Add main orchestration + CLI + JSON save ### Task 6: 메인 오케스트레이션 + CLI + JSON 저장 추가
**Files:** **Files:**
- Modify: `scripts/weekly_report.py` - Modify: `scripts/weekly_report.py`
- Test: `tests/test_weekly_report.py` - Test: `tests/test_weekly_report.py`
**Step 1: Write the failing test** **Step 1: 실패 테스트 작성**
```python ```python
# tests/test_weekly_report.py (append) # tests/test_weekly_report.py (추가)
def test_generate_report_orchestration(tmp_path): def test_generate_report_orchestration(tmp_path):
"""generate_report가 모든 단계를 조합하여 리포트 dict를 반환.""" """generate_report가 모든 단계를 조합하여 리포트 dict를 반환."""
from scripts.weekly_report import generate_report from scripts.weekly_report import generate_report
@@ -819,14 +819,14 @@ def test_save_report_creates_json(tmp_path):
assert loaded["date"] == "2026-03-07" assert loaded["date"] == "2026-03-07"
``` ```
**Step 2: Run tests to verify they fail** **Step 2: 테스트 실행하여 실패 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -k "generate_report or save_report" -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -k "generate_report or save_report" -v`
Expected: FAIL Expected: FAIL
**Step 3: Write implementation** **Step 3: 구현 작성**
Append to `scripts/weekly_report.py`: `scripts/weekly_report.py`에 추가:
```python ```python
import numpy as np import numpy as np
@@ -984,31 +984,31 @@ if __name__ == "__main__":
main() main()
``` ```
**Step 4: Run tests to verify they pass** **Step 4: 테스트 실행하여 통과 확인**
Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v` Run: `source .venv/bin/activate && pytest tests/test_weekly_report.py -v`
Expected: 14 PASS Expected: 14 PASS
**Step 5: Run existing test suite to verify no regressions** **Step 5: 기존 테스트 스위트 실행하여 회귀 없음 확인**
Run: `source .venv/bin/activate && bash scripts/run_tests.sh` Run: `source .venv/bin/activate && bash scripts/run_tests.sh`
Expected: 121+ passed (existing) + 14 new = 135+ passed Expected: 121+ 기존 통과 + 14 신규 = 135+ 통과
**Step 6: Commit** **Step 6: 커밋**
```bash ```bash
git add scripts/weekly_report.py tests/test_weekly_report.py git add scripts/weekly_report.py tests/test_weekly_report.py
git commit -m "feat(weekly-report): add main orchestration, CLI, JSON save" git commit -m "feat(weekly-report): 메인 오케스트레이션, CLI, JSON 저장 추가"
``` ```
--- ---
### Task 7: Manual smoke test + crontab guide ### Task 7: 수동 스모크 테스트 + 크론탭 가이드
**Files:** **Files:**
- No new files - 신규 파일 없음
**Step 1: Dry run (skip fetch, skip Discord)** **Step 1: 드라이 런 (데이터 수집 스킵, Discord 스킵)**
Run: Run:
```bash ```bash
@@ -1017,19 +1017,19 @@ source .venv/bin/activate && python scripts/weekly_report.py --skip-fetch --date
Expected: 리포트가 터미널에 출력되고 `results/weekly/report_2026-03-07.json` 저장됨. Expected: 리포트가 터미널에 출력되고 `results/weekly/report_2026-03-07.json` 저장됨.
**Step 2: Verify saved JSON** **Step 2: 저장된 JSON 확인**
Run: `cat results/weekly/report_2026-03-07.json | python -m json.tool | head -30` Run: `cat results/weekly/report_2026-03-07.json | python -m json.tool | head -30`
Expected: valid JSON with date, backtest, live_trades, trend, ml_trigger keys Expected: date, backtest, live_trades, trend, ml_trigger 키가 포함된 유효한 JSON
**Step 3: Commit final state** **Step 3: 최종 상태 커밋**
```bash ```bash
git add results/weekly/.gitkeep git add results/weekly/.gitkeep
git commit -m "chore: add results/weekly directory" git commit -m "chore: results/weekly 디렉토리 추가"
``` ```
**Step 4: Document crontab setup** **Step 4: 크론탭 설정 문서화**
프로덕션 서버에서: 프로덕션 서버에서:
```bash ```bash
@@ -1041,31 +1041,31 @@ crontab -e
--- ---
### Task 8: Update CLAUDE.md plan history ### Task 8: CLAUDE.md 플랜 히스토리 업데이트
**Files:** **Files:**
- Modify: `CLAUDE.md` - Modify: `CLAUDE.md`
**Step 1: Add plan entry to history table** **Step 1: 히스토리 테이블에 플랜 항목 추가**
Add to the plan history table: 플랜 히스토리 테이블에 추가:
``` ```
| 2026-03-07 | `weekly-report` (plan) | Completed | | 2026-03-07 | `weekly-report` (plan) | Completed |
``` ```
**Step 2: Add weekly report commands to Common Commands section** **Step 2: Common Commands 섹션에 주간 리포트 명령어 추가**
```bash ```bash
# Weekly strategy report (manual) # 주간 전략 리포트 (수동)
python scripts/weekly_report.py --skip-fetch python scripts/weekly_report.py --skip-fetch
# Weekly report with data refresh # 주간 리포트 (데이터 새로고침 포함)
python scripts/weekly_report.py python scripts/weekly_report.py
``` ```
**Step 3: Commit** **Step 3: 커밋**
```bash ```bash
git add CLAUDE.md git add CLAUDE.md
git commit -m "docs: add weekly-report to plan history and commands" git commit -m "docs: 주간 리포트를 플랜 히스토리 및 명령어에 추가"
``` ```

View File

@@ -0,0 +1,92 @@
{
"date": "2026-03-07",
"backtest": {
"summary": {
"profit_factor": 1.24,
"win_rate": 61.4,
"max_drawdown_pct": 17.1,
"total_trades": 88,
"total_pnl": 379.4
},
"per_symbol": {
"XRPUSDT": {
"total_trades": 27,
"total_pnl": 217.0703,
"return_pct": 21.71,
"win_rate": 66.67,
"avg_win": 33.2223,
"avg_loss": -42.3256,
"profit_factor": 1.57,
"max_drawdown_pct": 11.99,
"sharpe_ratio": 33.32,
"total_fees": 102.7825,
"close_reasons": {
"STOP_LOSS": 9,
"TAKE_PROFIT": 18
}
},
"TRXUSDT": {
"total_trades": 25,
"total_pnl": 72.3058,
"return_pct": 7.23,
"win_rate": 64.0,
"avg_win": 20.3593,
"avg_loss": -28.1603,
"profit_factor": 1.29,
"max_drawdown_pct": 7.66,
"sharpe_ratio": 15.17,
"total_fees": 97.1591,
"close_reasons": {
"STOP_LOSS": 9,
"TAKE_PROFIT": 16
}
},
"DOGEUSDT": {
"total_trades": 36,
"total_pnl": 90.019,
"return_pct": 9.0,
"win_rate": 55.56,
"avg_win": 52.4749,
"avg_loss": -59.9675,
"profit_factor": 1.09,
"max_drawdown_pct": 17.14,
"sharpe_ratio": 6.64,
"total_fees": 139.8283,
"close_reasons": {
"STOP_LOSS": 15,
"TAKE_PROFIT": 20,
"REVERSE_SIGNAL": 1
}
}
}
},
"live_trades": {
"count": 0,
"net_pnl": 0,
"win_rate": 0
},
"trend": {
"pf": [
1.24
],
"win_rate": [
61.4
],
"mdd": [
17.1
],
"pf_declining_3w": false
},
"ml_trigger": {
"conditions": {
"cumulative_trades_enough": false,
"pf_below_1": false,
"pf_declining_3w": false
},
"met_count": 0,
"recommend": false,
"cumulative_trades": 88,
"threshold": 150
},
"sweep": null
}

View File

@@ -14,14 +14,16 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
import argparse import argparse
import json import json
import os import os
import re
import subprocess import subprocess
from datetime import date, timedelta from datetime import date, timedelta
import httpx
import numpy as np import numpy as np
from dotenv import load_dotenv
from loguru import logger from loguru import logger
load_dotenv()
from src.backtester import WalkForwardBacktester, WalkForwardConfig from src.backtester import WalkForwardBacktester, WalkForwardConfig
from src.notifier import DiscordNotifier from src.notifier import DiscordNotifier
@@ -76,55 +78,30 @@ def run_backtest(
return wf.run() return wf.run()
# ── 로그 파싱 패턴 ──────────────────────────────────────────────── # ── 대시보드 API에서 실전 트레이드 가져오기 ──────────────────────────
_RE_ENTRY = re.compile( DASHBOARD_API_URL = os.getenv("DASHBOARD_API_URL", "http://10.1.10.24:8000")
r"\[(\w+)\]\s+(LONG|SHORT)\s+진입:\s+가격=([\d.]+),\s+수량=([\d.]+),\s+SL=([\d.]+),\s+TP=([\d.]+)"
)
_RE_CLOSE = re.compile(
r"\[(\w+)\]\s+청산 감지\((\w+)\):\s+exit=([\d.]+),\s+rp=([\d.-]+),\s+commission=([\d.]+),\s+net_pnl=([\d.-]+)"
)
_RE_TIMESTAMP = re.compile(r"^(\d{4}-\d{2}-\d{2})\s")
def parse_live_trades(log_path: str, days: int = 7) -> list[dict]: def fetch_live_trades(api_url: str = DASHBOARD_API_URL, limit: int = 500) -> list[dict]:
"""봇 로그에서 최근 N일간의 진입/청산 기록을 파싱한다.""" """운영 LXC 대시보드 API에서 청산된 트레이드 내역을 가져온다."""
path = Path(log_path) try:
if not path.exists(): resp = httpx.get(f"{api_url}/api/trades", params={"limit": limit}, timeout=10)
resp.raise_for_status()
return resp.json().get("trades", [])
except Exception as e:
logger.warning(f"대시보드 API 트레이드 조회 실패: {e}")
return [] return []
cutoff = (date.today() - timedelta(days=days)).isoformat()
open_trades: dict[str, dict] = {}
closed_trades: list[dict] = []
for line in path.read_text().splitlines(): def fetch_live_stats(api_url: str = DASHBOARD_API_URL) -> dict:
m_ts = _RE_TIMESTAMP.match(line) """운영 LXC 대시보드 API에서 전체 통계를 가져온다."""
if m_ts and m_ts.group(1) < cutoff: try:
continue resp = httpx.get(f"{api_url}/api/stats", timeout=10)
resp.raise_for_status()
m = _RE_ENTRY.search(line) return resp.json()
if m: except Exception as e:
sym, side, price, qty, sl, tp = m.groups() logger.warning(f"대시보드 API 통계 조회 실패: {e}")
open_trades[sym] = { return {}
"symbol": sym, "side": side,
"entry_price": float(price), "quantity": float(qty),
"sl": float(sl), "tp": float(tp),
"entry_time": m_ts.group(1) if m_ts else "",
}
continue
m = _RE_CLOSE.search(line)
if m:
sym, reason, exit_price, rp, commission, net_pnl = m.groups()
trade = open_trades.pop(sym, {"symbol": sym, "side": "UNKNOWN"})
trade.update({
"close_reason": reason, "exit_price": float(exit_price),
"expected_pnl": float(rp), "commission": float(commission),
"net_pnl": float(net_pnl),
"exit_time": m_ts.group(1) if m_ts else "",
})
closed_trades.append(trade)
return closed_trades
# ── 추이 추적 ──────────────────────────────────────────────────── # ── 추이 추적 ────────────────────────────────────────────────────
@@ -373,11 +350,12 @@ def save_report(report: dict, report_dir: str) -> Path:
def generate_report( def generate_report(
symbols: list[str], symbols: list[str],
report_dir: str = str(WEEKLY_DIR), report_dir: str = str(WEEKLY_DIR),
log_path: str = "logs/bot.log",
report_date: date | None = None, report_date: date | None = None,
api_url: str | None = None,
) -> dict: ) -> dict:
"""전체 주간 리포트를 생성한다.""" """전체 주간 리포트를 생성한다."""
today = report_date or date.today() today = report_date or date.today()
dashboard_url = api_url or DASHBOARD_API_URL
# 1) Walk-Forward 백테스트 (심볼별) # 1) Walk-Forward 백테스트 (심볼별)
logger.info("백테스트 실행 중...") logger.info("백테스트 실행 중...")
@@ -416,28 +394,31 @@ def generate_report(
"total_pnl": round(combined_pnl, 2), "total_pnl": round(combined_pnl, 2),
} }
# 2) 실전 트레이드 파싱 # 2) 운영 대시보드 API에서 실전 트레이드 조회
logger.info("실전 로그 파싱 중...") logger.info(f"대시보드 API에서 실전 트레이드 조회 중... ({dashboard_url})")
live_trades_list = parse_live_trades(log_path, days=7) live_stats = fetch_live_stats(dashboard_url)
live_wins = sum(1 for t in live_trades_list if t.get("net_pnl", 0) > 0) live_trades_list = fetch_live_trades(dashboard_url)
live_pnl = sum(t.get("net_pnl", 0) for t in live_trades_list)
live_count = live_stats.get("total_trades", len(live_trades_list))
live_wins = live_stats.get("wins", 0)
live_pnl = live_stats.get("total_pnl", 0)
live_summary = { live_summary = {
"count": len(live_trades_list), "count": live_count,
"net_pnl": round(live_pnl, 2), "net_pnl": round(float(live_pnl), 2),
"win_rate": round(live_wins / len(live_trades_list) * 100, 1) if live_trades_list else 0, "win_rate": round(live_wins / live_count * 100, 1) if live_count > 0 else 0,
} }
# 3) 추이 로드 # 3) 추이 로드
trend = load_trend(report_dir) trend = load_trend(report_dir)
# 4) 누적 트레이드 수 # 4) 누적 트레이드 수 (실전 + 이전 리포트)
cumulative = combined_trades + len(live_trades_list) cumulative = live_count
rdir = Path(report_dir) rdir = Path(report_dir)
if rdir.exists(): if rdir.exists():
for rpath in sorted(rdir.glob("report_*.json")): for rpath in sorted(rdir.glob("report_*.json")):
try: try:
prev = json.loads(rpath.read_text()) prev = json.loads(rpath.read_text())
cumulative += prev.get("live_trades", {}).get("count", 0) cumulative = max(cumulative, prev.get("live_trades", {}).get("count", 0))
except (json.JSONDecodeError, KeyError): except (json.JSONDecodeError, KeyError):
pass pass

View File

@@ -48,33 +48,56 @@ def test_run_backtest_returns_summary():
assert result["summary"]["total_trades"] == 27 assert result["summary"]["total_trades"] == 27
def test_parse_live_trades_extracts_entries(tmp_path): def test_fetch_live_trades_from_api():
"""봇 로그에서 진입/청산 패턴을 파싱하여 트레이드 리스트를 반환.""" """대시보드 API에서 청산 트레이드를 가져오는지 확인."""
from scripts.weekly_report import parse_live_trades from scripts.weekly_report import fetch_live_trades
log_content = """2026-03-01 10:00:00.000 | INFO | src.bot:process_candle:42 - [XRPUSDT] LONG 진입: 가격=2.5000, 수량=100.0, SL=2.4000, TP=2.7000 mock_response = MagicMock()
2026-03-01 10:15:00.000 | INFO | src.bot:process_candle:42 - [XRPUSDT] 신호: HOLD | 현재가: 2.5500 USDT mock_response.json.return_value = {
2026-03-01 12:00:00.000 | INFO | src.user_data_stream:_handle_order:80 - [XRPUSDT] 청산 감지(TAKE_PROFIT): exit=2.7000, rp=20.0000, commission=0.2160, net_pnl=19.5680 "trades": [
""" {"symbol": "XRPUSDT", "direction": "LONG", "net_pnl": 19.568,
log_file = tmp_path / "bot.log" "commission": 0.216, "status": "CLOSED"},
log_file.write_text(log_content) ],
"total": 1,
}
mock_response.raise_for_status = MagicMock()
with patch("scripts.weekly_report.httpx.get", return_value=mock_response):
trades = fetch_live_trades("http://test:8000")
trades = parse_live_trades(str(log_file), days=7)
assert len(trades) == 1 assert len(trades) == 1
assert trades[0]["symbol"] == "XRPUSDT" assert trades[0]["symbol"] == "XRPUSDT"
assert trades[0]["side"] == "LONG"
assert trades[0]["net_pnl"] == pytest.approx(19.568) assert trades[0]["net_pnl"] == pytest.approx(19.568)
assert trades[0]["close_reason"] == "TAKE_PROFIT"
def test_parse_live_trades_empty_log(tmp_path): def test_fetch_live_trades_api_failure():
"""로그 파일이 없으면 빈 리스트 반환.""" """API 실패 시 빈 리스트 반환."""
from scripts.weekly_report import parse_live_trades from scripts.weekly_report import fetch_live_trades
with patch("scripts.weekly_report.httpx.get", side_effect=Exception("connection refused")):
trades = fetch_live_trades("http://unreachable:8000")
trades = parse_live_trades(str(tmp_path / "nonexistent.log"), days=7)
assert trades == [] assert trades == []
def test_fetch_live_stats_from_api():
"""대시보드 API에서 전체 통계를 가져오는지 확인."""
from scripts.weekly_report import fetch_live_stats
mock_response = MagicMock()
mock_response.json.return_value = {
"total_trades": 15, "wins": 9, "losses": 6,
"total_pnl": 42.5, "total_fees": 3.2,
}
mock_response.raise_for_status = MagicMock()
with patch("scripts.weekly_report.httpx.get", return_value=mock_response):
stats = fetch_live_stats("http://test:8000")
assert stats["total_trades"] == 15
assert stats["wins"] == 9
import json import json
from datetime import date, timedelta from datetime import date, timedelta
@@ -252,16 +275,16 @@ def test_generate_report_orchestration(tmp_path):
} }
with patch("scripts.weekly_report.run_backtest", return_value=mock_bt_result): with patch("scripts.weekly_report.run_backtest", return_value=mock_bt_result):
with patch("scripts.weekly_report.parse_live_trades", return_value=[]): with patch("scripts.weekly_report.fetch_live_stats", return_value={"total_trades": 0, "wins": 0, "total_pnl": 0}):
with patch("scripts.weekly_report.load_trend", return_value={ with patch("scripts.weekly_report.fetch_live_trades", return_value=[]):
"pf": [1.31], "win_rate": [48.0], "mdd": [9.0], "pf_declining_3w": False, with patch("scripts.weekly_report.load_trend", return_value={
}): "pf": [1.31], "win_rate": [48.0], "mdd": [9.0], "pf_declining_3w": False,
report = generate_report( }):
symbols=["XRPUSDT"], report = generate_report(
report_dir=str(tmp_path), symbols=["XRPUSDT"],
log_path=str(tmp_path / "bot.log"), report_dir=str(tmp_path),
report_date=date(2026, 3, 7), report_date=date(2026, 3, 7),
) )
assert report["date"] == "2026-03-07" assert report["date"] == "2026-03-07"
# PF는 avg_win/avg_loss에서 재계산됨 (GP=40*20=800, GL=48*10=480 → 1.67) # PF는 avg_win/avg_loss에서 재계산됨 (GP=40*20=800, GL=48*10=480 → 1.67)