# 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 ์ตœ์ข… ?๏ฟฝ์•ฝ ๏ฟฝ??๏ฟฝ์„ ?๏ฟฝ์œ„ ๋กœ๋“œ๏ฟฝ? ## ?๏ฟฝ๏ฟฝ ๊ฐœ์„  ๊ถŒ๊ณ ?๏ฟฝํ•ญ ?๏ฟฝ์„ ?๏ฟฝ์œ„ ### ?๏ฟฝ๏ฟฝ CRITICAL (์ฆ‰์‹œ ?๏ฟฝ์ • ?๏ฟฝ์š”) | ID | ??๏ฟฝ๏ฟฝ | ?๏ฟฝํ–ฅ??| ?๏ฟฝ์ƒ ?๏ฟฝ์—… ?๏ฟฝ๊ฐ„ | |----|------|--------|---------------| | **CRITICAL-003** | ์ค‘๋ณต ์ฃผ๋ฌธ ๊ฒ€๏ฟฝ?Timestamp ?๏ฟฝ๋ฝ | ?๏ฟฝ๊ฑฐ???๏ฟฝ์ต ?๏ฟฝ์‹ค | 2?๏ฟฝ๊ฐ„ | **๊ถŒ์žฅ ?๏ฟฝ์—… ?๏ฟฝ์„œ**: 1. `order.py`??`_has_duplicate_pending_order()` ?๏ฟฝ์ˆ˜??`lookback_sec=120` ?๏ฟฝ๋ผ๋ฏธํ„ฐ ์ถ”๏ฟฝ? 2. Timestamp ๋น„๊ต ๋กœ์ง ๊ตฌํ˜„ (created_at ?๏ฟฝ๋“œ ?๏ฟฝ์‹ฑ) 3. ?๏ฟฝ์Šค??์ผ€?๏ฟฝ์Šค ์ถ”๏ฟฝ? (?๏ฟฝ๊ฐ„ ๊ฒฝ๊ณ„ ์ผ€?๏ฟฝ์Šค) 4. ?๏ฟฝ์ œ ๊ฑฐ๋ž˜ ??Dry-run ๊ฒ€๏ฟฝ? --- ### ?๏ฟฝ๏ฟฝ HIGH (?๏ฟฝ๊ธฐ ??๊ฐœ์„  ๊ถŒ์žฅ) | ID | ??๏ฟฝ๏ฟฝ | ?๏ฟฝํ–ฅ??| ?๏ฟฝ์ƒ ?๏ฟฝ์—… ?๏ฟฝ๊ฐ„ | |----|------|--------|---------------| | **HIGH-001** | ?๏ฟฝํ™˜ import ?๏ฟฝ์žฌ ?๏ฟฝํ—˜ | ?๏ฟฝ๊ธฐ ?๏ฟฝ๏ฟฝ?๋ณด์ˆ˜??| 4?๏ฟฝ๊ฐ„ | | **HIGH-002** | ?๏ฟฝ์ • ๊ฒ€๏ฟฝ?๋ถ€๏ฟฝ?| ?๏ฟฝ์˜ ?๏ฟฝ๊ณ  ?๏ฟฝ๋ฐฉ | 2?๏ฟฝ๊ฐ„ | **๊ถŒ์žฅ ?๏ฟฝ๊ทผ**: - HIGH-001: ?๏ฟฝ์กด????๏ฟฝ๏ฟฝ ?๏ฟฝํ„ด ?๏ฟฝ์šฉ, ์ฝœ๋ฐฑ ๊ธฐ๋ฐ˜ ?๏ฟฝ๊ณ„๏ฟฝ?๋ฆฌํŒฉ?๏ฟฝ๋ง - HIGH-002: `validate_config()` ๊ฐ•ํ™”, ?๏ฟฝํ˜ธ ?๏ฟฝ์กด???๏ฟฝ๋ฆฌ??๋ชจ์ˆœ ๊ฒ€๏ฟฝ?์ถ”๏ฟฝ? --- ### ?๏ฟฝ๏ฟฝ MEDIUM (์ค‘๊ธฐ ๊ฐœ์„  ??๏ฟฝ๏ฟฝ) | ID | ??๏ฟฝ๏ฟฝ | ?๏ฟฝํ–ฅ??| ?๏ฟฝ์ƒ ?๏ฟฝ์—… ?๏ฟฝ๊ฐ„ | |----|------|--------|---------------| | **MEDIUM-004** | ThreadPoolExecutor ์ข…๋ฃŒ ์ฒ˜๋ฆฌ | ?๏ฟฝ์˜ ?๏ฟฝ์ •??| 3?๏ฟฝ๊ฐ„ | | **MEDIUM-006** | End-to-End ?๏ฟฝ์Šค??๋ถ€??| ?๏ฟฝ๏ฟฝ? ?๏ฟฝ์Šค??| 6?๏ฟฝ๊ฐ„ | **๊ถŒ์žฅ ?๏ฟฝ๊ทผ**: - MEDIUM-004: Signal handler ์ถ”๏ฟฝ?, graceful shutdown ๊ตฌํ˜„ - MEDIUM-006: ?๏ฟฝ์ฒด ๊ฑฐ๋ž˜ ?๏ฟฝ๋กœ???๏ฟฝํ•ฉ ?๏ฟฝ์Šค???๏ฟฝ์„ฑ --- ### ?๏ฟฝ๏ฟฝ LOW (?๏ฟฝ๊ธฐ ๊ฐœ์„  ??๏ฟฝ๏ฟฝ) | ID | ??๏ฟฝ๏ฟฝ | ?๏ฟฝํ–ฅ??| ?๏ฟฝ์ƒ ?๏ฟฝ์—… ?๏ฟฝ๊ฐ„ | |----|------|--------|---------------| | **LOW-001** | ๋กœ๊ทธ ?๏ฟฝ๋ฒจ ?๏ฟฝ๏ฟฝ???| ?๏ฟฝ๋ฒ„๏ฟฝ??๏ฟฝ์œจ | 1?๏ฟฝ๊ฐ„ | | **LOW-002** | f-string vs % ?๏ฟฝ๋งค???๏ฟฝ์ผ | ์ฝ”๋“œ ?๏ฟฝ๏ฟฝ???| 1?๏ฟฝ๊ฐ„ | | **LOW-005** | API ??๊ฒ€๏ฟฝ?๊ฐ•ํ™” | ๋ณด์•ˆ | 2?๏ฟฝ๊ฐ„ | | **LOW-006** | API ๋ฌธ์„œ ?๏ฟฝ์„ฑ | ๊ฐœ๋ฐœ ?๏ฟฝ์‚ฐ??| 4?๏ฟฝ๊ฐ„ | --- ## ?๏ฟฝ๏ฟฝ ๊ฒ€๏ฟฝ??๏ฟฝ๋ฃŒ ??๏ฟฝ๏ฟฝ | ??๏ฟฝ๏ฟฝ | ?๏ฟฝํƒœ | ๋น„๊ณ  | |------|------|------| | **OHLCV ์บ์‹œ** | ??๊ตฌํ˜„ ?๏ฟฝ๋ฃŒ | `indicators.py`???๏ฟฝ๏ฟฝ? ?๏ฟฝ์šฉ??(TTL 240๏ฟฝ? | | **v5 ๊ฐœ์„ ?๏ฟฝํ•ญ** | ??๋ชจ๋‘ ?๏ฟฝ์šฉ | CRITICAL-001, CRITICAL-002, HIGH-001, MEDIUM-001, MEDIUM-002 | --- ## ?? 3?๏ฟฝ๊ณ„ ?๏ฟฝํ–‰ ๊ณ„ํš ### Phase 1: ๊ธด๊ธ‰ (1์ฃผ์ผ) ``` 1. CRITICAL-003 ?๏ฟฝ์ • (2h) 2. HIGH-002 ๊ตฌํ˜„ (2h) 3. ?๏ฟฝ๏ฟฝ? ?๏ฟฝ์Šค??(1h) ๏ฟฝ??๏ฟฝ์š”: 5?๏ฟฝ๊ฐ„ ``` ### Phase 2: ?๏ฟฝ๊ธฐ (2์ฃผ์ผ) ``` 1. HIGH-001 ๋ฆฌํŒฉ?๏ฟฝ๋ง (4h) 2. MEDIUM-004 ๊ฐœ์„  (3h) 3. ?๏ฟฝํ•ฉ ?๏ฟฝ์Šค??(2h) ๏ฟฝ??๏ฟฝ์š”: 9?๏ฟฝ๊ฐ„ ``` ### Phase 3: ์ค‘์žฅ๏ฟฝ?(1๊ฐœ์›”) ``` 1. MEDIUM-006 E2E ?๏ฟฝ์Šค??(6h) 2. LOW ??๏ฟฝ๏ฟฝ ?๏ฟฝ๊ด„ ์ฒ˜๋ฆฌ (8h) 3. ๋ฌธ์„œ??(4h) ๏ฟฝ??๏ฟฝ์š”: 18?๏ฟฝ๊ฐ„ ``` --- ## ?๏ฟฝ๏ฟฝ ?๏ฟฝ์‹ฌ ?๏ฟฝ์ฐฐ 1. **?๏ฟฝํ‚ค?๏ฟฝ์ฒ˜ ?๏ฟฝ์ˆ˜??*: ๋ชจ๋“ˆ ๋ถ„๋ฆฌ, ?๏ฟฝ์‹œ???๏ฟฝ๊ณ„??Google/Meta ?๏ฟฝ๏ฟฝ? 2. **?๏ฟฝ์ „ ๊ฒ€๏ฟฝ??๏ฟฝ์š”**: CRITICAL-003?๏ฟฝ ?๏ฟฝ๊ฑฐ??์ง์ „ ๋ฐ˜๋“œ???๏ฟฝ์ • 3. **๊ธฐ์ˆ  ๋ถ€๏ฟฝ?๊ด€๏ฟฝ?*: HIGH/MEDIUM ??๏ฟฝ๏ฟฝ?๏ฟฝ ์ฝ”๋“œ ?๏ฟฝ์žฅ ???๏ฟฝ๊ฒฐ ๊ถŒ์žฅ 4. **์ง€?๏ฟฝ์  ๊ฐœ์„ **: ??? ?๏ฟฝ์„ ?๏ฟฝ์œ„ ??๏ฟฝ๏ฟฝ???๏ฟฝ์ง„??๊ฐœ์„ ?๏ฟฝ๋กœ ?๏ฟฝ์งˆ ?๏ฟฝ์ƒ --- ## ๋ณ€๏ฟฝ??๏ฟฝ๋ ฅ **v6.1 (2025-12-10)**: - ๊ฒ€?๏ฟฝ์˜๏ฟฝ?๋ฐ˜์˜: CRITICAL-003 ?๏ฟฝ๊ธ‰ ?๏ฟฝํ–ฅ (Medium ??Critical) - HIGH-001, HIGH-002 ?๏ฟฝ๊ธ‰ ?๏ฟฝํ–ฅ (Medium ??High) - MEDIUM-004 ?๏ฟฝ๊ธ‰ ?๏ฟฝํ–ฅ (Low ??Medium) - OHLCV ์บ์‹œ ๊ตฌํ˜„ ?๏ฟฝ์ธ ?๏ฟฝ๋ฃŒ, LOW-004 ??๏ฟฝ๏ฟฝ - ?๏ฟฝ์„ ?๏ฟฝ์œ„ ๋กœ๋“œ๏ฟฝ?๏ฟฝ?3?๏ฟฝ๊ณ„ ?๏ฟฝํ–‰ ๊ณ„ํš ์ถ”๏ฟฝ? **v6.0 (2025-12-10)**: - ์ตœ์ดˆ ?๏ฟฝ์„ฑ: 7๊ณ„์ธต ?๏ฟฝ์ธต ๋ถ„์„ ?๏ฟฝ๋ ˆ?๏ฟฝ์›Œ???๏ฟฝ์šฉ - 11๏ฟฝ?๊ฐœ์„  ??๏ฟฝ๏ฟฝ ?๏ฟฝ์ถœ (CRITICAL 1, HIGH 2, MEDIUM 2, LOW 6) - v5 ๊ฐœ์„ ?๏ฟฝํ•ญ ๊ฒ€๏ฟฝ??๏ฟฝ๋ฃŒ (5/5 ??๏ฟฝ๏ฟฝ)