import gzip import logging import logging.handlers import os import shutil from pathlib import Path LOG_DIR = os.getenv("LOG_DIR", "logs") LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() Path(LOG_DIR).mkdir(parents=True, exist_ok=True) LOG_FILE = os.path.join(LOG_DIR, "AutoCoinTrader.log") logger = logging.getLogger("macd_alarm") _logger_configured = False # 거래소 및 계산 상수 # 부동소수점 비교용 엡실론 (일반적 정밀도) FLOAT_EPSILON = 1e-10 # 거래소별 최소 수량 (Upbit 기준) MIN_TRADE_AMOUNT = 1e-8 # 0.00000001 (암호화폐 최소 단위) # 최소 주문 금액 (KRW) MIN_KRW_ORDER = 5000 # Upbit 최소 주문 금액 # 데이터 파일 경로 상수 (중앙 집중 관리) DATA_DIR = Path("data") DATA_DIR.mkdir(parents=True, exist_ok=True) HOLDINGS_FILE = str(DATA_DIR / "holdings.json") TRADES_FILE = str(DATA_DIR / "trades.json") PENDING_ORDERS_FILE = str(DATA_DIR / "pending_orders.json") class CompressedRotatingFileHandler(logging.handlers.RotatingFileHandler): """RotatingFileHandler with gzip compression for rotated logs.""" def rotation_filename(self, default_name): """Append .gz to rotated log files.""" return default_name + ".gz" def rotate(self, source, dest): """Compress the rotated log file.""" if os.path.exists(source): with open(source, "rb") as f_in: with gzip.open(dest, "wb") as f_out: shutil.copyfileobj(f_in, f_out) os.remove(source) def setup_logger(dry_run: bool): """ Configure logging with rotation and compression. Args: dry_run: If True, also output to console. If False, only to file. Log Rotation Strategy: - Size-based: 10MB per file, keep 7 backups (total ~80MB) - Compression: Old logs are gzipped (saves ~70% space) Log Levels (production recommendation): - dry_run=True: INFO (development/testing) - dry_run=False: INFO (production - retain important trading logs) ⚠️ CRITICAL: Production mode uses INFO level to ensure trading events are logged. This is essential for auditing buy/sell orders and debugging issues. For high-volume environments, adjust LOG_LEVEL via environment variable. """ global logger, _logger_configured if _logger_configured: return logger.handlers.clear() # Use INFO level for both dry_run and production to ensure trading events are logged # Production systems can override via LOG_LEVEL environment variable if needed effective_level = getattr(logging, LOG_LEVEL, logging.INFO) logger.setLevel(effective_level) formatter = logging.Formatter("%(asctime)s - %(levelname)s - [%(threadName)s] - %(message)s") # Console handler (only in dry_run mode) if dry_run: ch = logging.StreamHandler() ch.setLevel(effective_level) ch.setFormatter(formatter) logger.addHandler(ch) # Size-based rotating file handler with compression (only one rotation strategy) fh_size = CompressedRotatingFileHandler( LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=7, encoding="utf-8", # 10MB per file # Keep 7 backups ) fh_size.setLevel(effective_level) fh_size.setFormatter(formatter) logger.addHandler(fh_size) _logger_configured = True logger.info( "[SYSTEM] 로그 설정 완료: level=%s, size_rotation=%dMB×%d (일별 로테이션 제거됨)", logging.getLevelName(effective_level), 10, 7, )