테스트 강화 및 코드 품질 개선
This commit is contained in:
382
docs/improvements_implementation_summary.md
Normal file
382
docs/improvements_implementation_summary.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# 코드 리뷰 개선사항 구현 요약
|
||||
|
||||
**날짜**: 2025-12-09
|
||||
**참조**: `docs/code_review_report_v1.md`
|
||||
**제외 항목**: CRITICAL-004, HIGH-004, MEDIUM-004
|
||||
|
||||
---
|
||||
|
||||
## 📋 구현 완료 항목
|
||||
|
||||
### 🔴 CRITICAL Issues
|
||||
|
||||
#### [CRITICAL-001] API Rate Limiter 구현 ✅
|
||||
**문제**: Upbit API 초당 10회 제한을 멀티스레딩 환경에서 초과 위험
|
||||
|
||||
**해결**:
|
||||
- **파일**: `src/common.py`
|
||||
- 토큰 버킷 알고리즘 기반 `RateLimiter` 클래스 추가
|
||||
- 초당 8회 제한 (여유분 확보)
|
||||
- Thread-Safe 구현 (threading.Lock 사용)
|
||||
- `src/indicators.py`의 `fetch_ohlcv()`에 적용
|
||||
|
||||
**코드**:
|
||||
```python
|
||||
# src/common.py
|
||||
class RateLimiter:
|
||||
def __init__(self, max_calls: int = 8, period: float = 1.0):
|
||||
self.max_calls = max_calls
|
||||
self.period = period
|
||||
self.calls = deque()
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def acquire(self):
|
||||
# Rate Limit 초과 시 자동 대기
|
||||
...
|
||||
|
||||
api_rate_limiter = RateLimiter(max_calls=8, period=1.0)
|
||||
|
||||
# src/indicators.py
|
||||
from .common import api_rate_limiter
|
||||
api_rate_limiter.acquire() # API 호출 전
|
||||
df = pyupbit.get_ohlcv(...)
|
||||
```
|
||||
|
||||
**영향**: API 호출 제한 초과로 인한 418 에러 방지
|
||||
|
||||
---
|
||||
|
||||
#### [CRITICAL-002] 최고가 갱신 로직 구현 ✅
|
||||
**문제**: holdings.json의 max_price가 실시간 갱신되지 않아 트레일링 스톱 오작동
|
||||
|
||||
**해결**:
|
||||
- **파일**: `src/holdings.py`, `main.py`
|
||||
- `update_max_price()` 함수 추가 (Thread-Safe)
|
||||
- main.py의 손절/익절 체크 전 자동 갱신
|
||||
|
||||
**코드**:
|
||||
```python
|
||||
# src/holdings.py
|
||||
def update_max_price(symbol: str, current_price: float, holdings_file: str = HOLDINGS_FILE) -> None:
|
||||
with holdings_lock:
|
||||
holdings = load_holdings(holdings_file)
|
||||
if symbol in holdings:
|
||||
old_max = holdings[symbol].get("max_price", 0)
|
||||
if current_price > old_max:
|
||||
holdings[symbol]["max_price"] = current_price
|
||||
save_holdings(holdings, holdings_file)
|
||||
|
||||
# main.py
|
||||
for symbol in holdings.keys():
|
||||
current_price = get_current_price(symbol)
|
||||
update_max_price(symbol, current_price, HOLDINGS_FILE)
|
||||
```
|
||||
|
||||
**영향**: 정확한 트레일링 스톱 동작, 수익 극대화
|
||||
|
||||
---
|
||||
|
||||
#### [CRITICAL-003] Thread-Safe holdings 저장 ✅
|
||||
**문제**: Race Condition으로 holdings.json 데이터 손실 위험
|
||||
|
||||
**해결**:
|
||||
- **파일**: `src/holdings.py`
|
||||
- `save_holdings()`에 holdings_lock 적용 (이미 구현됨 확인)
|
||||
- 원자적 쓰기 (.tmp 파일 → rename)
|
||||
|
||||
**영향**: 멀티스레드 환경에서 데이터 무결성 보장
|
||||
|
||||
---
|
||||
|
||||
#### [CRITICAL-005] 부분 매수 지원 ✅
|
||||
**문제**: 잔고 9,000원일 때 10,000원 주문 시도 시 매수 불가 (5,000원 이상이면 가능한데)
|
||||
|
||||
**해결**:
|
||||
- **파일**: `src/order.py`
|
||||
- `place_buy_order_upbit()` 수정
|
||||
- 잔고 부족 시 가능한 만큼 매수 (최소 주문 금액 이상)
|
||||
|
||||
**코드**:
|
||||
```python
|
||||
# 잔고 확인 및 조정
|
||||
if not cfg.dry_run:
|
||||
krw_balance = upbit.get_balance("KRW")
|
||||
if krw_balance < amount_krw:
|
||||
if krw_balance >= min_order_value:
|
||||
logger.info("[%s] 잔고 부족, 부분 매수: %.0f원 → %.0f원", market, amount_krw, krw_balance)
|
||||
amount_krw = krw_balance
|
||||
else:
|
||||
return {"status": "skipped_insufficient_balance"}
|
||||
|
||||
# 수수료 고려 (0.05%)
|
||||
amount_krw = amount_krw * 0.9995
|
||||
```
|
||||
|
||||
**영향**: 기회 손실 방지, 자금 효율성 증가
|
||||
|
||||
---
|
||||
|
||||
### 🟡 HIGH Priority Issues
|
||||
|
||||
#### [HIGH-005] Circuit Breaker 임계값 조정 ✅
|
||||
**문제**: failure_threshold=5는 너무 높음
|
||||
|
||||
**해결**:
|
||||
- **파일**: `src/circuit_breaker.py`
|
||||
- failure_threshold: 5 → 3
|
||||
- recovery_timeout: 30초 → 300초 (5분)
|
||||
|
||||
**영향**: API 오류 발생 시 더 빠르게 차단, 계정 보호
|
||||
|
||||
---
|
||||
|
||||
#### [HIGH-007] Telegram 메시지 자동 분할 ✅
|
||||
**문제**: Telegram 메시지 4096자 제한 초과 시 전송 실패
|
||||
|
||||
**해결**:
|
||||
- **파일**: `src/notifications.py`
|
||||
- `send_telegram()` 수정
|
||||
- 4000자 초과 메시지 자동 분할 전송
|
||||
- 분할 메시지 간 0.5초 대기 (Rate Limit 방지)
|
||||
|
||||
**코드**:
|
||||
```python
|
||||
if len(payload_text) > max_length:
|
||||
chunks = [payload_text[i:i+max_length] for i in range(0, len(payload_text), max_length)]
|
||||
for i, chunk in enumerate(chunks, 1):
|
||||
header = f"[메시지 {i}/{len(chunks)}]\n"
|
||||
send_message(header + chunk)
|
||||
if i < len(chunks):
|
||||
time.sleep(0.5) # Rate Limit 방지
|
||||
```
|
||||
|
||||
**영향**: 긴 메시지 전송 실패 방지, 알림 안정성 향상
|
||||
|
||||
---
|
||||
|
||||
#### [HIGH-008] 재매수 방지 기능 ✅
|
||||
**문제**: 매도 직후 같은 코인 재매수 → 휩소 손실
|
||||
|
||||
**해결**:
|
||||
- **파일**: `src/common.py`, `src/signals.py`, `src/order.py`
|
||||
- `record_sell()`: 매도 기록 저장
|
||||
- `can_buy()`: 재매수 가능 여부 확인 (기본 24시간 쿨다운)
|
||||
- 매도 성공 시 자동 기록, 매수 전 자동 확인
|
||||
|
||||
**코드**:
|
||||
```python
|
||||
# src/common.py
|
||||
def record_sell(symbol: str):
|
||||
sells = json.load(open(RECENT_SELLS_FILE))
|
||||
sells[symbol] = time.time()
|
||||
json.dump(sells, open(RECENT_SELLS_FILE, "w"))
|
||||
|
||||
def can_buy(symbol: str, cooldown_hours: int = 24) -> bool:
|
||||
sells = json.load(open(RECENT_SELLS_FILE))
|
||||
if symbol in sells:
|
||||
elapsed = time.time() - sells[symbol]
|
||||
return elapsed >= cooldown_hours * 3600
|
||||
return True
|
||||
|
||||
# src/signals.py (_process_symbol_core)
|
||||
if not can_buy(symbol, cooldown_hours):
|
||||
return {"summary": [f"재매수 대기 중"]}
|
||||
|
||||
# src/order.py (execute_sell_order_with_confirmation)
|
||||
if trade_status in ["simulated", "filled"]:
|
||||
record_sell(symbol)
|
||||
```
|
||||
|
||||
**영향**: 휩소 손실 방지, 거래 효율성 증가
|
||||
|
||||
---
|
||||
|
||||
### 🟢 MEDIUM Priority Issues
|
||||
|
||||
#### [MEDIUM-001] 설정 파일 검증 ✅
|
||||
**문제**: config.json 필수 항목 누락 시 런타임 에러
|
||||
|
||||
**해결**:
|
||||
- **파일**: `src/config.py`
|
||||
- `validate_config()` 함수 추가
|
||||
- 필수 항목 확인, 범위 검증, 타입 체크
|
||||
|
||||
**코드**:
|
||||
```python
|
||||
def validate_config(cfg: dict) -> tuple[bool, str]:
|
||||
required_keys = [
|
||||
"buy_check_interval_minutes",
|
||||
"stop_loss_check_interval_minutes",
|
||||
"profit_taking_check_interval_minutes",
|
||||
"dry_run",
|
||||
"auto_trade"
|
||||
]
|
||||
|
||||
for key in required_keys:
|
||||
if key not in cfg:
|
||||
return False, f"필수 설정 항목 누락: '{key}'"
|
||||
|
||||
# 범위 검증
|
||||
if cfg["buy_check_interval_minutes"] < 1:
|
||||
return False, "buy_check_interval_minutes는 1 이상이어야 함"
|
||||
|
||||
return True, ""
|
||||
```
|
||||
|
||||
**영향**: 설정 오류 조기 발견, 안정성 향상
|
||||
|
||||
---
|
||||
|
||||
### 🔒 보안 개선
|
||||
|
||||
#### 파일 권한 설정 ✅
|
||||
**문제**: holdings.json, config.json 파일 권한 미설정 → 유출 위험
|
||||
|
||||
**해결**:
|
||||
- **파일**: `src/holdings.py`
|
||||
- holdings.json 저장 시 자동으로 0o600 권한 설정 (소유자만 읽기/쓰기)
|
||||
|
||||
**코드**:
|
||||
```python
|
||||
import stat
|
||||
os.chmod(holdings_file, stat.S_IRUSR | stat.S_IWUSR) # rw-------
|
||||
```
|
||||
|
||||
**영향**: 민감 정보 보호
|
||||
|
||||
---
|
||||
|
||||
#### API 키 유효성 검증 ✅
|
||||
**문제**: 실전 모드 시작 시 API 키 검증 없음 → 런타임 에러
|
||||
|
||||
**해결**:
|
||||
- **파일**: `main.py`
|
||||
- 프로그램 시작 시 Upbit API 키 유효성 검증 (실전 모드 전용)
|
||||
|
||||
**코드**:
|
||||
```python
|
||||
if not cfg.dry_run:
|
||||
is_valid, msg = validate_upbit_api_keys(cfg.upbit_access_key, cfg.upbit_secret_key)
|
||||
if not is_valid:
|
||||
logger.error("[ERROR] Upbit API 키 검증 실패: %s. 종료합니다.", msg)
|
||||
return
|
||||
```
|
||||
|
||||
**영향**: 조기 에러 발견, 안전한 운영
|
||||
|
||||
---
|
||||
|
||||
## 🚫 제외된 항목
|
||||
|
||||
### CRITICAL-004: RSI/MACD 조건 개선
|
||||
- **사유**: **함수 미존재** (과거에 제거됨) + 사용자 요청으로 제외
|
||||
- **현황**:
|
||||
- `check_rsi_oversold`, `check_macd_signal` 함수는 코드베이스에 없음
|
||||
- 현재는 `_evaluate_buy_conditions()` 함수가 **MACD + SMA + ADX** 복합 조건 사용
|
||||
- RSI와 단순 MACD 체크는 사용되지 않음
|
||||
- **결론**: 현재 전략이 더 정교하므로 개선 불필요
|
||||
|
||||
### HIGH-004: Bollinger Bands 로직 수정
|
||||
- **사유**: **함수 미존재** (Bollinger Bands 미사용) + 사용자 요청으로 제외
|
||||
- **현황**:
|
||||
- `check_bollinger_reversal` 함수는 코드베이스에 없음
|
||||
- 현재 매수 전략에 Bollinger Bands 미사용
|
||||
- **결론**: 필요 시 추가 구현 가능 (선택사항)
|
||||
|
||||
### MEDIUM-004: 백테스팅 기능
|
||||
- **사유**: 사용자 요청으로 제외
|
||||
- **내용**: 과거 데이터 기반 전략 검증
|
||||
|
||||
---
|
||||
|
||||
## 📊 개선 효과 예상
|
||||
|
||||
| 항목 | 개선 전 | 개선 후 | 효과 |
|
||||
|------|---------|---------|------|
|
||||
| API Rate Limit 초과 | 가능 (멀티스레드) | 불가능 | 계정 정지 방지 |
|
||||
| 최고가 갱신 | 수동 | 자동 (실시간) | 정확한 트레일링 스톱 |
|
||||
| holdings 데이터 손실 | 가능 (Race Condition) | 불가능 (Lock) | 데이터 무결성 보장 |
|
||||
| 잔고 부족 시 매수 | 실패 | 부분 매수 | 기회 손실 방지 |
|
||||
| Circuit Breaker | 5회 실패 후 차단 | 3회 실패 후 차단 | 빠른 보호 |
|
||||
| Telegram 긴 메시지 | 전송 실패 | 자동 분할 | 알림 안정성 |
|
||||
| 재매수 방지 | 없음 | 24시간 쿨다운 | 휩소 손실 방지 |
|
||||
| 설정 오류 | 런타임 에러 | 시작 시 검증 | 안정성 향상 |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 검증 방법
|
||||
|
||||
### 1. 자동 테스트 실행
|
||||
```bash
|
||||
python scripts/verify_improvements.py
|
||||
```
|
||||
|
||||
**테스트 항목**:
|
||||
- Rate Limiter 동작 확인
|
||||
- 설정 파일 검증
|
||||
- 재매수 방지 기능
|
||||
- 최고가 갱신 로직
|
||||
- Telegram 메시지 분할
|
||||
|
||||
### 2. 수동 검증
|
||||
|
||||
#### Rate Limiter
|
||||
```python
|
||||
from src.common import api_rate_limiter
|
||||
import time
|
||||
|
||||
start = time.time()
|
||||
for i in range(10):
|
||||
api_rate_limiter.acquire()
|
||||
print(f"호출 {i+1}: {time.time() - start:.2f}초")
|
||||
```
|
||||
|
||||
#### 재매수 방지
|
||||
```python
|
||||
from src.common import record_sell, can_buy
|
||||
|
||||
symbol = "KRW-BTC"
|
||||
print(can_buy(symbol)) # True
|
||||
record_sell(symbol)
|
||||
print(can_buy(symbol)) # False (24시간 동안)
|
||||
```
|
||||
|
||||
#### 최고가 갱신
|
||||
```python
|
||||
from src.holdings import update_max_price, load_holdings
|
||||
|
||||
symbol = "KRW-BTC"
|
||||
update_max_price(symbol, 50000000)
|
||||
holdings = load_holdings()
|
||||
print(holdings[symbol]["max_price"]) # 50000000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 향후 개선 계획
|
||||
|
||||
### Phase 2 (1주 내)
|
||||
- [ ] **HIGH-001**: 타입 힌팅 추가 (전체 프로젝트)
|
||||
- [ ] **HIGH-002**: 예외 처리 구체화 (나머지 모듈)
|
||||
- [ ] **HIGH-003**: 로깅 레벨 일관성 개선
|
||||
- [ ] **HIGH-006**: Decimal 기반 가격 계산
|
||||
|
||||
### Phase 3 (1개월 내)
|
||||
- [ ] **MEDIUM-002**: 캔들 데이터 캐싱 (lru_cache)
|
||||
- [ ] **MEDIUM-003**: 에러 코드 표준화
|
||||
- [ ] **MEDIUM-005~012**: 운영 편의성 개선
|
||||
- [ ] **LOW-001~007**: 코드 품질 개선
|
||||
|
||||
---
|
||||
|
||||
## 📝 참고 문서
|
||||
|
||||
- **코드 리뷰 보고서**: `docs/code_review_report_v1.md`
|
||||
- **프로젝트 상태**: `docs/project_state.md`
|
||||
- **검증 스크립트**: `scripts/verify_improvements.py`
|
||||
|
||||
---
|
||||
|
||||
**작성자**: GitHub Copilot (Claude Sonnet 4.5)
|
||||
**작성일**: 2025-12-09
|
||||
**버전**: v1.0
|
||||
Reference in New Issue
Block a user