# 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, 트레이딩 모범 사례 **참고**: 이 리포트는 코드 정적 분석 기반이며, 실제 운영 데이터 분석은 별도 필요