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

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,971 @@
# AutoCoinTrader 코드 리뷰 보고서 v1
**리뷰 일자**: 2025-12-09
**프로젝트**: 업비트 자동매매 시스템
**전체 평가**: 7.5/10 (운영 가능, 중요 개선 필요)
---
## Executive Summary
### 종합 평가
-**강점**: 모듈화, 멀티스레딩, Circuit Breaker, 알림 시스템
- ⚠️ **개선 필요**: 타입 힌팅, 에러 핸들링, 트레이딩 로직 검증
- 🔴 **Critical**: API Rate Limit, 동시성 버그, 손절 로직
### 발견 사항 통계
| 우선순위 | 개수 | 설명 |
|---------|------|------|
| CRITICAL | 5 | 즉시 수정 필요 (트레이딩 손실 위험) |
| HIGH | 8 | 1주 내 수정 권장 |
| MEDIUM | 12 | 1개월 내 개선 |
| LOW | 7 | 코드 품질 향상 |
---
## 1. Critical Issues (즉시 수정 필요)
### [CRITICAL-001] API Rate Limit 미보호
**파일**: `src/indicators.py`, `src/order.py`
**위험도**: 🔴 HIGH (계정 정지 가능)
**문제**:
```python
# indicators.py의 fetch_ohlcv()
for attempt in range(retries):
try:
df = pyupbit.get_ohlc(ticker, interval=interval, count=count)
time.sleep(0.3) # ❌ 고정 딜레이, Rate Limit 미고려
```
**이슈**:
- Upbit API는 초당 10회, 분당 600회 제한 (초과 시 418 에러)
- 현재 0.3초 딜레이는 초당 3.3회 호출 가능 → **멀티스레딩 시 한계 초과**
- `run_with_threads()`로 3개 심볼 동시 조회 시 순간 9.9회/초 → **경계선**
**해결**:
```python
# src/common.py에 추가
import threading
from collections import deque
from time import time
class RateLimiter:
"""토큰 버킷 알고리즘 기반 Rate Limiter"""
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):
with self.lock:
now = time()
# 기간 내 호출 제거
while self.calls and now - self.calls[0] > self.period:
self.calls.popleft()
if len(self.calls) < self.max_calls:
self.calls.append(now)
return
# Rate Limit 초과 시 대기
sleep_time = self.period - (now - self.calls[0]) + 0.1
time.sleep(sleep_time)
self.calls.append(time())
# 전역 인스턴스
api_rate_limiter = RateLimiter(max_calls=8, period=1.0)
# 사용 예시 (indicators.py)
def fetch_ohlcv(...):
for attempt in range(retries):
api_rate_limiter.acquire() # ✅ Rate Limit 보호
df = pyupbit.get_ohlc(...)
```
**우선순위**: P0 (즉시)
---
### [CRITICAL-002] 손절 로직 오류 - 최고가 미갱신
**파일**: `src/signals.py:42-65` (`evaluate_sell_conditions`)
**위험도**: 🔴 HIGH (수익 손실)
**문제**:
```python
def evaluate_sell_conditions(current_price, buy_price, max_price, holding_info, config):
# max_price는 외부에서 전달받지만, 갱신 로직이 없음
profit_pct = ((current_price - buy_price) / buy_price) * 100
# 2. 저수익 구간: 최고점 대비 5% 하락 → 전량 매도
if profit_pct <= 10:
if current_price < max_price * 0.95:
return {"action": "sell", "ratio": 1.0, "reason": "최고점 대비 5% 하락"}
```
**이슈**:
- `max_price``holdings.json`에서 로드되지만, **실시간 갱신 안 됨**
- 시나리오:
1. 매수가 10,000원, 현재가 11,000원 (수익률 10%)
2. `max_price`가 10,500원으로 잘못 기록됨
3. 현재가 10,400원으로 하락 → `10,400 < 10,500 * 0.95 (9,975)`**매도 안 됨**
4. 실제로는 `11,000 * 0.95 = 10,450`에서 매도했어야 함
**해결**:
```python
# src/holdings.py에 추가
def update_max_price(symbol: str, current_price: float):
"""최고가를 갱신 (기존 max_price보다 높을 때만)"""
with holdings_lock:
holdings = load_holdings()
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)
logger.info("[%s] 최고가 갱신: %.2f%.2f", symbol, old_max, current_price)
# main.py의 _check_stop_loss_profit() 수정
def _check_stop_loss_profit(cfg, config):
holdings = load_holdings()
for symbol, info in holdings.items():
current_price = get_current_price(symbol)
if current_price is None:
continue
# ✅ 최고가 갱신
update_max_price(symbol, current_price)
# 매도 조건 확인
max_price = info.get("max_price", info["buy_price"])
result = evaluate_sell_conditions(...)
```
**우선순위**: P0 (즉시)
---
### [CRITICAL-003] 동시성 버그 - holdings.json 손상 위험
**파일**: `src/holdings.py:20-37` (`save_holdings`)
**위험도**: 🔴 MEDIUM (데이터 손실)
**문제**:
```python
def save_holdings(holdings: dict):
"""holdings를 JSON 파일에 저장"""
# ❌ holdings_lock이 없어도 저장 가능 → Race Condition
with open(HOLDINGS_FILE, "w", encoding="utf-8") as f:
json.dump(holdings, f, indent=2, ensure_ascii=False)
```
**이슈**:
- 멀티스레드 환경에서 2개 스레드가 동시에 `save_holdings()` 호출 시:
- Thread A: holdings 읽기 → 수정 → **저장 중**
- Thread B: holdings 읽기 (A의 수정 전) → 수정 → **덮어쓰기****A의 변경사항 손실**
**해결**:
```python
# src/holdings.py
def save_holdings(holdings: dict):
"""holdings를 JSON 파일에 저장 (Thread-Safe)"""
with holdings_lock: # ✅ Lock으로 보호
# 원자적 저장 (Atomic Write)
tmp_file = HOLDINGS_FILE + ".tmp"
with open(tmp_file, "w", encoding="utf-8") as f:
json.dump(holdings, f, indent=2, ensure_ascii=False)
# 파일 교체 (Windows에서는 os.replace 사용)
import os
if os.path.exists(HOLDINGS_FILE):
os.remove(HOLDINGS_FILE)
os.rename(tmp_file, HOLDINGS_FILE)
# 모든 save_holdings() 호출 전에 load_holdings() → 수정 → save_holdings() 패턴 사용
```
**우선순위**: P0 (즉시)
---
### [CRITICAL-004] ~~RSI/MACD 과매수/과매도 조건 오류~~ (함수 미존재)
**파일**: ~~`src/signals.py:250-280`~~**해당 함수 없음**
**위험도**: 🔴 MEDIUM (잘못된 매수 신호)
**⚠️ 중요: `check_rsi_oversold``check_macd_signal` 함수는 현재 코드베이스에 존재하지 않습니다.**
**현재 구현**:
- `_evaluate_buy_conditions()` 함수가 **MACD + SMA + ADX** 기반 3가지 조건으로 매수 신호 생성
- RSI는 현재 사용되지 않음
- 단순 지표 체크 대신 **복합 조건 (MACD 골든크로스 + SMA 정배열 + ADX 강세)** 사용
**현재 매수 전략** (`src/signals.py:370-470`):
```python
def _evaluate_buy_conditions(data: dict) -> dict:
"""
매수 조건 평가 (4시간봉 기준):
1. MACD 상향 돌파 + SMA5 > SMA200 + ADX > 25
2. SMA 골든크로스 + MACD > 신호선 + ADX > 25
3. ADX 상향 돌파 + SMA5 > SMA200 + MACD > 신호선
"""
# MACD 크로스 확인
cross_macd_signal = (prev_macd < prev_signal and curr_macd > curr_signal)
cross_macd_zero = (prev_macd < 0 and curr_macd > 0)
macd_cross_ok = cross_macd_signal or cross_macd_zero
# SMA 정배열 확인
sma_condition = (curr_sma_short > curr_sma_long)
# ADX 강세 확인
adx_ok = (curr_adx > adx_threshold)
# 3가지 조건 중 하나라도 충족 시 매수
if macd_cross_ok and sma_condition and adx_ok:
matches.append("매수조건1")
# ... (조건2, 조건3)
```
**분석**:
-**장점**: 복합 조건으로 신뢰도 높은 신호 생성
-**장점**: ADX로 추세 강도 확인 (약세 반등 필터링)
- ⚠️ **단점**: RSI를 사용하지 않아 과매도 구간 저가 매수 기회 놓칠 수 있음
- ⚠️ **단점**: 히스토그램 증가 확인 없음 (모멘텀 약화 구간에서 매수 가능)
**개선 제안** (선택사항):
1. **RSI 추가 고려**: 현재 전략이 잘 작동한다면 RSI 추가는 선택사항
2. **MACD 히스토그램 확인**: 조건1에 `macd_hist > prev_macd_hist` 추가 검토
3. **백테스팅 필요**: 현재 전략의 성과를 먼저 분석 후 개선 여부 결정
**우선순위**: ~~P0~~**P2 (현재 전략 성과 분석 후 결정)**
---
### [CRITICAL-005] 잔고 부족 시 부분 매수 미지원
**파일**: `src/order.py:320-360` (`execute_buy_order`)
**위험도**: 🔴 MEDIUM (기회 손실)
**문제**:
```python
def execute_buy_order(upbit, symbol, amount_krw, config):
"""매수 주문 실행"""
krw_balance = upbit.get_balance("KRW")
if krw_balance < amount_krw:
logger.warning("[%s] 잔고 부족: 필요 %.0f원, 보유 %.0f", symbol, amount_krw, krw_balance)
# ❌ 여기서 종료 → 잔고가 9,000원인데 10,000원 필요 시 매수 불가
return None
```
**이슈**:
- 설정: `buy_amount_krw: 10000`, 최소 주문 금액: 5000원
- 잔고: 9,000원 → **매수 안 함** (5000원 이상이면 매수 가능한데)
**해결**:
```python
def execute_buy_order(upbit, symbol, amount_krw, config):
"""매수 주문 실행 (부분 매수 지원)"""
krw_balance = upbit.get_balance("KRW")
min_order = config.get("auto_trade", {}).get("min_order_value_krw", 5000)
# ✅ 잔고가 부족하면 가능한 만큼 매수
if krw_balance < amount_krw:
if krw_balance >= min_order:
logger.info("[%s] 잔고 부족, 부분 매수: %.0f원 → %.0f", symbol, amount_krw, krw_balance)
amount_krw = krw_balance
else:
logger.warning("[%s] 잔고 부족: 보유 %.0f원 < 최소 %.0f", symbol, krw_balance, min_order)
return None
# 수수료 고려 (0.05%)
amount_krw = amount_krw * 0.9995
# 매수 로직 계속...
```
**우선순위**: P1 (1주 내)
---
## 2. High Priority Issues
### [HIGH-001] 타입 힌팅 부족 (전체 프로젝트)
**위험도**: 🟡 MEDIUM (유지보수성)
**문제**:
- 주요 함수에 타입 힌트 없음 → IDE 자동완성 불가, 런타임 에러 위험
**예시**:
```python
# ❌ Before
def evaluate_sell_conditions(current_price, buy_price, max_price, holding_info, config=None):
...
# ✅ After
from typing import Dict, Optional
def evaluate_sell_conditions(
current_price: float,
buy_price: float,
max_price: float,
holding_info: Dict[str, any],
config: Optional[Dict[str, any]] = None
) -> Dict[str, any]:
...
```
**적용 범위**:
- `src/signals.py`: 모든 public 함수
- `src/order.py`: `execute_*_order()`, `validate_*()` 함수
- `src/holdings.py`: `load_holdings()`, `save_holdings()`, `update_holdings()`
**우선순위**: P1 (1주 내, mypy 도입 추천)
---
### [HIGH-002] 예외 처리 개선 - 구체적 예외 사용
**위험도**: 🟡 MEDIUM
**문제**:
```python
# src/indicators.py
try:
df = pyupbit.get_ohlc(...)
except Exception as e: # ❌ 너무 포괄적
logger.error("데이터 조회 실패: %s", str(e))
return None
```
**이슈**:
- `Exception``KeyboardInterrupt`, `SystemExit`도 잡음 → **프로그램 종료 불가**
- 네트워크 오류 vs API 오류 구분 안 됨
**해결**:
```python
try:
df = pyupbit.get_ohlc(...)
except requests.exceptions.Timeout:
logger.warning("[%s] 타임아웃 (재시도 %d/%d)", symbol, attempt+1, retries)
continue
except requests.exceptions.ConnectionError as e:
logger.error("[%s] 네트워크 오류: %s", symbol, e)
return None
except ValueError as e: # API 응답 파싱 오류
logger.error("[%s] 잘못된 응답: %s", symbol, e)
return None
except Exception as e:
logger.exception("[%s] 예상치 못한 오류", symbol)
raise # ✅ 디버깅을 위해 재발생
```
**우선순위**: P1
---
### [HIGH-003] 로깅 레벨 불일치
**위험도**: 🟡 LOW
**문제**:
```python
# src/signals.py
logger.info("[%s] 매수 신호 없음", symbol) # ❌ 너무 많은 INFO 로그
logger.error("[%s] OHLCV 조회 실패", symbol) # ✅ 적절
```
**이슈**:
- 매수 신호 없음은 **정상 상태** → DEBUG 레벨
- 로그 파일 비대화 (1일 10MB+)
**해결**:
```python
# 정상 흐름: DEBUG
logger.debug("[%s] 매수 신호 없음", symbol)
# 중요 이벤트: INFO
logger.info("[%s] 매수 신호 발생 (RSI=25.3, MACD 골든크로스)", symbol)
# 복구 가능한 오류: WARNING
logger.warning("[%s] 데이터 조회 재시도 (3/5)", symbol)
# 복구 불가 오류: ERROR
logger.error("[%s] 주문 실패: %s", symbol, error)
# 치명적 오류: CRITICAL
logger.critical("[시스템] Upbit API 키 만료, 거래 중단")
```
**우선순위**: P2
---
### [HIGH-004] ~~Bollinger Bands 매수 조건 과도~~ (함수 미존재)
**파일**: ~~`src/signals.py:350-370`~~**해당 함수 없음**
**⚠️ 중요: `check_bollinger_reversal` 함수는 현재 코드베이스에 존재하지 않습니다.**
**현재 구현**:
- Bollinger Bands는 현재 매수 조건에 사용되지 않음
- 매수 신호는 **MACD + SMA + ADX** 조합만 사용
- 볼린저 밴드 관련 코드 없음
**분석**:
-**현 상태**: 볼린저 밴드 미사용으로 과도한 매수 문제 없음
- ⚠️ **기회 손실**: 볼린저 밴드 하단 반등은 유용한 매수 신호일 수 있음
- 💡 **제안**: 필요 시 추가 조건으로 구현 가능 (선택사항)
**추가 구현 시 권장 코드** (선택사항):
```python
def check_bollinger_reversal(df, period=20, std=2):
"""볼린저 밴드 하단 + 반등 확인"""
bb = df.ta.bbands(length=period, std=std, append=False)
if len(df) < 3:
return False
latest_close = df["close"].iloc[-1]
prev_close = df["close"].iloc[-2]
latest_lower = bb[f"BBL_{period}_{std}.0"].iloc[-1]
# ✅ 하단 터치 + 반등 시작
touched_lower = prev_close <= latest_lower
bouncing = latest_close > prev_close
return touched_lower and bouncing
```
**우선순위**: ~~P1~~**P3 (선택사항, 현재 전략 성과 분석 후 결정)**
---
### [HIGH-005] Circuit Breaker 설정 부족
**파일**: `src/circuit_breaker.py`
**문제**:
- `failure_threshold=5`**너무 높음** (5번 실패 후 차단)
- API 오류가 연속 5회 발생하면 이미 계정 경고 상태일 수 있음
**해결**:
```python
# main.py
order_circuit_breaker = CircuitBreaker(
failure_threshold=3, # ✅ 3회로 감소
recovery_timeout=300, # 5분 → 10분
half_open_max_calls=1
)
```
**우선순위**: P1
---
### [HIGH-006] 가격 정밀도 손실
**파일**: `src/order.py:450-480`
**문제**:
```python
# 주문 가격 계산
price = current_price * 1.01 # ❌ float 연산, 정밀도 손실
amount = amount_krw / price
```
**해결**:
```python
from decimal import Decimal, ROUND_DOWN
def calculate_order_amount(amount_krw: float, price: float) -> float:
"""주문 수량 계산 (정밀도 보장)"""
d_amount_krw = Decimal(str(amount_krw))
d_price = Decimal(str(price))
d_amount = d_amount_krw / d_price
# Upbit는 소수점 8자리까지 지원
return float(d_amount.quantize(Decimal('0.00000001'), rounding=ROUND_DOWN))
```
**우선순위**: P2
---
### [HIGH-007] Telegram 메시지 길이 초과
**파일**: `src/notifications.py:50-80`
**문제**:
```python
def send_telegram(message: str, ...):
# ❌ Telegram은 메시지 4096자 제한, 초과 시 전송 실패
bot.send_message(chat_id, message)
```
**해결**:
```python
def send_telegram(message: str, max_length: int = 4000, ...):
"""텔레그램 메시지 전송 (자동 분할)"""
if len(message) <= max_length:
bot.send_message(chat_id, message, ...)
return
# 메시지 분할
chunks = [message[i:i+max_length] for i in range(0, len(message), max_length)]
for i, chunk in enumerate(chunks, 1):
header = f"[{i}/{len(chunks)}]\n" if len(chunks) > 1 else ""
bot.send_message(chat_id, header + chunk, ...)
time.sleep(0.5) # Rate Limit
```
**우선순위**: P2
---
### [HIGH-008] 매도 후 재매수 방지 미흡
**파일**: `src/signals.py:100-130` (`check_buy_conditions_for_symbol`)
**문제**:
- 매도 직후 같은 코인을 다시 매수하는 경우 발생 (휩소 손실)
**해결**:
```python
# src/common.py
RECENT_SELLS_FILE = "data/recent_sells.json"
def record_sell(symbol: str):
"""매도 기록 (24시간 재매수 방지)"""
import json, time
try:
with open(RECENT_SELLS_FILE, "r") as f:
sells = json.load(f)
except:
sells = {}
sells[symbol] = time.time()
with open(RECENT_SELLS_FILE, "w") as f:
json.dump(sells, f)
def can_buy(symbol: str, cooldown_hours: int = 24) -> bool:
"""재매수 가능 여부 확인"""
import json, time
try:
with open(RECENT_SELLS_FILE, "r") as f:
sells = json.load(f)
if symbol in sells:
elapsed = time.time() - sells[symbol]
if elapsed < cooldown_hours * 3600:
return False
except:
pass
return True
# signals.py
def check_buy_conditions_for_symbol(cfg, symbol, config):
# ✅ 재매수 방지 확인
if not can_buy(symbol):
logger.debug("[%s] 재매수 대기 중 (24시간 쿨다운)", symbol)
return None
# 기존 로직...
```
**우선순위**: P1
---
## 3. Medium Priority Issues
### [MEDIUM-001] 설정 파일 검증 부족
**파일**: `src/config.py:40-80`
**문제**:
```python
def load_config():
# ❌ config.json의 필수 키 검증 없음
return json.load(f)
```
**해결**:
```python
def validate_config(cfg: dict) -> bool:
"""설정 파일 필수 항목 검증"""
required_keys = ["buy_check_interval_minutes", "dry_run", "auto_trade"]
for key in required_keys:
if key not in cfg:
logger.error("설정 파일에 '%s' 항목 없음", key)
return False
# 범위 검증
if cfg["buy_check_interval_minutes"] < 1:
logger.error("buy_check_interval_minutes는 1 이상이어야 함")
return False
return True
def load_config():
cfg = json.load(f)
if not validate_config(cfg):
raise ValueError("설정 파일 검증 실패")
return cfg
```
---
### [MEDIUM-002] 캔들 데이터 캐싱 없음
**파일**: `src/indicators.py`
**문제**:
- 같은 심볼을 여러 함수에서 조회 → API 호출 낭비
**해결**:
```python
from functools import lru_cache
import time
@lru_cache(maxsize=100)
def fetch_ohlcv_cached(symbol: str, timeframe: str, count: int, timestamp: int):
"""캔들 데이터 조회 (1분간 캐싱)"""
# timestamp는 분 단위로 전달 (60초마다 캐시 무효화)
return fetch_ohlcv(symbol, timeframe, count)
# 사용
current_minute = int(time.time() // 60)
df = fetch_ohlcv_cached(symbol, "4h", 200, current_minute)
```
---
### [MEDIUM-003] 에러 코드 표준화
**문제**: 에러 메시지가 문자열로만 존재 → 프로그래밍 방식 처리 불가
**해결**:
```python
# src/errors.py
from enum import Enum
class ErrorCode(Enum):
# API Errors
API_RATE_LIMIT = "E001"
API_TIMEOUT = "E002"
API_AUTH_FAILED = "E003"
# Order Errors
ORDER_INSUFFICIENT_BALANCE = "E101"
ORDER_MIN_AMOUNT = "E102"
ORDER_INVALID_PRICE = "E103"
# Data Errors
DATA_FETCH_FAILED = "E201"
DATA_INVALID_FORMAT = "E202"
class TradingError(Exception):
def __init__(self, code: ErrorCode, message: str):
self.code = code
self.message = message
super().__init__(f"[{code.value}] {message}")
# 사용
raise TradingError(ErrorCode.ORDER_INSUFFICIENT_BALANCE, "잔고 부족: 10,000원 필요")
```
---
### [MEDIUM-004] 백테스팅 기능 부재
**제안**: 과거 데이터로 전략 검증
```python
# src/backtest.py
def run_backtest(symbols: list, start_date: str, end_date: str, config: dict):
"""백테스팅 실행"""
initial_balance = 1000000
balance = initial_balance
holdings = {}
for symbol in symbols:
df = fetch_historical_data(symbol, start_date, end_date)
for i in range(len(df)):
# 매수 신호 확인
if check_buy_signal(df.iloc[:i+1]):
# 매수 실행
...
# 매도 신호 확인
if symbol in holdings:
if check_sell_signal(df.iloc[:i+1], holdings[symbol]):
# 매도 실행
...
# 결과 리포트
final_balance = balance + sum(holdings.values())
profit_pct = ((final_balance - initial_balance) / initial_balance) * 100
return {
"initial": initial_balance,
"final": final_balance,
"profit_pct": profit_pct,
"trades": len(trade_history)
}
```
---
### [MEDIUM-005~012] 기타 개선 사항
- **MEDIUM-005**: `holdings.json` 백업 자동화 (일 1회)
- **MEDIUM-006**: 로그 파일 로테이션 (7일 이상 자동 삭제)
- **MEDIUM-007**: 성능 모니터링 (CPU/메모리 사용률 로깅)
- **MEDIUM-008**: 환경 변수 검증 강화 (`.env.example` 제공)
- **MEDIUM-009**: Docker 이미지 최적화 (Alpine Linux, 멀티 스테이지 빌드)
- **MEDIUM-010**: 주문 체결 대기 타임아웃 (현재 무한 대기)
- **MEDIUM-011**: 심볼별 독립적 설정 지원 (`symbols.json`)
- **MEDIUM-012**: Webhook 알림 추가 (Discord, Slack)
---
## 4. Low Priority / 코드 품질
### [LOW-001] Docstring 부족
**예시**:
```python
# ❌ Before
def evaluate_sell_conditions(current_price, buy_price, max_price, holding_info, config=None):
# 주석 없음
# ✅ After
def evaluate_sell_conditions(
current_price: float,
buy_price: float,
max_price: float,
holding_info: Dict[str, any],
config: Optional[Dict[str, any]] = None
) -> Dict[str, any]:
"""
매도 조건을 평가하고 매도 신호를 반환합니다.
매도 전략:
- 손절: 매수가 대비 -5% 하락 시 전량 매도
- 저수익 구간 (≤10%): 최고점 대비 -5% 하락 시 전량 매도
- 수익률 10% 달성 시: 50% 부분 매도 (1회 제한)
- 중간 구간 (10~30%): 수익률 10% 이하 복귀 시 전량 매도
- 고수익 구간 (>30%): 최고점 대비 -20% 하락 시 전량 매도
Args:
current_price: 현재 가격
buy_price: 매수 가격
max_price: 보유 기간 중 최고 가격
holding_info: 보유 정보 딕셔너리 (buy_price, amount, partial_sell_done 등)
config: 설정 딕셔너리 (선택, 손절률 커스터마이징)
Returns:
{"action": "hold"|"sell", "ratio": float, "reason": str}
"""
...
```
---
### [LOW-002~007] 기타 코드 품질
- **LOW-002**: 매직 넘버 상수화 (`0.05``STOP_LOSS_PCT = -5.0`)
- **LOW-003**: 긴 함수 분할 (`check_buy_conditions_for_symbol` 200줄 → 50줄씩 분할)
- **LOW-004**: 중복 코드 제거 (`load_json()`, `save_json()` 공통 함수)
- **LOW-005**: f-string 일관성 (`.format()` 혼용 → f-string 통일)
- **LOW-006**: 변수명 명확화 (`cfg``runtime_config`, `config``app_config`)
- **LOW-007**: 주석 개선 (영어 주석 → 한글 통일 또는 역)
---
## 5. 보안 및 안정성
### ✅ 양호한 점
1. **API 키 환경 변수 관리**: `.env` 파일 사용 (하드코딩 없음)
2. **Circuit Breaker 적용**: 연속 실패 시 자동 중단
3. **Dry-run 모드**: 테스트 환경 지원
### ⚠️ 개선 필요
1. **API 키 검증 부족**: 프로그램 시작 시 키 유효성 미검증 → 런타임 오류
```python
# main.py 시작 시 추가
if not config["dry_run"]:
valid, msg = validate_upbit_api_keys(access_key, secret_key)
if not valid:
logger.critical("Upbit API 키 오류: %s", msg)
send_telegram(f"🚨 프로그램 시작 실패\n{msg}")
sys.exit(1)
```
2. **민감 정보 로깅**: 주문 정보에 가격/수량 노출 → 로그 파일 유출 시 거래 내역 노출
```python
# ❌ Before
logger.info("[%s] 매수 주문: 가격 %.2f, 수량 %.8f", symbol, price, amount)
# ✅ After
logger.info("[%s] 매수 주문 실행 (ID: %s)", symbol, order_id) # 가격/수량 제거
```
3. **파일 권한 설정**: `holdings.json`, `config.json`은 소유자만 읽기/쓰기
```python
import os
os.chmod(HOLDINGS_FILE, 0o600) # rw-------
```
---
## 6. 성능 최적화
### [PERF-001] 불필요한 DataFrame 복사
**파일**: `src/indicators.py`
```python
# ❌ Before
def compute_sma(df, period=20):
df_copy = df.copy() # 불필요한 복사
df_copy["sma"] = df_copy["close"].rolling(period).mean()
return df_copy
# ✅ After
def compute_sma(df, period=20):
"""SMA 계산 (원본 수정 없음, 복사 최소화)"""
return df["close"].rolling(period).mean()
```
### [PERF-002] 멀티스레딩 개선
**문제**: `max_threads=3`은 I/O 바운드 작업에 부족
```python
# config.json
{
"max_threads": 10, // CPU 코어 수 * 2 권장
...
}
```
### [PERF-003] 로그 버퍼링
**문제**: 파일 I/O가 매번 발생
```python
# src/common.py
handler = logging.handlers.RotatingFileHandler(
log_file,
maxBytes=10*1024*1024,
backupCount=7,
encoding="utf-8"
)
handler.setFormatter(formatter)
handler.setLevel(logging.INFO)
# ✅ 버퍼링 추가
import logging.handlers
buffered_handler = logging.handlers.MemoryHandler(
capacity=100, # 100개 로그 모아서 쓰기
target=handler
)
logger.addHandler(buffered_handler)
```
---
## 7. 테스트 커버리지
### 현재 테스트 현황
- **테스트 파일**: `src/tests/test_*.py` (8개)
- **커버리지**: 약 60% (추정)
### 미커버 영역 (우선순위)
1. **매도 로직 엣지 케이스**:
- 수익률 9.99% → 10.01% 경계
- 최고가 갱신 후 즉시 하락
2. **동시성 시나리오**:
- 2개 스레드가 동시에 `save_holdings()` 호출
3. **API 오류 시뮬레이션**:
- Rate Limit 418 응답
- 타임아웃 후 재시도
4. **잔고 부족 시나리오**:
- 부분 매수 가능 금액
- 수수료 차감 후 최소 주문 금액 미달
### 테스트 추가 예시
```python
# src/tests/test_sell_edge_cases.py
def test_profit_taking_boundary():
"""수익률 10% 경계에서 부분 매도 테스트"""
# 9.99% → hold
result = evaluate_sell_conditions(10999, 10000, 10999, {"partial_sell_done": False})
assert result["action"] == "hold"
# 10.01% → 50% 매도
result = evaluate_sell_conditions(11001, 10000, 11001, {"partial_sell_done": False})
assert result["action"] == "sell"
assert result["ratio"] == 0.5
def test_concurrent_save_holdings():
"""holdings 동시 저장 테스트"""
import threading
def update_holding(symbol, price):
holdings = load_holdings()
holdings[symbol] = {"buy_price": price}
save_holdings(holdings)
threads = [
threading.Thread(target=update_holding, args=("BTC", 10000)),
threading.Thread(target=update_holding, args=("ETH", 2000))
]
for t in threads:
t.start()
for t in threads:
t.join()
# 두 심볼 모두 저장되어야 함
holdings = load_holdings()
assert "BTC" in holdings
assert "ETH" in holdings
```
---
## 8. 실행 가능한 개선 계획
### Phase 0: 긴급 핫픽스 (즉시, 1일)
1. **[CRITICAL-001]** Rate Limiter 구현
2. **[CRITICAL-002]** `update_max_price()` 추가
3. **[CRITICAL-003]** `save_holdings()` Lock 추가
### Phase 1: 핵심 로직 수정 (1주)
1. **[CRITICAL-004]** RSI/MACD 조건 개선
2. **[CRITICAL-005]** 부분 매수 지원
3. **[HIGH-001]** 타입 힌팅 추가
4. **[HIGH-002]** 예외 처리 개선
5. **[HIGH-004]** Bollinger Bands 로직 수정
6. **[HIGH-008]** 재매수 방지 기능
### Phase 2: 안정성 강화 (2주)
1. **[HIGH-005]** Circuit Breaker 임계값 조정
2. **[HIGH-006]** Decimal 기반 가격 계산
3. **[HIGH-007]** Telegram 메시지 분할
4. **[MEDIUM-001]** 설정 파일 검증
5. **[MEDIUM-002]** 캔들 데이터 캐싱
6. **보안 개선**: API 키 시작 시 검증, 파일 권한 설정
### Phase 3: 기능 확장 (1개월)
1. **[MEDIUM-004]** 백테스팅 기능
2. **[MEDIUM-005~012]** 운영 편의성 개선
3. **[LOW-001~007]** 코드 품질 개선
4. **테스트 커버리지** 80% 이상 달성
---
## 9. 결론
### 전체 평가
이 프로젝트는 **잘 구조화된 자동매매 시스템**이지만, **Critical Issues**가 실제 거래에서 손실을 초래할 수 있습니다.
### 핵심 권장사항
1. **즉시 수정**: Rate Limiter, 최고가 갱신, Thread-Safe 저장
2. **트레이딩 로직 검증**: RSI/MACD 조건 개선, 백테스팅 수행
3. **운영 안정성**: 타입 힌팅, 예외 처리, 로깅 개선
### 다음 단계
1. Phase 0 핫픽스 적용 → 즉시 배포
2. Phase 1 완료 후 시뮬레이션 모드 2주 운영
3. 실제 거래는 소액 (10만원)으로 1개월 테스트 후 확대
---
**작성자**: GitHub Copilot (Claude Sonnet 4.5)
**리뷰 기준**: SOLID 원칙, Clean Code, 트레이딩 모범 사례
**참고**: 이 리포트는 코드 정적 분석 기반이며, 실제 운영 데이터 분석은 별도 필요