#!/usr/bin/env python3 """`.env` 완전성 및 config 로드 점검.""" from __future__ import annotations import os import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) ENV_FILE = ROOT / ".env" CONFIG_FILE = ROOT / "config.py" # config.py에서 읽는 환경 변수 키 (코드 기본값이 있는 항목) CONFIG_ENV_KEYS = { "BITHUMB_API_URL", "BITHUMB_API_CANDLE_COUNT", "BITHUMB_MINUTE_INTERVALS", "HTS_API_RETRY_SLEEP_SEC", "SYMBOL", "COIN_NAME", "DAILY_INTERVAL_MIN", "ENTRY_INTERVAL", "TREND_INTERVAL_1H", "TREND_INTERVAL_1D", "ALL_INTERVALS", "DOWNLOAD_INTERVALS", "GENERAL_ANALYSIS_INTERVALS", "TIMING_INTERVALS", "TREND_INTERVALS", "INTERVAL_PREFIX", "BB_PERIOD", "BB_STD", "BB_MIN_WIDTH_PCT", "RSI_PERIOD", "DISPARITY_PERIODS", "DISPARITY_OVERBOUGHT", "DISPARITY_OVERSOLD", "MACD_FAST", "MACD_SLOW", "MACD_SIGNAL", "STOCH_K_PERIOD", "STOCH_D_PERIOD", "STOCH_SMOOTH_K", "STOCH_OVERSOLD", "STOCH_OVERBOUGHT", "TREND_RANGE_MA_GAP_PCT", "ALIGN_RSI_OVERSOLD", "ALIGN_RSI_OVERBOUGHT", "ALIGN_RSI_CONFLICT_TIMING_LOW", "ALIGN_RSI_CONFLICT_TIMING_HIGH", "ALIGN_RSI_CONFLICT_TREND_LOW", "ALIGN_RSI_CONFLICT_TREND_HIGH", "ALIGN_BB_POS_LOW", "ALIGN_BB_POS_HIGH", "DOWNLOAD_MONTHS", "DOWNLOAD_MONTHS_1M", "INCREMENTAL_OVERLAP_BARS", "DOWNLOAD_BACKFILL_EXTRA_BARS", "DOWNLOAD_MIN_INCREMENTAL_BARS", "DOWNLOAD_DAILY_EXTRA_DAYS", "CHART_LOOKBACK_DAYS", "DB_READ_LIMIT_DEFAULT", "DB_ROW_WARMUP_BARS", "DB_ROW_MIN_DAILY_BARS", "DB_ROW_DAILY_PADDING_DAYS", "DB_PATH", "GROUND_TRUTH_FILE", "GT_UNLIMITED_CHRONOLOGICAL_DAYS", "GT_MIN_SWING_PCT", "GT_PIVOT_ORDER", "GT_MIN_BARS_BETWEEN", "GT_MAX_ROUND_TRIPS", "GT_SELECTION_MODE", "GT_MIN_LEG_PCT", "GT_BUY_MIN_SWING_PCT", "GT_BUY_BB_MAX", "GT_BUY_MIN_BARS", "GT_MAX_BUYS_PER_LEG", "GT_MAX_SELLS_PER_LEG", "GT_SELL_SPLIT_GAP_PCT", "GT_MARKER_SIZE_MIN", "GT_MARKER_SIZE_MAX", "GT_INITIAL_CASH_KRW", "TRADING_FEE_RATE", "MONITOR_LOOP_SLEEP_SEC", "MONITOR_POOL_WORKERS", "MONITOR_DEFAULT_INTERVAL", "MONITOR_API_RETRIES", "MONITOR_API_BONG_COUNT", "MONITOR_SLEEP_AFTER_REQUEST_SEC", "MONITOR_SLEEP_RATE_LIMIT_SEC", "MONITOR_SLEEP_BETWEEN_CHUNKS_SEC", "MONITOR_API_CHUNK_BARS", "MONITOR_MA_WINDOWS", "MONITOR_NORM_WINDOW", "MONITOR_TELEGRAM_BATCH_SIZE", "GA_COL_PREFIX", "LOOKBACK_BARS", "CONTEXT_TAIL_ROWS", "GA_DEFAULT_TAIL_EXPORT", "GA_PATTERN_TOLERANCE_PCT", "GA_VP_BINS", "GA_VP_VALUE_AREA_PCT", "GA_HV_ROLLING_BARS", "GA_HV_PERCENTILE_WINDOW", "GA_HV_ANNUALIZE_SQRT", "GA_DIVERGENCE_LOOKBACK", "GA_SMA_PERIODS", "GA_EMA_SPANS", "GA_ATR_PERIOD", "GA_KELTNER_ATR_MULT", "GA_AO_FAST", "GA_AO_SLOW", "GA_LINREG_WINDOW", "GA_ADX_PERIOD", "GA_ADX_TREND_THRESHOLD", "GA_SUPERTREND_ATR_MULT", "GA_VOL_SPIKE_MULT", "GA_VOL_MA_WINDOW", "GA_CCI_PERIOD", "GA_WILLIAMS_PERIOD", "GA_ROC_PERIOD", "GA_MFI_PERIOD", "GA_CMF_PERIOD", "GA_DONCHIAN_PERIOD", "GA_BB_SQUEEZE_WINDOW", "GA_BB_SQUEEZE_QUANTILE", "GA_PIVOT_ORDER", "GA_PSAR_AF_START", "GA_PSAR_AF_STEP", "GA_PSAR_AF_MAX", } # 비어 있어도 되는 선택 항목 (현재는 모두 채움) OPTIONAL_EMPTY = frozenset() def parse_env_file(path: Path) -> dict[str, str]: """`.env` 키=값 파싱.""" out: dict[str, str] = {} if not path.is_file(): return out for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#"): continue if "=" not in line: continue key, _, val = line.partition("=") out[key.strip()] = val.strip() return out def main() -> int: errors: list[str] = [] if not ENV_FILE.is_file(): errors.append(f".env 없음: {ENV_FILE}") for e in errors: print(f"FAIL: {e}") return 1 env_vars = parse_env_file(ENV_FILE) empty = [k for k, v in env_vars.items() if v == "" and k not in OPTIONAL_EMPTY] missing = sorted(CONFIG_ENV_KEYS - set(env_vars.keys())) extra = sorted(set(env_vars.keys()) - CONFIG_ENV_KEYS - { "BITHUMB_ACCESS_KEY", "BITHUMB_SECRET_KEY", "COIN_TELEGRAM_BOT_TOKEN", "COIN_TELEGRAM_CHAT_ID", }) if empty: errors.append(f"빈 값: {', '.join(empty)}") if missing: errors.append(f"config 대비 .env 누락: {', '.join(missing)}") if extra: print(f"WARN: config 미사용 .env 키: {', '.join(extra)}") from deepcoin.env_loader import env_status, load_project_env loaded = load_project_env(override=True) if not loaded: errors.append("load_project_env: .env 로드 실패 (python-dotenv 확인)") import config # noqa: E402 checks = [ ("SYMBOL", config.SYMBOL, env_vars.get("SYMBOL")), ("DB_PATH", config.DB_PATH, env_vars.get("DB_PATH")), ("BITHUMB_ACCESS_KEY", bool(config.BITHUMB_ACCESS_KEY), bool(env_vars.get("BITHUMB_ACCESS_KEY"))), ("LOOKBACK_BARS[3]", config.LOOKBACK_BARS.get(3), 120), ] for name, got, expected in checks: if got != expected and expected is not None: errors.append(f"config 불일치 {name}: got={got!r} expected={expected!r}") status = env_status() print("env_status:", status) print(f".env 키 수: {len(env_vars)} (config 필수 {len(CONFIG_ENV_KEYS)})") print(f"SYMBOL={config.SYMBOL} DB_PATH={config.DB_PATH}") print(f"BITHUMB_KEY set={bool(config.BITHUMB_ACCESS_KEY)} TELEGRAM set={bool(config.COIN_TELEGRAM_BOT_TOKEN)}") if errors: print("\n=== 점검 실패 ===") for e in errors: print(f" - {e}") return 1 print("\n=== 점검 통과: .env → config 로드 정상 ===") return 0 if __name__ == "__main__": raise SystemExit(main())