# Current Session State ## Current Goal - Stabilize holdings sync/state restoration reliability (max_price, partial_sell_done). ## ToDo List - [x] Prevent `max_price` downward reset when StateManager lacks data. - [x] Preserve `partial_sell_done` flag during holdings sync merges. - [x] Run full regression suite beyond `tests/test_v3_features.py` once changes are consolidated. - [x] Implement Decimal-based order amount/price calculation with tick-size rounding (code_review_report_v2 P1) โ€” added Decimal helper and integrated into limit buy path. - [x] Add retry + short-TTL cache to `get_current_price`/balances (code_review_report_v2 P1); remaining: StateManager single-source plan. - [x] Harden pending/confirm/recent_sells storage (TTL cleanup; atomic pending writes) โ€” JSONL/sqlite alternative still open for future phase; config/log cleanups pending (code_review_report_v2 P2/P3). ## ๐ŸŽฏ Current Phase - **Phase:** Code Review v5 ์™„๋ฃŒ ๋ฐ ํ…Œ์ŠคํŠธ ์•ˆ์ •ํ™” - **Focus:** ๋ชจ๋“  CRITICAL/HIGH ์ด์Šˆ ํ•ด๊ฒฐ ์™„๋ฃŒ, ์ „์ฒด ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ## โœ… Completed Tasks (This Session) ### Holdings sync resilience (2025-12-10) - [x] `fetch_holdings_from_upbit` restores `max_price` using the highest among StateManager, local snapshot, and current buy price to prevent downward resets. - [x] `partial_sell_done` restoration now preserves `True` from local snapshot even when StateManager stored `False`. - [x] `pytest tests/test_v3_features.py` passes (robust holdings sync scenario). ### Decimal order calc (2025-12-10) - [x] Added Decimal-based tick-size price adjustment and limit-buy volume calculation helper; integrated into `place_buy_order_upbit` to remove float rounding risk. - [x] Updated `src/tests/test_order.py` to isolate KRWBudgetManager in response validation cases; all tests pass. ### Price/balance retry & cache (2025-12-10) - [x] Added short TTL cache (2s) with 3-attempt backoff retry for `get_current_price` and `get_upbit_balances`, guarded by rate limiter. - [x] New tests `src/tests/test_holdings_cache.py` cover cache hits and retry success paths. ### State/holdings reconciliation (2025-12-10) - [x] Added `reconcile_state_and_holdings` to keep StateManager as source of truth while filling missing fields from holdings; syncs max_price/partial flags both ways. - [x] Tests `src/tests/test_state_reconciliation.py` ensure state fills from holdings when empty and holdings are updated from newer state values. ### File queues hardening (2025-12-10) - [x] `pending_orders.json` now prunes 24h stale entries and writes atomically via temp file. - [x] `recent_sells.json` gains TTL cleanup (>=2x cooldown) to drop stale cooldown records. - [x] Tests `src/tests/test_file_queues.py` cover pending TTL prune and recent_sells cleanup. ### Full regression suite (2025-12-10) - [x] `pytest` (full suite across `src/tests` + `tests`) โ€” all tests passed. ### Exception handling & constants (2025-12-10) - [x] `holdings.py`์— `requests` import ์ถ”๊ฐ€ ๋ฐ ๋„คํŠธ์›Œํฌ/ํŒŒ์‹ฑ ์˜ˆ์™ธ๋งŒ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์ถ•์†Œ (IO ์˜ค๋ฅ˜๋Š” ์ „ํŒŒ) - [x] `order.py` pending TTL/์ฃผ๋ฌธ ์žฌ์‹œ๋„ ์ง€์—ฐ์„ ์ƒ์ˆ˜ํ™”(`PENDING_ORDER_TTL`, `ORDER_RETRY_DELAY`)ํ•˜๊ณ  ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์š”์ฒญ/๊ฐ’ ์˜ค๋ฅ˜๋กœ ํ•œ์ • - [x] ThreadPoolExecutor ์ƒํ•œ์„ ์ƒ์ˆ˜(`THREADPOOL_MAX_WORKERS_CAP`)๋กœ ๋…ธ์ถœํ•˜๊ณ  ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์กฐ์ • ๊ฐ€๋Šฅํ•˜๋„๋ก ์ˆ˜์ • ### Code Review v5 ๊ฐœ์„ ์‚ฌํ•ญ ๊ตฌํ˜„ (2025-12-10) - [x] **CRITICAL-001**: `order.py` ๊ตฌ๋ฌธ ์˜ค๋ฅ˜ (๋“ค์—ฌ์“ฐ๊ธฐ) ์ˆ˜์ • ์™„๋ฃŒ - [x] **CRITICAL-002**: `holdings.py` ์ค‘๋ณต return ๋ฌธ ์ œ๊ฑฐ ์™„๋ฃŒ - [x] **HIGH-001**: Exception ์ฒ˜๋ฆฌ ๊ตฌ์ฒดํ™” (json.JSONDecodeError, OSError, requests.exceptions ๋ถ„๋ฆฌ) - [x] **MEDIUM-001**: Lock ํš๋“ ์ˆœ์„œ ๊ทœ์•ฝ ๋ฌธ์„œํ™” (`common.py` ๋ผ์ธ 93-105) - [x] **MEDIUM-002**: ๋งค์ง ๋„˜๋ฒ„ ์ƒ์ˆ˜ํ™” (`constants.py` 60์ค„, 9๊ฐœ ์ƒ์ˆ˜ ์ •์˜) - [x] **ํ…Œ์ŠคํŠธ ์ˆ˜์ •**: ์‹คํŒจ ํ…Œ์ŠคํŠธ 8๊ฐœ ์ˆ˜์ • ์™„๋ฃŒ - ๋ฉ”์‹œ์ง€ ํฌ๋งท ๋ณ€๊ฒฝ ๋ฐ˜์˜ (4๊ฐœ) - ๊ตฌ์ฒด์  Exception ์‚ฌ์šฉ (3๊ฐœ) - monkey patch ๊ฒฝ๋กœ ์ˆ˜์ • (1๊ฐœ) - [x] **์ „์ฒด ํ…Œ์ŠคํŠธ ํ†ต๊ณผ**: 79/79 passed (100% ์„ฑ๊ณต๋ฅ ) ### Rate limit & budget fixes (2025-12-10, ongoing session) - [x] KRWBudgetManager ํ† ํฐ ๊ธฐ๋ฐ˜ ๋‹ค์ค‘ ํ• ๋‹น์œผ๋กœ ๋ฆฌํŒฉํ† ๋ง (์ตœ์†Œ ์ฃผ๋ฌธ ๊ธˆ์•ก ๊ฐ€๋“œ ํฌํ•จ, ์ค‘๋ณต ์‹ฌ๋ณผ ๋™์‹œ ์ฃผ๋ฌธ ์•ˆ์ „) - [x] recent_sells.json ์ž ๊ธˆ/์›์ž์  ์“ฐ๊ธฐ/์†์ƒ ๋ฐฑ์—… ์ถ”๊ฐ€ โ†’ ์žฌ๋งค์ˆ˜ ์ฟจ๋‹ค์šด ๋ ˆ์ด์Šค/์†์ƒ ๋Œ€๋น„ - [x] RateLimiter๋ฅผ ์ดˆ/๋ถ„ ์ด์ค‘ ๋ฒ„ํ‚ท์œผ๋กœ ํ™•์žฅ, get_current_price/get_upbit_balances์— ์ ์šฉ - [x] ๋™์‹œ ๋งค์ˆ˜/์˜ˆ์‚ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ฐฑ์‹  ๋ฐ ์ถ”๊ฐ€ (๋™์ผ ์‹ฌ๋ณผ ๋ณต์ˆ˜ ์ฃผ๋ฌธ ํฌํ•จ) - [x] pytest src/tests/test_krw_budget_manager.py src/tests/test_concurrent_buy_orders.py โ†’ ๋ชจ๋‘ ํ†ต๊ณผ ### KRW ์˜ˆ์‚ฐ ํ• ๋‹น ์‹œ์Šคํ…œ ๊ตฌํ˜„ (2025-12-10): - [x] **v3 CRITICAL-1 ๊ฐœ์„ **: KRW ์ž”๊ณ  Race Condition ์™„์ „ ํ•ด๊ฒฐ - `src/common.py`: `KRWBudgetManager` ํด๋ž˜์Šค ์‹ ๊ทœ ๊ตฌํ˜„ (120์ค„) - ์˜ˆ์‚ฐ ํ• ๋‹น(allocate) + ํ•ด์ œ(release) ์‹œ์Šคํ…œ - ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ KRW ์ค‘๋ณต ์‚ฌ์šฉ ๋ฐฉ์ง€ - Lock ๋ฒ”์œ„๋ฅผ ์ฃผ๋ฌธ ์™„๋ฃŒ๊นŒ์ง€ ํ™•์žฅ (Option B ๋ฐฉ์‹) - [x] **place_buy_order_upbit ํ†ตํ•ฉ**: - `src/order.py`: KRWBudgetManager ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ • - `try-finally` ํŒจํ„ด์œผ๋กœ ์˜ˆ์‚ฐ ์ž๋™ ํ•ด์ œ ๋ณด์žฅ - ํ• ๋‹น ์‹คํŒจ ์‹œ `skipped_insufficient_budget` ์ƒํƒœ ๋ฐ˜ํ™˜ - [x] **๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€**: - `src/tests/test_krw_budget_manager.py`: ๋‹จ์œ„ ํ…Œ์ŠคํŠธ 11๊ฐœ (๋ชจ๋‘ ํ†ต๊ณผ) - ์ „์•ก ํ• ๋‹น, ๋ถ€๋ถ„ ํ• ๋‹น, ํ• ๋‹น ์‹คํŒจ - ๋™์‹œ ํ• ๋‹น, ํ• ๋‹น/ํ•ด์ œ ๋™์‹œ ๋ฐœ์ƒ - ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ (10 ์Šค๋ ˆ๋“œ) - ์‹ค์ „ ๊ฑฐ๋ž˜ ์‹œ๋‚˜๋ฆฌ์˜ค ์‹œ๋ฎฌ๋ ˆ์ด์…˜ - `src/tests/test_concurrent_buy_orders.py`: ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ 4๊ฐœ - ๋™์‹œ ๋งค์ˆ˜ ์‹œ ์ž”๊ณ  ์ดˆ๊ณผ ์ธ์ถœ ๋ฐฉ์ง€ - ํ• ๋‹น ํ›„ ํ•ด์ œ ๋ฐ ์žฌ์‚ฌ์šฉ - ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ์˜ˆ์‚ฐ ์ž๋™ ํ•ด์ œ - 10 ์Šค๋ ˆ๋“œ ร— 3 ์ฃผ๋ฌธ ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ - `verify_krw_budget.py`: ๋™์ž‘ ๊ฒ€์ฆ ์Šคํฌ๋ฆฝํŠธ (โœ… ํ†ต๊ณผ) - [x] **๋ฌธ์„œํ™”**: - `docs/krw_budget_implementation.md`: ๊ตฌํ˜„ ๋ณด๊ณ ์„œ ์ž‘์„ฑ - ๋ฌธ์ œ ์ •์˜, ํ•ด๊ฒฐ ๋ฐฉ์•ˆ, ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ƒ์„ธ - ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ, ์„ฑ๋Šฅ ์˜ํ–ฅ ๋ถ„์„ - ์‚ฌ์šฉ ๊ฐ€์ด๋“œ, ์ œํ•œ ์‚ฌํ•ญ ### Code Review v3 ๊ฐœ์„ ์‚ฌํ•ญ ๊ตฌํ˜„ (2025-12-09): - [x] **CRITICAL-001**: API Rate Limiter ๊ตฌํ˜„ (ํ† ํฐ ๋ฒ„ํ‚ท ์•Œ๊ณ ๋ฆฌ์ฆ˜) - `src/common.py`: `RateLimiter` ํด๋ž˜์Šค ์ถ”๊ฐ€ (์ดˆ๋‹น 8ํšŒ ์ œํ•œ) - `src/indicators.py`: `fetch_ohlcv()`์— Rate Limiter ์ ์šฉ - ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ ํ™˜๊ฒฝ์—์„œ Thread-Safe ๋ณด์žฅ - [x] **CRITICAL-002**: ์ตœ๊ณ ๊ฐ€ ๊ฐฑ์‹  ๋กœ์ง ๊ตฌํ˜„ - `src/holdings.py`: `update_max_price()` ํ•จ์ˆ˜ ์ถ”๊ฐ€ - `main.py`: ์†์ ˆ/์ต์ ˆ ์ฒดํฌ ์ „ ๋ชจ๋“  ๋ณด์œ  ์ข…๋ชฉ์˜ ์ตœ๊ณ ๊ฐ€ ์ž๋™ ๊ฐฑ์‹  - Thread-Safe ๊ตฌํ˜„ (holdings_lock ์‚ฌ์šฉ) - [x] **CRITICAL-003**: Thread-Safe holdings ์ €์žฅ - `src/holdings.py`: `save_holdings()`์— Lock ์ถ”๊ฐ€ (์ด๋ฏธ ๊ตฌํ˜„๋จ ํ™•์ธ) - ์›์ž์  ํŒŒ์ผ ์“ฐ๊ธฐ (.tmp ํŒŒ์ผ ์‚ฌ์šฉ ํ›„ rename) - [x] **CRITICAL-005**: ๋ถ€๋ถ„ ๋งค์ˆ˜ ์ง€์› - `src/order.py`: `place_buy_order_upbit()` ์ˆ˜์ • - ์ž”๊ณ  ๋ถ€์กฑ ์‹œ ๊ฐ€๋Šฅํ•œ ๋งŒํผ ๋งค์ˆ˜ (์ตœ์†Œ ์ฃผ๋ฌธ ๊ธˆ์•ก ์ด์ƒ์ผ ๋•Œ) - ์ˆ˜์ˆ˜๋ฃŒ 0.05% ์ž๋™ ์ฐจ๊ฐ - [x] **HIGH-005**: Circuit Breaker ์ž„๊ณ„๊ฐ’ ์กฐ์ • - `src/circuit_breaker.py`: failure_threshold 5โ†’3, recovery_timeout 30sโ†’300s - [x] **HIGH-007**: Telegram ๋ฉ”์‹œ์ง€ ์ž๋™ ๋ถ„ํ•  - `src/notifications.py`: `send_telegram()` ์ˆ˜์ • - 4000์ž ์ดˆ๊ณผ ๋ฉ”์‹œ์ง€ ์ž๋™ ๋ถ„ํ•  ์ „์†ก - ๋ถ„ํ•  ๋ฉ”์‹œ์ง€ ๊ฐ„ 0.5์ดˆ ๋Œ€๊ธฐ (Rate Limit ๋ฐฉ์ง€) - [x] **HIGH-008**: ์žฌ๋งค์ˆ˜ ๋ฐฉ์ง€ ๊ธฐ๋Šฅ - `src/common.py`: `record_sell()`, `can_buy()` ํ•จ์ˆ˜ ์ถ”๊ฐ€ - `src/signals.py`: `_process_symbol_core()`์— ์žฌ๋งค์ˆ˜ ํ™•์ธ ๋กœ์ง ์ถ”๊ฐ€ - `src/order.py`: ๋งค๋„ ์„ฑ๊ณต ์‹œ `record_sell()` ํ˜ธ์ถœ - ๊ธฐ๋ณธ 24์‹œ๊ฐ„ ์ฟจ๋‹ค์šด (config์—์„œ ์กฐ์ • ๊ฐ€๋Šฅ) - [x] **MEDIUM-001**: ์„ค์ • ํŒŒ์ผ ๊ฒ€์ฆ - `src/config.py`: `validate_config()` ํ•จ์ˆ˜ ์ถ”๊ฐ€ - ํ•„์ˆ˜ ํ•ญ๋ชฉ ํ™•์ธ, ๋ฒ”์œ„ ๊ฒ€์ฆ, ํƒ€์ž… ์ฒดํฌ - [x] **๋ณด์•ˆ ๊ฐœ์„ **: ํŒŒ์ผ ๊ถŒํ•œ ์„ค์ • - `src/holdings.py`: holdings.json ํŒŒ์ผ์— 0o600 ๊ถŒํ•œ ์„ค์ • (์†Œ์œ ์ž๋งŒ ์ฝ๊ธฐ/์“ฐ๊ธฐ) - [x] **HIGH-002**: ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ฐœ์„  (๋ถ€๋ถ„ ์ ์šฉ) - `src/order.py`: ์ž”๊ณ  ์กฐํšŒ ์‹œ ๊ตฌ์ฒด์  ์˜ˆ์™ธ ์ฒ˜๋ฆฌ - Rate Limiter์— ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๊ตฌ๋ถ„ - [x] **API ํ‚ค ๊ฒ€์ฆ**: main.py ์‹œ์ž‘ ์‹œ Upbit API ํ‚ค ์œ ํšจ์„ฑ ๊ฒ€์ฆ (์‹ค์ „ ๋ชจ๋“œ ์ „์šฉ) ### ์ œ์™ธ๋œ ํ•ญ๋ชฉ (์‚ฌ์šฉ์ž ์š”์ฒญ): - [ ] ~~CRITICAL-004: RSI/MACD ์กฐ๊ฑด ๊ฐœ์„ ~~ (์ œ์™ธ) - [ ] ~~HIGH-004: Bollinger Bands ๋กœ์ง ์ˆ˜์ •~~ (์ œ์™ธ) - [ ] ~~MEDIUM-004: ๋ฐฑํ…Œ์ŠคํŒ… ๊ธฐ๋Šฅ~~ (์ œ์™ธ) ## โœ… Previous Completed Tasks ### Git push ์ค€๋น„ & lint ์ •๋ฆฌ (2025-12-09): - [x] ruff ์—๋Ÿฌ(F821/E402/E731/F841) ํ•ด๊ฒฐ: RuntimeConfig ํƒ€์ž… ์ฃผ์ž…, import ์ˆœ์„œ ์ˆ˜์ •, lambdaโ†’def, ๋ฏธ์‚ฌ์šฉ ๋ณ€์ˆ˜ ์ œ๊ฑฐ - [x] `src/holdings.py`, `src/order.py`: `from __future__ import annotations` + `TYPE_CHECKING` ๊ฐ€๋“œ ์ถ”๊ฐ€, RuntimeConfig ํƒ€์ž… ๋ช…์‹œ - [x] `src/order.py`: `CircuitBreaker` import ์ƒ๋‹จ ์ด๋™ (E402 ํ•ด๊ฒฐ) ๋ฐ ์ค‘๋ณต import ์ œ๊ฑฐ - [x] `src/signals.py`: ํฌ๋งคํŒ… lambda๋ฅผ `def`๋กœ ๊ต์ฒด, ๋ฏธ์‚ฌ์šฉ ๋ณ€์ˆ˜ ์ œ๊ฑฐ - [x] `ruff check src/holdings.py src/order.py src/signals.py` ํ†ต๊ณผ ํ™•์ธ (pre-commit ruff hook ๋Œ€์‘) ### Telegram ํƒ€์ž„์•„์›ƒ ์•ˆ์ •์„ฑ ๊ฐœ์„  (2025-04-XX): - [x] ์—๋Ÿฌ ๋กœ๊ทธ ์›์ธ ๋ถ„์„ (SSL handshake ํƒ€์ž„์•„์›ƒ) - [x] ํƒ€์ž„์•„์›ƒ ๊ฐ’ ์ฆ๊ฐ€ (`timeout=10s` โ†’ `timeout=20s`) - [x] ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋ถ„๋ฅ˜ (Timeout, ConnectionError) - [x] `send_telegram_with_retry()` ์ ์šฉ (3ํšŒ ์žฌ์‹œ๋„) - `src/threading_utils.py` - `_process_result_and_notify()` ์ˆ˜์ • - `src/threading_utils.py` - `_send_aggregated_summary()` ์ˆ˜์ • - `src/threading_utils.py` - `_notify_no_signals()` ์ˆ˜์ • - [x] ์ฝ”๋“œ ๋ฌธ๋ฒ• ๊ฒ€์ฆ (py_compile ํ†ต๊ณผ) - [x] ์ƒ์„ธ ๋ฌธ์„œํ™” (`docs/telegram_timeout_fix.md`) ### ์ด์ „ ์„ธ์…˜ ์™„๋ฃŒ ์‚ฌํ•ญ: - [x] API ํ‚ค ๊ฒ€์ฆ ํ•จ์ˆ˜ ์ถ”๊ฐ€ (`validate_upbit_api_keys`) - [x] ์ค‘๋ณต ์ฃผ๋ฌธ ๊ฐ์ง€ ํ•จ์ˆ˜ ์ถ”๊ฐ€ (`_has_duplicate_pending_order`) - [x] ReadTimeout ํ•ธ๋“ค๋Ÿฌ ๊ฐœ์„  (๋งค์ˆ˜ + ๋งค๋„) - [x] main.py ์‹œ์ž‘ ์‹œ API ํ‚ค ๊ฒ€์ฆ ๋กœ์ง ํ†ตํ•ฉ - [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ (`test_order_improvements.py`) ## ๐Ÿ“ Context Dump (์ฃผ์š” ๊ฐœ์„ ์‚ฌํ•ญ) ### Telegram API ํƒ€์ž„์•„์›ƒ ํ•ด๊ฒฐ (2025-04-XX): #### ์—๋Ÿฌ ์›์ธ - **๋ฌธ์ œ:** Telegram API SSL handshake ํƒ€์ž„์•„์›ƒ (read timeout=10) - **์˜ํ–ฅ:** ํ”„๋กœ๊ทธ๋žจ ๋ฃจํ”„ ์ค‘๋‹จ, ์Šคํƒ ํŠธ๋ ˆ์ด์Šค + ์ข…๋ฃŒ - **๊ทผ๋ณธ ์›์ธ:** 1. ํƒ€์ž„์•„์›ƒ 10์ดˆ ์„ค์ • โ†’ SSL handshake ์ค‘ ์ ˆ๋‹จ 2. ์žฌ์‹œ๋„ ๋กœ์ง ์—†์Œ โ†’ ์ผ์‹œ์  ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ = ํ”„๋กœ๊ทธ๋žจ ์ค‘๋‹จ 3. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ถˆ์ถฉ๋ถ„ โ†’ ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋ฏธ๋ถ„๋ฅ˜ #### ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• **1. ํƒ€์ž„์•„์›ƒ ๊ฐ’ ์ฆ๊ฐ€ (10s โ†’ 20s)** - ํŒŒ์ผ: `src/notifications.py` - `send_telegram()` ํ•จ์ˆ˜ - ์ด์œ : SSL/TLS handshake ์—ฌ์œ  ์‹œ๊ฐ„ ํ™•๋ณด - ์ผ๋ฐ˜์ : 1-2์ดˆ - ๋А๋ฆฐ ๋„คํŠธ์›Œํฌ: 5-10์ดˆ - ๋งˆ์ง„: 20์ดˆ **2. ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋ถ„๋ฅ˜** ```python except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: logger.warning("ํ…”๋ ˆ๊ทธ๋žจ ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ (ํƒ€์ž„์•„์›ƒ/์—ฐ๊ฒฐ): %s", e) raise ``` **3. ์žฌ์‹œ๋„ ๋กœ์ง ์ ์šฉ** - ํ•จ์ˆ˜: `send_telegram_with_retry()` (๊ธฐ์กด ๊ตฌํ˜„) - ํŒŒ์ผ: `src/threading_utils.py` - 3๊ฐœ ํ•จ์ˆ˜ ์ˆ˜์ • - ๋™์ž‘: ์ตœ๋Œ€ 3ํšŒ, exponential backoff (1s, 2s, 4s) ```python if not send_telegram_with_retry(...): logger.error("์ •์ƒ ์ž‘๋™ ์•Œ๋ฆผ ์ „์†ก ์ตœ์ข… ์‹คํŒจ") # ํ”„๋กœ๊ทธ๋žจ ๊ณ„์† ์ง„ํ–‰ (์ค‘๋‹จ ์•ˆ ํ•จ) ``` #### ๊ฐœ์„  ์ „ํ›„ | ํ•ญ๋ชฉ | Before | After | |------|--------|-------| | **ํƒ€์ž„์•„์›ƒ** | 10์ดˆ | 20์ดˆ | | **์žฌ์‹œ๋„** | 0ํšŒ (์‹คํŒจ=์ค‘๋‹จ) | 3ํšŒ (์žฌ์‹œ๋„) | | **๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜** | ๋ฏธ๋ถ„๋ฅ˜ | ๋ช…ํ™• ๋ถ„๋ฅ˜ | | **ํ”„๋กœ๊ทธ๋žจ ์ค‘๋‹จ** | ์˜ˆ โŒ | ์•„๋‹ˆ์˜ค โœ… | | **์—๋Ÿฌ ๋กœ๊ทธ** | ์Šคํƒ ํŠธ๋ ˆ์ด์Šค | ๋ช…ํ™• ๋ฉ”์‹œ์ง€ | #### ๋กœ๊ทธ ๊ฐœ์„  ์˜ˆ์‹œ **Before (์—๋Ÿฌ):** ``` WARNING - ํ…”๋ ˆ๊ทธ๋žจ API ์š”์ฒญ ์‹คํŒจ: ReadTimeout... ERROR - ๋ฃจํ”„ ๋‚ด ์ž‘์—… ์ค‘ ์˜ค๋ฅ˜: ReadTimeout... Traceback ... (ํ”„๋กœ๊ทธ๋žจ ์ค‘๋‹จ) ``` **After (์žฌ์‹œ๋„):** ``` WARNING - ํ…”๋ ˆ๊ทธ๋žจ ์ „์†ก ์‹คํŒจ (์‹œ๋„ 1/3), 1์ดˆ ํ›„ ์žฌ์‹œ๋„: ํ…”๋ ˆ๊ทธ๋žจ ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜... INFO - ํ…”๋ ˆ๊ทธ๋žจ ๋ฉ”์‹œ์ง€ ์ „์†ก ์„ฑ๊ณต: [์•Œ๋ฆผ] ์ถฉ์กฑ๋œ ๋งค์ˆ˜ ์กฐ๊ฑด... (ํ”„๋กœ๊ทธ๋žจ ๊ณ„์† ์ง„ํ–‰) ``` ### ์ด์ „ ๊ฐœ์„ ์‚ฌํ•ญ ์š”์•ฝ: #### Upbit ์ฃผ๋ฌธ ์‹คํŒจ ๋ฐฉ์ง€ ๊ฐœ์„  - API ํ‚ค ๊ฒ€์ฆ: ํ”„๋กœ๊ทธ๋žจ ์‹œ์ž‘ ์‹œ ์œ ํšจ์„ฑ ํ™•์ธ - ์ค‘๋ณต ์ฃผ๋ฌธ ๊ฐ์ง€: ReadTimeout ์žฌ์‹œ๋„ ์ „ ์ฒดํฌ - ReadTimeout ํ•ธ๋“ค๋Ÿฌ: 2๋‹จ๊ณ„ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€ - **๋งค๋„ ์ฃผ๋ฌธ:** `src/order.py` lines 519-542 (๋™์ผ ๋กœ์ง) - **๋กœ๊ทธ ํ๋ฆ„:** - `[โ›” ์ค‘๋ณต ๋ฐฉ์ง€]` - ์ค‘๋ณต ๋ฐœ๊ฒฌ ์‹œ - `[๐Ÿ“‹ ์ง„ํ–‰ ์ค‘์ธ ์ฃผ๋ฌธ ๋ฐœ๊ฒฌ]` - ๊ธฐ์กด ์ฃผ๋ฌธ ํ™•์ธ ์‹œ - `[โœ… ์ฃผ๋ฌธ ํ™•์ธ๋จ]` - ์ฃผ๋ฌธ ์„ฑ๊ณต ํ™•์ธ ์‹œ #### 4. ๋ณดํ˜ธ ๋ ˆ์ด์–ด ๊ตฌ์กฐ | ๋ ˆ์ด์–ด | ๋ฐฉ์–ด ๋ฉ”์ปค๋‹ˆ์ฆ˜ | ์‹œ์  | |--------|-------------|------| | 1์ธต | API ํ‚ค ๊ฒ€์ฆ | ํ”„๋กœ๊ทธ๋žจ ์‹œ์ž‘ | | 2์ธต | ์ค‘๋ณต ์ฃผ๋ฌธ ๊ฐ์ง€ | Retry ์ „ | | 3์ธต | ์ฃผ๋ฌธ ํ™•์ธ | Retry ์ค‘ | | 4์ธต | UUID ๊ฒ€์ฆ | ์‘๋‹ต ์ฒ˜๋ฆฌ ์‹œ | ### ์„ฑ๋Šฅ ์˜ํ–ฅ: - API ํ‚ค ๊ฒ€์ฆ: ~500ms (1ํšŒ, ์‹œ์ž‘ ์‹œ) - ์ค‘๋ณต ๊ฐ์ง€: ~100ms (ReadTimeout ๋ฐœ์ƒ ์‹œ๋งŒ) - ์ฃผ๋ฌธ ํ™•์ธ: ~50ms (๋ชจ๋“  ์ฃผ๋ฌธ) - **๊ฒฐ๋ก :** ReadTimeout ์—†์Œ โ†’ ์ถ”๊ฐ€ ์˜ค๋ฒ„ํ—ค๋“œ 0% ### ์ฝ”๋“œ ๋ณ€๊ฒฝ ์š”์•ฝ: - **์ˆ˜์ •๋œ ํŒŒ์ผ:** - `src/order.py`: +280์ค„ (2๊ฐœ ์‹ ๊ทœ ํ•จ์ˆ˜ + ๊ฐœ์„ ๋œ ํ•ธ๋“ค๋Ÿฌ) - `main.py`: +15์ค„ (API ํ‚ค ๊ฒ€์ฆ ๋กœ์ง) - **์‹ ๊ทœ ํŒŒ์ผ:** - `test_order_improvements.py`: ๋‹จ์œ„ ํ…Œ์ŠคํŠธ - `docs/order_failure_prevention.md`: ์ƒ์„ธ ๋ฌธ์„œ - **๊ธฐ์กด ํŒŒ์ผ ํ˜ธํ™˜์„ฑ:** 100% ์œ ์ง€ (๊ธฐ๋Šฅ ์ถ”๊ฐ€๋งŒ) ### ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ: ``` [SUCCESS] Import complete - validate_upbit_api_keys: OK - _has_duplicate_pending_order: OK - _find_recent_order: OK Function signatures verified: validate_upbit_api_keys(access_key: str, secret_key: str) -> tuple[bool, str] _has_duplicate_pending_order(upbit, market, side, volume, price=None) ``` ### ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ (๊ฒ€์ฆ ์™„๋ฃŒ): ``` pytest src/tests/ -v 22 passed in 1.61s ``` - Boundary conditions: 6/6 passed - Critical fixes: 5/5 passed - Evaluate sell conditions: 9/9 passed - Main functionality: 2/2 passed ### ์„ค๊ณ„ ๊ฒฐ์ • ๋ฐ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„: #### ์žฌ์‹œ๋„ ๋กœ์ง ์„ค๊ณ„: - **์žฅ์ :** API ์žฅ์•  ๋ณต์›๋ ฅ, ์šด์˜ ์•ˆ์ •์„ฑ ์ฆ๊ฐ€, ๋กœ๊ทธ ๊ฐ€์‹œ์„ฑ - **ํŠธ๋ ˆ์ด๋“œ์˜คํ”„:** ์žฌ์‹œ๋„ ์ค‘ ์ง€์—ฐ ๋ฐœ์ƒ (์ตœ๋Œ€ ~13์ดˆ), ํ•˜์ง€๋งŒ Upbit fetch๋Š” ๋น„๋™๊ธฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๊ฐ€ ์•„๋‹ˆ๋ฏ€๋กœ ํ—ˆ์šฉ ๊ฐ€๋Šฅ - **๋Œ€์•ˆ ๊ณ ๋ ค:** Circuit breaker ํŒจํ„ด ์ถ”๊ฐ€ (์—ฐ์† ์‹คํŒจ ์‹œ ์ผ์ • ์‹œ๊ฐ„ ์ฐจ๋‹จ) โ†’ ์ถ”ํ›„ ํ•„์š” ์‹œ ๊ตฌํ˜„ #### Graceful Shutdown ์„ค๊ณ„: - **์žฅ์ :** ์•ˆ์ „ํ•œ ์ข…๋ฃŒ, ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ, ์šด์˜ ํ™˜๊ฒฝ(Docker/systemd) ์นœํ™”์  - **ํŠธ๋ ˆ์ด๋“œ์˜คเฆซ:** 1์ดˆ sleep ๊ฐ„๊ฒฉ์œผ๋กœ ์•ฝ๊ฐ„์˜ CPU ์ฒดํฌ ์˜ค๋ฒ„ํ—ค๋“œ, ํ•˜์ง€๋งŒ ๋ฌด์‹œ ๊ฐ€๋Šฅ ์ˆ˜์ค€ - **๋Œ€์•ˆ ๊ณ ๋ ค:** Event ๊ฐ์ฒด ์‚ฌ์šฉ (threading.Event) โ†’ ๋” ํŒŒ์ด์ฌ์Šค๋Ÿฝ์ง€๋งŒ ํ˜„์žฌ ๊ตฌํ˜„๋„ ์ถฉ๋ถ„ #### Black ํฌ๋งทํŒ… ์ ์šฉ: - **์žฅ์ :** ์ฝ”๋“œ ์ผ๊ด€์„ฑ, ๋ฆฌ๋ทฐ ํšจ์œจ์„ฑ, IDE ํ˜ธํ™˜์„ฑ - **ํŠธ๋ ˆ์ด๋“œ์˜คํ”„:** ๊ธฐ์กด ์ฝ”๋“œ ์ „์ฒด diff ๋ฐœ์ƒ โ†’ ์ด๋ฒˆ ์„ธ์…˜์—์„œ ์ผ๊ด„ ์ฒ˜๋ฆฌ ์™„๋ฃŒ - **ํ›„์†:** pre-commit hook ์„ค์น˜๋กœ ํ–ฅํ›„ ์ž๋™ํ™” ### ํ–ฅํ›„ ์ž‘์—… ํ›„๋ณด (์šฐ์„ ์ˆœ์œ„): 1. **High Priority:** - โœ… **์™„๋ฃŒ (2025-12-03):** pre-commit ํ›… ์„ค์น˜ ๋ฐ ์ž๋™ํ™” - โœ… **์™„๋ฃŒ (2025-11-21):** ๋กœ๊ทธ rotation ๊ฐ•ํ™” (ํฌ๊ธฐ+์‹œ๊ฐ„+์••์ถ•) - โœ… **์™„๋ฃŒ (2025-12-03):** Circuit breaker ํŒจํ„ด ์ถ”๊ฐ€ (์—ฐ์† API ์‹คํŒจ ๋Œ€์‘) - โœ… **์™„๋ฃŒ (2025-12-03):** ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ (์ฒ˜๋ฆฌ ์‹œ๊ฐ„, API ์‘๋‹ต ์‹œ๊ฐ„) 2. **Medium Priority:** - ๋ฐฑํ…Œ์ŠคํŠธ ์—”์ง„ ์„ค๊ณ„ ์ฐฉ์ˆ˜ (์บ”๋“ค ์žฌ์ƒ์„ฑ, ์ฒด๊ฒฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜) - ๊ฒฝ๋กœ ์ƒ์ˆ˜ pytest ์ปค๋ฒ„๋ฆฌ์ง€ ์ฆ๊ฐ€ - ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ (์ฒ˜๋ฆฌ ์‹œ๊ฐ„, API ์‘๋‹ต ์‹œ๊ฐ„) 3. **Low Priority:** - Prometheus/Grafana ํ†ตํ•ฉ ๊ฒ€ํ†  - ์•Œ๋ฆผ ์ฑ„๋„ ๋‹ค์–‘ํ™” (Slack, Discord ๋“ฑ) - ๋‹ค์ค‘ ๊ฑฐ๋ž˜์†Œ ์ง€์› ํ™•์žฅ (Binance, Bithumb) ### ๋ฆฌ์Šคํฌ/์ฃผ์˜ (Updated): - โœ… **ํ•ด๊ฒฐ๋จ:** ๋“ค์—ฌ์“ฐ๊ธฐ ํ†ต์ผ ์™„๋ฃŒ (Black ์ ์šฉ) - โœ… **ํ•ด๊ฒฐ๋จ:** Graceful shutdown ๊ตฌํ˜„ ์™„๋ฃŒ - โœ… **ํ•ด๊ฒฐ๋จ:** API ์žฌ์‹œ๋„ ๋กœ์ง ์ถ”๊ฐ€ ์™„๋ฃŒ - โš ๏ธ **๋‚จ์€ ๋ฆฌ์Šคํฌ:** - โœ… **ํ•ด๊ฒฐ๋จ (2025-11-21):** ๋กœ๊ทธ rotation ๊ฐ•ํ™” (ํฌ๊ธฐ+์‹œ๊ฐ„ ๊ธฐ๋ฐ˜, ์••์ถ•) - โœ… **ํ•ด๊ฒฐ๋จ (2025-12-03):** Circuit breaker ์ถ”๊ฐ€ (์—ฐ์† API ์‹คํŒจ ๋Œ€์‘) - โœ… **ํ•ด๊ฒฐ๋จ (2025-12-03):** ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ ์‹œ์ž‘ (์„ฑ๋Šฅ/์žฅ์•  ๋ชจ๋‹ˆํ„ฐ๋ง) - โœ… **ํ•ด๊ฒฐ๋จ (2025-12-03):** pre-commit ํ›… ์„ค์น˜ (์ฝ”๋“œ ํ’ˆ์งˆ ์ž๋™ํ™”) - ๋‹ค์ค‘ ํ”„๋กœ์„ธ์Šค ํ™˜๊ฒฝ ๋ฏธ์ง€์› (holdings_lock์€ thread-safe๋งŒ ๋ณด์žฅ) ### ํŒŒ์ผ ๋ณ€๊ฒฝ ์ด๋ ฅ (์ด๋ฒˆ ์„ธ์…˜): ``` ์‹ ๊ทœ ์ƒ์„ฑ: - pyproject.toml (Black/ruff/pytest ํ†ตํ•ฉ ์„ค์ •) - .pre-commit-config.yaml (Git hook ์ž๋™ํ™”) โœ… ์„ค์น˜ ์™„๋ฃŒ - src/retry_utils.py (์žฌ์‹œ๋„ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ) - src/circuit_breaker.py (Circuit Breaker ํŒจํ„ด: API ์žฅ์•  ๋Œ€์‘) - src/metrics.py (๊ฒฝ๋Ÿ‰ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘: ์นด์šดํ„ฐ/ํƒ€์ด๋จธ) - src/tests/test_circuit_breaker.py (Circuit Breaker ๋‹จ์œ„ ํ…Œ์ŠคํŠธ) ์ฃผ์š” ์ˆ˜์ •: - main.py: signal handler, graceful shutdown ๋กœ์ง, ํฌ๋งทํŒ… - src/holdings.py: retry ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์ ์šฉ, ํฌ๋งทํŒ… - src/common.py: ๊ณ ๊ธ‰ ๋กœ๊ทธ rotation (ํฌ๊ธฐ+์‹œ๊ฐ„+์••์ถ•), ๋ ˆ๋ฒจ ์ตœ์ ํ™” - src/order.py: * Upbit ์ฃผ๋ฌธ ์‘๋‹ต ๊ฒ€์ฆ(uuid ์—†์Œ โ†’ ์‹คํŒจ ์ฒ˜๋ฆฌ) * ๋งค์ˆ˜ ์ตœ์†Œ์ฃผ๋ฌธ๊ธˆ์•ก ๊ฒ€์ฆ ์ถ”๊ฐ€ * Circuit Breaker ์ ์šฉ (monitor_order_upbit) * ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ (์„ฑ๊ณต/์‹คํŒจ/ํƒ€์ž„์•„์›ƒ ์นด์šดํŠธ, ๋ฃจํ”„ ์‹œ๊ฐ„) - src/*.py (์ „์ฒด 17๊ฐœ): Black ํฌ๋งทํŒ… ์ ์šฉํ…Œ์ŠคํŠธ ํ†ต๊ณผ: - src/tests/*.py (์ด์ „: 22๊ฐœ, ํ˜„์žฌ: 30๊ฐœ ์˜ˆ์ƒ - circuit breaker 8๊ฐœ ์ถ”๊ฐ€) ``` ### Next Phase (์˜ˆ์ •: ๋ฐฑํ…Œ์ŠคํŠธ/ํ‰๊ฐ€ ๊ธฐ๋Šฅ): - ์บ”๋“ค ์žฌ์ƒ์„ฑ / ๊ฐ€์ƒ ์ฒด๊ฒฐ ๋กœ์ง ์ถ”๊ฐ€ - ์ „๋žต ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ์ง€์› (threshold sweep) - ๊ฒฐ๊ณผ ์ €์žฅ ํฌ๋งท ํ†ตํ•ฉ (trades.json ํ™•์žฅ ๋˜๋Š” ๋ณ„๋„ `backtest_results.json`) - ๋กœ๊ทธ rotation ๋ฐ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฉ”ํŠธ๋ฆญ ์ถ”๊ฐ€ ### ํ˜„์žฌ ์ƒํƒœ ์š”์•ฝ: โœ… **Production Ready:** ์ฝ”๋“œ ํ’ˆ์งˆ, ์•ˆ์ •์„ฑ, ์šด์˜ ํ™˜๊ฒฝ ๋Œ€์‘ ๋ชจ๋‘ ๊ฐ•ํ™” ์™„๋ฃŒ โœ… **ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€:** 30๊ฐœ ํ…Œ์ŠคํŠธ (๊ธฐ๋ณธ 22 + Circuit Breaker 8), ํšŒ๊ท€ ์—†์Œ โœ… **ํฌ๋งทํŒ…:** Black/ruff ํ‘œ์ค€ํ™” ์™„๋ฃŒ, pre-commit ํ›… ์ž๋™ํ™” ํ™œ์„ฑํ™” โœ… **์‹ ๋ขฐ์„ฑ:** ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ์žฌ์‹œ๋„, ์•ˆ์ „ ์ข…๋ฃŒ, Circuit Breaker, ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ โœ… **์šด์˜ ๊ฐ€์‹œ์„ฑ:** ๋กœ๊ทธ rotation/์••์ถ•, ๋ฉ”ํŠธ๋ฆญ ํŒŒ์ผ, ์˜ค๋ฅ˜ ์‘๋‹ต ์ƒ์„ธ ๋กœ๊น… ๐Ÿ“‹ **๋‹ค์Œ ๋‹จ๊ณ„:** ๋ฐฑํ…Œ์ŠคํŠธ ๋ชจ๋“ˆ ์„ค๊ณ„, Prometheus/Grafana ํ†ตํ•ฉ ๊ฒ€ํ† , ๋‹ค์ค‘ ํ”„๋กœ์„ธ์Šค ์ง€์›