테스트 강화 및 코드 품질 개선

This commit is contained in:
2025-12-17 00:01:46 +09:00
parent 37a150bd0d
commit 00c57ddd32
51 changed files with 10670 additions and 217 deletions

View 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