import json import os import threading from typing import Any from .common import DATA_DIR, logger # 상태 파일 경로 STATE_FILE = str(DATA_DIR / "bot_state.json") # 상태 파일 잠금 _state_lock = threading.RLock() def _load_state_unsafe() -> dict[str, Any]: """내부 사용 전용: Lock 없이 상태 파일 로드""" if os.path.exists(STATE_FILE): try: if os.path.getsize(STATE_FILE) == 0: return {} with open(STATE_FILE, encoding="utf-8") as f: return json.load(f) except json.JSONDecodeError: logger.warning("[StateManager] 상태 파일 손상됨, 빈 상태 반환: %s", STATE_FILE) return {} except OSError as e: logger.error("[StateManager] 상태 파일 읽기 실패: %s", e) return {} return {} def _save_state_unsafe(state: dict[str, Any]) -> None: """내부 사용 전용: Lock 없이 상태 파일 저장 (원자적)""" try: temp_file = f"{STATE_FILE}.tmp" with open(temp_file, "w", encoding="utf-8") as f: json.dump(state, f, indent=2, ensure_ascii=False) f.flush() os.fsync(f.fileno()) os.replace(temp_file, STATE_FILE) except (OSError, TypeError, ValueError) as e: logger.error("[StateManager] 상태 저장 실패: %s", e) def load_state() -> dict[str, Any]: """전체 봇 상태를 로드합니다.""" with _state_lock: return _load_state_unsafe() def save_state(state: dict[str, Any]) -> None: """전체 봇 상태를 저장합니다.""" with _state_lock: _save_state_unsafe(state) def get_value(symbol: str, key: str, default: Any = None) -> Any: """ 특정 심볼의 상태 값을 조회합니다. 예: get_value("KRW-BTC", "max_price", 0.0) """ with _state_lock: state = _load_state_unsafe() symbol_data = state.get(symbol, {}) return symbol_data.get(key, default) def set_value(symbol: str, key: str, value: Any) -> None: """ 특정 심볼의 상태 값을 설정하고 저장합니다. 예: set_value("KRW-BTC", "max_price", 100000000) """ with _state_lock: state = _load_state_unsafe() if symbol not in state: state[symbol] = {} state[symbol][key] = value _save_state_unsafe(state) logger.debug("[StateManager] 상태 업데이트: [%s] %s = %s", symbol, key, value) def update_max_price_state(symbol: str, current_price: float) -> float: """ 최고가(max_price)를 상태 파일에 업데이트합니다. 기존 값보다 클 경우에만 업데이트합니다. Returns: 업데이트된(또는 유지된) max_price """ with _state_lock: state = _load_state_unsafe() if symbol not in state: state[symbol] = {} old_max = float(state[symbol].get("max_price", 0.0) or 0.0) if current_price > old_max: state[symbol]["max_price"] = current_price _save_state_unsafe(state) logger.debug("[StateManager] [%s] max_price 갱신: %.2f -> %.2f", symbol, old_max, current_price) return current_price return old_max