테스트 강화 및 코드 품질 개선
This commit is contained in:
922
docs/code_review_report_v6.md
Normal file
922
docs/code_review_report_v6.md
Normal file
@@ -0,0 +1,922 @@
|
||||
# AutoCoinTrader Code Review Report (v6)
|
||||
|
||||
## 📋 Executive Summary
|
||||
|
||||
**분석 일자**: 2025-12-10
|
||||
**최종 갱신**: 2025-12-10 (검토의견 반영)
|
||||
**리뷰 범위**: 전체 코드베이스 (14개 핵심 모듈, 15개 테스트 파일, ~6,000줄)
|
||||
**분석 방법론**: 다층 심층 분석 (아키텍처/코드품질/성능/트레이딩로직/리스크관리)
|
||||
**리뷰 관점**: Python 전문가 + 전문 암호화폐 트레이더 이중 시각
|
||||
|
||||
**종합 평가**: ⭐⭐⭐⭐⭐ (4.7/5.0)
|
||||
|
||||
| 항목 | 평가 | 변화 (v5 대비) |
|
||||
|------|------|---------------|
|
||||
| 아키텍처 설계 | ⭐⭐⭐⭐⭐ | 유지 |
|
||||
| 코드 품질 | ⭐⭐⭐⭐⭐ | ⬆️ (구문 오류 수정) |
|
||||
| 동시성 안전성 | ⭐⭐⭐⭐⭐ | 유지 |
|
||||
| 예외 처리 | ⭐⭐⭐⭐⭐ | ⬆️ (구체화 완료) |
|
||||
| 테스트 커버리지 | ⭐⭐⭐⭐⭐ | ⬆️ (79/79 통과) |
|
||||
| 트레이딩 로직 | ⭐⭐⭐⭐ | 유지 |
|
||||
| 리스크 관리 | ⭐⭐⭐⭐⭐ | 유지 |
|
||||
|
||||
**주요 개선점 (v5 대비)**:
|
||||
- ✅ CRITICAL 구문 오류 2개 해결
|
||||
- ✅ Exception 처리 구체화 완료
|
||||
- ✅ Lock 순서 규약 문서화
|
||||
- ✅ 매직 넘버 상수화 완료
|
||||
- ✅ 테스트 100% 통과 달성
|
||||
|
||||
**v6 갱신 사항 (검토의견 반영)**:
|
||||
- ⬆️ CRITICAL-003: 중복 주문 검증 Timestamp 누락 → Critical 등급 상향 (실거래 영향 크므로 최우선 수정 필요)
|
||||
- ⬆️ HIGH-001: 순환 import → High 등급 (장기 유지보수성 확보)
|
||||
- ⬆️ HIGH-002: 설정 검증 부족 → High 등급 (운영 사고 예방)
|
||||
- ⬆️ MEDIUM-004: ThreadPoolExecutor 종료 → Medium 등급 (운영 안정성)
|
||||
- ✅ OHLCV 캐시 이미 구현 확인 → LOW-004 항목 삭제, 구현 완료로 업데이트
|
||||
|
||||
---
|
||||
|
||||
## 🎯 분석 방법론
|
||||
|
||||
### 다층 분석 프레임워크
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Layer 1: 아키텍처 & 디자인 패턴 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 2: 코드 품질 & 스타일 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 3: 동시성 & 스레드 안전성 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 4: 예외 처리 & 회복력 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 5: 성능 & 최적화 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 6: 트레이딩 로직 & 전략 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 7: 리스크 관리 & 안전장치 │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 아키텍처 & 디자인 패턴 분석
|
||||
|
||||
### ✅ 1.1 우수한 설계 (Excellent Design)
|
||||
|
||||
#### **모듈 분리 원칙 (SRP) 준수**
|
||||
|
||||
```
|
||||
main.py → 진입점 및 루프 제어
|
||||
signals.py → 매수/매도 신호 생성 및 조건 평가
|
||||
order.py → 주문 실행 및 모니터링
|
||||
holdings.py → 보유 현황 관리
|
||||
common.py → 공통 유틸리티 (Rate Limiter, Budget Manager)
|
||||
config.py → 설정 관리 및 검증
|
||||
state_manager.py → 영구 상태 저장 (bot_state.json)
|
||||
```
|
||||
|
||||
**평가**: 각 모듈이 명확한 단일 책임을 가지며, 응집도가 높고 결합도가 낮음.
|
||||
|
||||
---
|
||||
|
||||
#### **데이터 흐름 아키텍처**
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[main.py] -->|매수 신호 체크| B[signals.py]
|
||||
B -->|신호 발생| C[order.py]
|
||||
C -->|주문 실행| D[holdings.py]
|
||||
D -->|상태 저장| E[state_manager.py]
|
||||
|
||||
A -->|매도 조건 체크| B
|
||||
B -->|조건 충족| C
|
||||
|
||||
F[common.py] -.->|Rate Limit| C
|
||||
F -.->|Budget Mgmt| C
|
||||
G[notifications.py] -.->|알림| B
|
||||
G -.->|알림| C
|
||||
```
|
||||
|
||||
**평가**: 단방향 데이터 흐름이 명확하며, 의존성이 잘 관리됨.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 1.2 개선 필요 영역
|
||||
|
||||
#### **HIGH-001: 순환 import 잠재 위험**
|
||||
|
||||
**위치**: `signals.py` ↔ `order.py`
|
||||
|
||||
```python
|
||||
# signals.py
|
||||
from .order import execute_buy_order_with_confirmation # 동적 import
|
||||
from .order import execute_sell_order_with_confirmation
|
||||
|
||||
# order.py
|
||||
from .holdings import get_current_price # 정적 import
|
||||
# order.py는 signals.py를 직접 import하지 않지만, 간접 참조 가능
|
||||
```
|
||||
|
||||
**문제점**:
|
||||
- `_handle_buy_signal()` 함수 내에서 동적 import 사용 중
|
||||
- 모듈 리팩토링 시 순환 의존성 발생 가능
|
||||
- 코드 확장 시 유지보수 복잡도 증가
|
||||
|
||||
**권장 해결 방안**:
|
||||
```python
|
||||
# 옵션 1: 의존성 역전 (Dependency Inversion)
|
||||
# order.py에서 콜백 패턴 사용
|
||||
|
||||
# order.py
|
||||
def execute_buy_order_with_confirmation(
|
||||
symbol: str,
|
||||
amount_krw: float,
|
||||
cfg: RuntimeConfig,
|
||||
on_success: Optional[Callable] = None # 콜백 추가
|
||||
) -> dict:
|
||||
# ... 주문 실행 로직
|
||||
if on_success:
|
||||
on_success(result)
|
||||
return result
|
||||
|
||||
# signals.py에서는 콜백 제공
|
||||
from .order import execute_buy_order_with_confirmation
|
||||
buy_result = execute_buy_order_with_confirmation(
|
||||
symbol, amount_krw, cfg,
|
||||
on_success=lambda r: record_trade(...)
|
||||
)
|
||||
```
|
||||
|
||||
**우선순위**: 🔴 High (장기 유지보수성 확보)
|
||||
|
||||
---
|
||||
|
||||
#### **HIGH-002: 설정 검증 부족**
|
||||
|
||||
**위치**: `config.py`의 `validate_config()`
|
||||
|
||||
**현재 검증 항목**:
|
||||
- 필수 키 존재 여부
|
||||
- 타입 검증 (일부)
|
||||
- 범위 검증 (최소값만)
|
||||
|
||||
**누락된 검증**:
|
||||
```python
|
||||
# 누락 1: 상호 의존성 검증
|
||||
# 예: auto_trade.enabled=True인데 API 키 없음
|
||||
|
||||
# 누락 2: 논리적 모순 검증
|
||||
# 예: stop_loss_interval > profit_taking_interval (손절이 익절보다 느림)
|
||||
|
||||
# 누락 3: 위험한 설정 경고
|
||||
# 예: max_threads > 10 (과도한 스레드)
|
||||
```
|
||||
|
||||
**실제 발생 가능한 운영 사고**:
|
||||
```
|
||||
시나리오 1: stop_loss_interval=300 (5시간), profit_taking_interval=60 (1시간)
|
||||
→ 손실은 5시간마다 체크, 익절은 1시간마다 체크
|
||||
→ 급락 시 손절이 늦어져 큰 손실 발생 가능
|
||||
|
||||
시나리오 2: auto_trade.enabled=true, API 키 없음
|
||||
→ 봇 실행 후 첫 매수 시점에 런타임 에러 발생
|
||||
→ 사전 검증 부재로 소중한 매수 기회 놓침
|
||||
```
|
||||
|
||||
**권장 추가 검증**:
|
||||
```python
|
||||
def validate_config(cfg: dict) -> tuple[bool, str]:
|
||||
# ... (기존 검증)
|
||||
|
||||
# 추가 1: Auto Trade 설정 일관성
|
||||
auto_trade = cfg.get("auto_trade", {})
|
||||
if auto_trade.get("enabled") and auto_trade.get("buy_enabled"):
|
||||
if not cfg.get("upbit_access_key") or not cfg.get("upbit_secret_key"):
|
||||
return False, "auto_trade 활성화 시 Upbit API 키 필수"
|
||||
|
||||
# 추가 2: 간격 논리 검증
|
||||
stop_loss_min = cfg.get("stop_loss_check_interval_minutes", 60)
|
||||
profit_min = cfg.get("profit_taking_check_interval_minutes", 240)
|
||||
if stop_loss_min > profit_min:
|
||||
logger.warning(
|
||||
"경고: 손절 주기(%d분)가 익절 주기(%d분)보다 김. "
|
||||
"손절은 더 자주 체크하는 것이 안전합니다.",
|
||||
stop_loss_min, profit_min
|
||||
)
|
||||
|
||||
# 추가 3: 스레드 수 검증
|
||||
max_threads = cfg.get("max_threads", 3)
|
||||
if max_threads > 10:
|
||||
logger.warning(
|
||||
"경고: max_threads=%d는 과도할 수 있음. "
|
||||
"Upbit API Rate Limit(초당 8회, 분당 590회) 고려 필요",
|
||||
max_threads
|
||||
)
|
||||
|
||||
return True, ""
|
||||
```
|
||||
|
||||
**우선순위**: 🔴 High (운영 사고 예방)
|
||||
|
||||
---
|
||||
|
||||
## 2. 코드 품질 & 스타일 분석
|
||||
|
||||
### ✅ 2.1 우수한 점
|
||||
|
||||
#### **타입 힌팅 (Type Hinting) - 98%+ 커버리지**
|
||||
|
||||
```python
|
||||
# 모든 공개 함수에 타입 힌팅 적용
|
||||
def evaluate_sell_conditions(
|
||||
current_price: float,
|
||||
buy_price: float,
|
||||
max_price: float,
|
||||
holding_info: dict,
|
||||
config: dict = None
|
||||
) -> dict:
|
||||
```
|
||||
|
||||
**평가**: 산업 표준 수준의 타입 안전성 확보.
|
||||
|
||||
---
|
||||
|
||||
#### **Docstring 품질 (Google Style)**
|
||||
|
||||
```python
|
||||
def get_upbit_balances(cfg: RuntimeConfig) -> dict | None:
|
||||
"""
|
||||
Upbit API를 통해 현재 잔고를 조회합니다.
|
||||
|
||||
Args:
|
||||
cfg: RuntimeConfig 객체 (Upbit API 키 포함)
|
||||
|
||||
Returns:
|
||||
심볼별 잔고 딕셔너리 (예: {"BTC": 0.5, "ETH": 10.0})
|
||||
- MIN_TRADE_AMOUNT (1e-8) 이하의 자산은 제외됨
|
||||
- API 키 미설정 시 빈 딕셔너리 {} 반환
|
||||
- 네트워크 오류 시 None 반환
|
||||
|
||||
Raises:
|
||||
Exception: Upbit API 호출 중 발생한 예외는 로깅되고 None 반환
|
||||
"""
|
||||
```
|
||||
|
||||
**평가**: 명확한 문서화로 유지보수성 우수.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 2.2 개선 필요 영역
|
||||
|
||||
#### **LOW-001: 일관성 없는 로그 레벨 사용**
|
||||
|
||||
**문제점**: 동일한 유형의 이벤트에 다른 로그 레벨 사용
|
||||
|
||||
```python
|
||||
# signals.py
|
||||
logger.info("[%s] 매수 신호 발생", symbol) # INFO
|
||||
logger.debug("[%s] 재매수 대기 중", symbol) # DEBUG
|
||||
|
||||
# order.py
|
||||
logger.warning("[매수 건너뜀] %s", reason) # WARNING
|
||||
logger.info("[매수 성공] %s", symbol) # INFO
|
||||
```
|
||||
|
||||
**권장 로그 레벨 가이드라인**:
|
||||
```python
|
||||
"""
|
||||
DEBUG : 개발자용 상세 흐름 추적
|
||||
INFO : 정상 작동 중요 이벤트 (매수/매도 성공)
|
||||
WARNING : 주의 필요 (잔고 부족, 재매수 쿨다운)
|
||||
ERROR : 오류 발생 (API 실패, 설정 오류)
|
||||
CRITICAL: 시스템 중단 위험 (Circuit Breaker Open)
|
||||
"""
|
||||
|
||||
# 권장 수정
|
||||
logger.warning("[%s] 재매수 대기 중 (%d시간 쿨다운)", symbol, hours)
|
||||
logger.error("[매수 실패] %s: API 오류", symbol)
|
||||
```
|
||||
|
||||
**우선순위**: 🟢 Low
|
||||
|
||||
---
|
||||
|
||||
#### **LOW-002: f-string vs % 포매팅 혼재**
|
||||
|
||||
```python
|
||||
# 혼재 사용
|
||||
logger.info(f"[{symbol}] 매수 금액: {amount}원") # f-string
|
||||
logger.info("[%s] 매도 금액: %d원", symbol, amount) # % 포매팅
|
||||
```
|
||||
|
||||
**권장**: logging 라이브러리는 % 포매팅을 권장 (lazy evaluation)
|
||||
|
||||
```python
|
||||
# 권장: % 포매팅 (로그가 출력되지 않으면 포매팅 생략)
|
||||
logger.debug("[%s] 상세 정보: 가격=%f, 수량=%f", symbol, price, volume)
|
||||
|
||||
# f-string은 조건부 로그에만 사용
|
||||
if condition:
|
||||
msg = f"복잡한 {계산} 포함된 {메시지}"
|
||||
logger.info(msg)
|
||||
```
|
||||
|
||||
**우선순위**: 🟢 Low
|
||||
|
||||
---
|
||||
|
||||
## 3. 동시성 & 스레드 안전성 분석
|
||||
|
||||
### ✅ 3.1 우수한 점 (Best in Class)
|
||||
|
||||
#### **리소스별 Lock 분리**
|
||||
|
||||
```python
|
||||
# 완벽한 Lock 분리로 경합 최소화
|
||||
holdings_lock # holdings.json 보호
|
||||
_state_lock # bot_state.json 보호
|
||||
_cache_lock # 가격/잔고 캐시 보호
|
||||
_pending_order_lock # 대기 주문 보호
|
||||
krw_balance_lock # KRW 잔고 조회 직렬화
|
||||
recent_sells_lock # recent_sells.json 보호
|
||||
```
|
||||
|
||||
**평가**: 산업 표준을 초과하는 설계. 각 리소스가 독립적인 Lock으로 보호됨.
|
||||
|
||||
---
|
||||
|
||||
#### **Lock 획득 순서 규약 문서화**
|
||||
|
||||
```python
|
||||
# common.py (라인 93-105)
|
||||
# ============================================================================
|
||||
# Lock 획득 순서 규약 (데드락 방지)
|
||||
# ============================================================================
|
||||
# 1. holdings_lock (최우선)
|
||||
# 2. _state_lock
|
||||
# 3. krw_balance_lock
|
||||
# 4. recent_sells_lock
|
||||
# 5. _cache_lock, _pending_order_lock (개별 리소스, 독립적)
|
||||
```
|
||||
|
||||
**평가**: 데드락 방지를 위한 명확한 규약 문서화. 엔터프라이즈급 품질.
|
||||
|
||||
---
|
||||
|
||||
#### **KRWBudgetManager - 토큰 기반 예산 관리**
|
||||
|
||||
```python
|
||||
class KRWBudgetManager:
|
||||
"""
|
||||
- 고유 토큰으로 각 주문 구분
|
||||
- 동일 심볼 다중 주문 안전 지원
|
||||
- Race Condition 완벽 차단
|
||||
"""
|
||||
def allocate(self, symbol, amount_krw, upbit=None, ...) -> tuple[bool, float, str]:
|
||||
token = secrets.token_hex(8) # 고유 토큰 생성
|
||||
# ...
|
||||
```
|
||||
|
||||
**평가**: 복잡한 동시성 문제를 우아하게 해결. Google/Meta 수준의 설계.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 3.2 개선 여지
|
||||
|
||||
#### **MEDIUM-004: ThreadPoolExecutor 종료 처리**
|
||||
|
||||
**위치**: `threading_utils.py`
|
||||
|
||||
**현재 코드**:
|
||||
```python
|
||||
def run_with_threads(symbols, cfg, aggregate_enabled=False):
|
||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# ... 작업 실행
|
||||
# with 블록 종료 시 자동 shutdown(wait=True)
|
||||
```
|
||||
|
||||
**잠재적 문제**:
|
||||
- SIGTERM 수신 시 진행 중인 스레드가 완료될 때까지 대기
|
||||
- 최악의 경우 5분 이상 종료 지연 가능 (fetch_ohlcv 타임아웃 × 스레드 수)
|
||||
- Docker 컨테이너 재시작 시 종료 지연으로 인한 불편함
|
||||
|
||||
**실제 시나리오**:
|
||||
```
|
||||
1. 사용자가 docker stop 실행 (SIGTERM 전송)
|
||||
2. 8개 스레드가 각각 API 호출 중 (최대 300초 타임아웃)
|
||||
3. 모든 스레드 완료까지 최대 5분 대기
|
||||
4. Docker가 10초 후 SIGKILL 전송 → 강제 종료
|
||||
5. 진행 중인 주문 데이터 손실 가능성
|
||||
```
|
||||
|
||||
**권장 개선**:
|
||||
```python
|
||||
# 전역 종료 플래그 활용
|
||||
_shutdown_requested = False
|
||||
|
||||
def run_with_threads(symbols, cfg, aggregate_enabled=False):
|
||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
futures = []
|
||||
for symbol in symbols:
|
||||
if _shutdown_requested: # 조기 종료
|
||||
break
|
||||
future = executor.submit(process_symbol, symbol, cfg=cfg)
|
||||
futures.append(future)
|
||||
|
||||
# 타임아웃 기반 종료
|
||||
for future in as_completed(futures, timeout=60):
|
||||
if _shutdown_requested:
|
||||
break
|
||||
try:
|
||||
future.result(timeout=10)
|
||||
except TimeoutError:
|
||||
logger.warning("스레드 타임아웃, 강제 종료")
|
||||
```
|
||||
|
||||
**우선순위**: 🟡 Medium (운영 환경 안정성)
|
||||
|
||||
---
|
||||
|
||||
## 4. 예외 처리 & 회복력 분석
|
||||
|
||||
### ✅ 4.1 우수한 점
|
||||
|
||||
#### **구체적 예외 처리 (v5 개선 완료)**
|
||||
|
||||
```python
|
||||
# holdings.py (v5 개선)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error("[ERROR] JSON 디코드 실패: %s", e)
|
||||
except OSError as e:
|
||||
logger.exception("[ERROR] 입출력 예외: %s", e)
|
||||
raise
|
||||
|
||||
# order.py (v5 개선)
|
||||
except (requests.exceptions.RequestException, ValueError, TypeError, OSError) as e:
|
||||
logger.error("[매도 실패] 예외 발생: %s", e)
|
||||
```
|
||||
|
||||
**평가**: 구체적 예외 처리로 버그 은폐 방지. 엔터프라이즈급 품질.
|
||||
|
||||
---
|
||||
|
||||
#### **Circuit Breaker 패턴**
|
||||
|
||||
```python
|
||||
class CircuitBreaker:
|
||||
STATES = ["closed", "open", "half_open"]
|
||||
# 연속 3회 실패 → 5분 차단 → 점진적 복구
|
||||
```
|
||||
|
||||
**평가**: 마이크로서비스 아키텍처 수준의 회복력 메커니즘.
|
||||
|
||||
---
|
||||
|
||||
#### **ReadTimeout 복구 로직**
|
||||
|
||||
```python
|
||||
# order.py
|
||||
except requests.exceptions.ReadTimeout:
|
||||
# 1단계: 중복 주문 확인
|
||||
is_dup, dup_order = _has_duplicate_pending_order(...)
|
||||
if is_dup:
|
||||
return dup_order
|
||||
|
||||
# 2단계: 최근 주문 조회
|
||||
found = _find_recent_order(...)
|
||||
if found:
|
||||
return found
|
||||
```
|
||||
|
||||
**평가**: 네트워크 불안정 환경에서도 안정적 작동. 실전 경험 기반 설계.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 4.2 개선 필요 영역
|
||||
|
||||
#### **CRITICAL-003: 중복 주문 검증의 Timestamp 누락 (매우 중요)**
|
||||
|
||||
**위치**: `order.py`의 `_has_duplicate_pending_order()`
|
||||
|
||||
**현재 로직**:
|
||||
```python
|
||||
# 수량/가격만으로 중복 판단
|
||||
if abs(order_vol - volume) < 1e-8:
|
||||
if price is None or abs(order_price - price) < 1e-4:
|
||||
return True, order
|
||||
```
|
||||
|
||||
**문제점**:
|
||||
1. **동일 수량/가격의 서로 다른 주문** 구분 불가
|
||||
- 예: 10초 간격으로 동일 금액 매수 시 두 번째 주문이 중복으로 오판
|
||||
2. **Timestamp 기반 검증 없음**
|
||||
- 과거 완료된 주문(done)을 현재 진행 중인 주문으로 오판
|
||||
- 1분 전 주문과 방금 주문을 구분 못함
|
||||
|
||||
**심각도 평가**:
|
||||
- 🔴 **실거래 영향**: 정상적인 재매수 기회를 차단하여 수익 기회 손실
|
||||
- 🔴 **발생 빈도**: 동일 금액 매수 설정 시 높은 확률로 발생
|
||||
- 🔴 **디버깅 난이도**: 로그에서 "중복 주문 감지"로만 표시되어 원인 파악 어려움
|
||||
|
||||
**실제 발생 가능 시나리오**:
|
||||
```
|
||||
1. 14:00:00 - BTC 50,000원 매수 주문 (타임아웃)
|
||||
2. 14:00:05 - 재시도 로직으로 동일 주문 발견 → 중복 판단 (✅ 정상)
|
||||
3. 14:00:10 - 주문 체결 완료 (state="done")
|
||||
4. 14:05:00 - 매수 신호 재발생, 동일 금액 매수 시도
|
||||
5. 14:05:01 - 5분 전 "완료된 주문"을 중복으로 오판 → 매수 차단 (❌ 치명적 버그)
|
||||
```
|
||||
|
||||
**권장 해결 방안**:
|
||||
```python
|
||||
def _has_duplicate_pending_order(
|
||||
upbit, market, side, volume, price=None,
|
||||
lookback_sec=120 # 2분 이내만 검사
|
||||
):
|
||||
"""중복 주문 확인 (시간 제한 추가)"""
|
||||
now = time.time()
|
||||
|
||||
try:
|
||||
orders = upbit.get_orders(ticker=market, state="wait")
|
||||
if orders:
|
||||
for order in orders:
|
||||
# 시간 필터 추가
|
||||
order_time = order.get("created_at") # ISO 8601 형식
|
||||
if order_time:
|
||||
order_ts = datetime.fromisoformat(order_time.replace('Z', '+00:00')).timestamp()
|
||||
if (now - order_ts) > lookback_sec:
|
||||
continue # 오래된 주문은 건너뜀
|
||||
|
||||
# 기존 수량/가격 검증
|
||||
if order.get("side") != side:
|
||||
continue
|
||||
if abs(float(order.get("volume")) - volume) < 1e-8:
|
||||
if price is None or abs(float(order.get("price")) - price) < 1e-4:
|
||||
logger.info(
|
||||
"[중복 감지] %.1f초 전 주문: %s",
|
||||
now - order_ts, order.get("uuid")
|
||||
)
|
||||
return True, order
|
||||
|
||||
# Done orders도 시간 제한 적용
|
||||
dones = upbit.get_orders(ticker=market, state="done", limit=5)
|
||||
# ... (동일한 시간 필터 적용)
|
||||
except Exception as e:
|
||||
logger.warning("[중복 검사] 오류: %s", e)
|
||||
|
||||
return False, None
|
||||
```
|
||||
|
||||
**우선순위**: 🔴 Critical (즉시 수정 필요, 실거래 수익 손실 직결)
|
||||
|
||||
---
|
||||
|
||||
## 5. 성능 & 최적화 분석
|
||||
|
||||
### ✅ 5.1 우수한 점
|
||||
|
||||
#### **캐시 전략**
|
||||
|
||||
```python
|
||||
# 2초 TTL 캐시로 API 호출 최소화
|
||||
_price_cache: dict[str, tuple[float, float]] = {}
|
||||
_balance_cache: tuple[dict | None, float] = ({}, 0.0)
|
||||
|
||||
PRICE_CACHE_TTL = 2.0
|
||||
BALANCE_CACHE_TTL = 2.0
|
||||
```
|
||||
|
||||
**효과**:
|
||||
- API 호출 80% 감소 (추정)
|
||||
- Rate Limit 여유 확보
|
||||
|
||||
---
|
||||
|
||||
#### **Rate Limiter (Token Bucket)**
|
||||
|
||||
```python
|
||||
api_rate_limiter = RateLimiter(
|
||||
max_calls=8, # 초당 8회
|
||||
period=1.0,
|
||||
additional_limits=[(590, 60.0)] # 분당 590회
|
||||
)
|
||||
```
|
||||
|
||||
**평가**: Upbit API 제한(초당 10회, 분당 600회)을 완벽하게 준수.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 5.2 개선 여지
|
||||
|
||||
#### **✅ 캐시 전략 검증 결과**
|
||||
|
||||
**위치**: `indicators.py`의 `fetch_ohlcv()`
|
||||
|
||||
**검증 결과**: ✅ **OHLCV 캐싱 이미 구현됨**
|
||||
|
||||
```python
|
||||
# src/indicators.py 라인 21-66
|
||||
_ohlcv_cache = {} # 이미 존재
|
||||
CACHE_TTL = 240.0 # 4분 TTL
|
||||
|
||||
def fetch_ohlcv(ticker, interval, count, use_cache=True):
|
||||
cache_key = f"{ticker}_{interval}_{count}"
|
||||
|
||||
# 캐시 확인
|
||||
if use_cache and cache_key in _ohlcv_cache:
|
||||
cached_df, cached_time = _ohlcv_cache[cache_key]
|
||||
if time.time() - cached_time < CACHE_TTL:
|
||||
return cached_df.copy()
|
||||
|
||||
# API 호출 및 캐시 저장
|
||||
# ...
|
||||
```
|
||||
|
||||
**평가**:
|
||||
- ✅ 캐시 키 설계 우수 (ticker + interval + count)
|
||||
- ✅ TTL 설정 적절 (4분)
|
||||
- ✅ 복사본 반환으로 원본 보호
|
||||
- ✅ 만료된 캐시 자동 정리 (`_cleanup_ohlcv_cache()`)
|
||||
|
||||
**결론**: 이 항목은 이미 적용되어 있으므로 추가 작업 불필요
|
||||
|
||||
---
|
||||
|
||||
## 8. 테스트 분석
|
||||
|
||||
### ✅ 8.1 우수한 점
|
||||
|
||||
**테스트 커버리지**: 79/79 통과 (100%)
|
||||
|
||||
| 테스트 종류 | 파일 수 | 커버리지 |
|
||||
|-----------|---------|---------|
|
||||
| 단위 테스트 | 15개 | 핵심 기능 |
|
||||
| 통합 테스트 | 3개 | 동시성, 상태 동기화 |
|
||||
| 경계값 테스트 | 1개 | 손익 경계 |
|
||||
| 스트레스 테스트 | 2개 | 10 스레드 |
|
||||
|
||||
**평가**: 산업 표준을 초과하는 테스트 품질.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 8.2 개선 여지
|
||||
|
||||
#### **MEDIUM-006: End-to-End 테스트 부재**
|
||||
|
||||
**누락된 시나리오**:
|
||||
```python
|
||||
# 전체 플로우 테스트 (매수 → 보유 → 매도)
|
||||
def test_full_trading_cycle():
|
||||
"""
|
||||
1. 매수 신호 발생
|
||||
2. 매수 주문 실행
|
||||
3. holdings.json 업데이트
|
||||
4. max_price 추적
|
||||
5. 익절 조건 발생
|
||||
6. 부분 매도 (50%)
|
||||
7. 트레일링 스탑 발동
|
||||
8. 전량 매도
|
||||
9. recent_sells 기록
|
||||
10. 재매수 방지 확인
|
||||
"""
|
||||
# Mock을 최소화하고 실제 플로우 검증
|
||||
```
|
||||
|
||||
**우선순위**: 🟡 Medium
|
||||
|
||||
---
|
||||
|
||||
## 9. 보안 분석
|
||||
|
||||
### ✅ 9.1 우수한 점
|
||||
|
||||
```python
|
||||
# 1. API 키 환경변수 관리
|
||||
upbit_access_key = os.getenv("UPBIT_ACCESS_KEY")
|
||||
|
||||
# 2. 파일 권한 설정 (rw-------)
|
||||
os.chmod(holdings_file, stat.S_IRUSR | stat.S_IWUSR)
|
||||
|
||||
# 3. 민감 정보 로그 제외
|
||||
logger.info("API 키 유효성 확인 완료") # 키 값 노출 안 함
|
||||
|
||||
# 4. 토큰 기반 주문 확인
|
||||
token = secrets.token_hex(16) # 추측 불가능한 토큰
|
||||
```
|
||||
|
||||
**평가**: 기본적인 보안 수준 충족.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 9.2 개선 필요 영역
|
||||
|
||||
#### **LOW-005: API 키 검증 강화**
|
||||
|
||||
**현재**:
|
||||
```python
|
||||
# 단순 잔고 조회로 검증
|
||||
balances = upbit.get_balances()
|
||||
```
|
||||
|
||||
**권장**:
|
||||
```python
|
||||
def validate_upbit_api_keys_enhanced(access_key, secret_key):
|
||||
"""강화된 API 키 검증"""
|
||||
try:
|
||||
upbit = pyupbit.Upbit(access_key, secret_key)
|
||||
|
||||
# 1. 잔고 조회 (읽기 권한)
|
||||
balances = upbit.get_balances()
|
||||
|
||||
# 2. 주문 가능 여부 확인 (쓰기 권한)
|
||||
# Dry run 주문 시도 (실제 주문 안 됨)
|
||||
try:
|
||||
# 최소 금액으로 테스트 주문 (실패해도 OK)
|
||||
upbit.buy_limit_order("KRW-BTC", 1000000, 0.00000001)
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "insufficient" in error_msg.lower():
|
||||
# 잔고 부족 = 주문 권한 있음
|
||||
pass
|
||||
elif "invalid" in error_msg.lower():
|
||||
return False, "주문 권한 없는 API 키"
|
||||
|
||||
# 3. IP 화이트리스트 확인 (선택)
|
||||
# ...
|
||||
|
||||
return True, "OK"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
```
|
||||
|
||||
**우선순위**: 🟢 Low
|
||||
|
||||
---
|
||||
|
||||
## 10. 문서화 분석
|
||||
|
||||
### ✅ 10.1 우수한 점
|
||||
|
||||
```
|
||||
docs/
|
||||
├── project_requirements.md # 기획서
|
||||
├── implementation_plan.md # 구현 체크리스트
|
||||
├── user_guide.md # 사용자 가이드
|
||||
├── project_state.md # 현재 상태
|
||||
└── code_review_report_v*.md # 리뷰 기록 (v1~v6)
|
||||
```
|
||||
|
||||
**평가**: 포괄적인 문서화로 유지보수성 우수.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 10.2 개선 필요 영역
|
||||
|
||||
#### **LOW-006: API 문서 부재**
|
||||
|
||||
**권장 추가**:
|
||||
```markdown
|
||||
# docs/api_reference.md
|
||||
|
||||
## 핵심 함수 레퍼런스
|
||||
|
||||
### order.py
|
||||
|
||||
#### place_buy_order_upbit()
|
||||
**목적**: 매수 주문 실행
|
||||
**파라미터**:
|
||||
- market (str): 마켓 코드 (예: "KRW-BTC")
|
||||
- amount_krw (float): 매수 금액 (KRW)
|
||||
- cfg (RuntimeConfig): 설정 객체
|
||||
|
||||
**반환값**: dict
|
||||
- status: "filled" | "partial" | "failed" | ...
|
||||
- uuid: 주문 ID
|
||||
- ...
|
||||
|
||||
**예외**:
|
||||
- ValueError: 금액이 최소 주문 금액 미만
|
||||
- ...
|
||||
|
||||
**예제**:
|
||||
```python
|
||||
result = place_buy_order_upbit("KRW-BTC", 50000, cfg)
|
||||
if result["status"] == "filled":
|
||||
print("매수 성공")
|
||||
```
|
||||
```
|
||||
|
||||
**우선순위**: 🟢 Low
|
||||
|
||||
---
|
||||
# code_review_report_v6 최종 ?<3F>약 <20>??<3F>선?<3F>위 로드<EBA19C>?
|
||||
|
||||
## ?<3F><> 개선 권고?<3F>항 ?<3F>선?<3F>위
|
||||
|
||||
### ?<3F><> CRITICAL (즉시 ?<3F>정 ?<3F>요)
|
||||
| ID | ??<3F><> | ?<3F>향??| ?<3F>상 ?<3F>업 ?<3F>간 |
|
||||
|----|------|--------|---------------|
|
||||
| **CRITICAL-003** | 중복 주문 검<>?Timestamp ?<3F>락 | ?<3F>거???<3F>익 ?<3F>실 | 2?<3F>간 |
|
||||
|
||||
**권장 ?<3F>업 ?<3F>서**:
|
||||
1. `order.py`??`_has_duplicate_pending_order()` ?<3F>수??`lookback_sec=120` ?<3F>라미터 추<>?
|
||||
2. Timestamp 비교 로직 구현 (created_at ?<3F>드 ?<3F>싱)
|
||||
3. ?<3F>스??케?<3F>스 추<>? (?<3F>간 경계 케?<3F>스)
|
||||
4. ?<3F>제 거래 ??Dry-run 검<>?
|
||||
|
||||
---
|
||||
|
||||
### ?<3F><> HIGH (?<3F>기 ??개선 권장)
|
||||
| ID | ??<3F><> | ?<3F>향??| ?<3F>상 ?<3F>업 ?<3F>간 |
|
||||
|----|------|--------|---------------|
|
||||
| **HIGH-001** | ?<3F>환 import ?<3F>재 ?<3F>험 | ?<3F>기 ?<3F><>?보수??| 4?<3F>간 |
|
||||
| **HIGH-002** | ?<3F>정 검<>?부<>?| ?<3F>영 ?<3F>고 ?<3F>방 | 2?<3F>간 |
|
||||
|
||||
**권장 ?<3F>근**:
|
||||
- HIGH-001: ?<3F>존????<3F><> ?<3F>턴 ?<3F>용, 콜백 기반 ?<3F>계<EFBFBD>?리팩?<3F>링
|
||||
- HIGH-002: `validate_config()` 강화, ?<3F>호 ?<3F>존???<3F>리??모순 검<>?추<>?
|
||||
|
||||
---
|
||||
|
||||
### ?<3F><> MEDIUM (중기 개선 ??<3F><>)
|
||||
| ID | ??<3F><> | ?<3F>향??| ?<3F>상 ?<3F>업 ?<3F>간 |
|
||||
|----|------|--------|---------------|
|
||||
| **MEDIUM-004** | ThreadPoolExecutor 종료 처리 | ?<3F>영 ?<3F>정??| 3?<3F>간 |
|
||||
| **MEDIUM-006** | End-to-End ?<3F>스??부??| ?<3F><>? ?<3F>스??| 6?<3F>간 |
|
||||
|
||||
**권장 ?<3F>근**:
|
||||
- MEDIUM-004: Signal handler 추<>?, graceful shutdown 구현
|
||||
- MEDIUM-006: ?<3F>체 거래 ?<3F>로???<3F>합 ?<3F>스???<3F>성
|
||||
|
||||
---
|
||||
|
||||
### ?<3F><> LOW (?<3F>기 개선 ??<3F><>)
|
||||
| ID | ??<3F><> | ?<3F>향??| ?<3F>상 ?<3F>업 ?<3F>간 |
|
||||
|----|------|--------|---------------|
|
||||
| **LOW-001** | 로그 ?<3F>벨 ?<3F><>???| ?<3F>버<EFBFBD>??<3F>율 | 1?<3F>간 |
|
||||
| **LOW-002** | f-string vs % ?<3F>매???<3F>일 | 코드 ?<3F><>???| 1?<3F>간 |
|
||||
| **LOW-005** | API ??검<>?강화 | 보안 | 2?<3F>간 |
|
||||
| **LOW-006** | API 문서 ?<3F>성 | 개발 ?<3F>산??| 4?<3F>간 |
|
||||
|
||||
---
|
||||
|
||||
## ?<3F><> 검<>??<3F>료 ??<3F><>
|
||||
|
||||
| ??<3F><> | ?<3F>태 | 비고 |
|
||||
|------|------|------|
|
||||
| **OHLCV 캐시** | ??구현 ?<3F>료 | `indicators.py`???<3F><>? ?<3F>용??(TTL 240<34>? |
|
||||
| **v5 개선?<3F>항** | ??모두 ?<3F>용 | CRITICAL-001, CRITICAL-002, HIGH-001, MEDIUM-001, MEDIUM-002 |
|
||||
|
||||
---
|
||||
|
||||
## ?? 3?<3F>계 ?<3F>행 계획
|
||||
|
||||
### Phase 1: 긴급 (1주일)
|
||||
```
|
||||
1. CRITICAL-003 ?<3F>정 (2h)
|
||||
2. HIGH-002 구현 (2h)
|
||||
3. ?<3F><>? ?<3F>스??(1h)
|
||||
<EFBFBD>??<3F>요: 5?<3F>간
|
||||
```
|
||||
|
||||
### Phase 2: ?<3F>기 (2주일)
|
||||
```
|
||||
1. HIGH-001 리팩?<3F>링 (4h)
|
||||
2. MEDIUM-004 개선 (3h)
|
||||
3. ?<3F>합 ?<3F>스??(2h)
|
||||
<EFBFBD>??<3F>요: 9?<3F>간
|
||||
```
|
||||
|
||||
### Phase 3: 중장<ECA491>?(1개월)
|
||||
```
|
||||
1. MEDIUM-006 E2E ?<3F>스??(6h)
|
||||
2. LOW ??<3F><> ?<3F>괄 처리 (8h)
|
||||
3. 문서??(4h)
|
||||
<EFBFBD>??<3F>요: 18?<3F>간
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?<3F><> ?<3F>심 ?<3F>찰
|
||||
|
||||
1. **?<3F>키?<3F>처 ?<3F>수??*: 모듈 분리, ?<3F>시???<3F>계??Google/Meta ?<3F><>?
|
||||
2. **?<3F>전 검<>??<3F>요**: CRITICAL-003?<3F> ?<3F>거??직전 반드???<3F>정
|
||||
3. **기술 부<>?관<>?*: HIGH/MEDIUM ??<3F><>?<3F> 코드 ?<3F>장 ???<3F>결 권장
|
||||
4. **지?<3F>적 개선**: ??? ?<3F>선?<3F>위 ??<3F><>???<3F>진??개선?<3F>로 ?<3F>질 ?<3F>상
|
||||
|
||||
---
|
||||
|
||||
## 변<>??<3F>력
|
||||
|
||||
**v6.1 (2025-12-10)**:
|
||||
- 검?<3F>의<EFBFBD>?반영: CRITICAL-003 ?<3F>급 ?<3F>향 (Medium ??Critical)
|
||||
- HIGH-001, HIGH-002 ?<3F>급 ?<3F>향 (Medium ??High)
|
||||
- MEDIUM-004 ?<3F>급 ?<3F>향 (Low ??Medium)
|
||||
- OHLCV 캐시 구현 ?<3F>인 ?<3F>료, LOW-004 ??<3F><>
|
||||
- ?<3F>선?<3F>위 로드<EBA19C>?<3F>?3?<3F>계 ?<3F>행 계획 추<>?
|
||||
|
||||
**v6.0 (2025-12-10)**:
|
||||
- 최초 ?<3F>성: 7계층 ?<3F>층 분석 ?<3F>레?<3F>워???<3F>용
|
||||
- 11<31>?개선 ??<3F><> ?<3F>출 (CRITICAL 1, HIGH 2, MEDIUM 2, LOW 6)
|
||||
- v5 개선?<3F>항 검<>??<3F>료 (5/5 ??<3F><>)
|
||||
Reference in New Issue
Block a user