변경 사항 (Changes): 구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함. 스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함. 코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함. 시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함. 기대 효과 (Impact): 이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
160 lines
5.7 KiB
Python
160 lines
5.7 KiB
Python
"""
|
|
kis_trader/utils/env.py — DB(config_* + env_config 병합) 우선 + os.environ 폴백 환경변수 헬퍼
|
|
===========================================================================
|
|
기존 kis_scalping_ver2 / kis_short_ver3 에 흩어져 있던 `get_env_*` 를 통합.
|
|
- DB get_merged_env_snapshot / get_strategy_env_dict → os.environ → default 순으로 조회.
|
|
- 하드코딩 금지 원칙에 맞춰 전 모듈에서 이 함수들만 사용하도록 한다.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import time
|
|
from typing import Any, Dict, Optional
|
|
|
|
logger = logging.getLogger("kis_trader.env")
|
|
|
|
# 순환 import 방지용 레이지 TradeDB 참조
|
|
_db_instance = None
|
|
# get_merged_env_snapshot() — TTL 내 재사용 (실매: 웹에서 MAX_DAILY 등 변경 즉시 반영)
|
|
_merged_env_cache: Optional[Dict[str, str]] = None
|
|
_merged_env_cache_ts: float = 0.0
|
|
# env 캐시 세대 카운터 — invalidate 시 +1. 파생 캐시(예: whipsaw 파라미터)가
|
|
# 이 값으로 무효화를 감지해 안전하게 재계산한다(값은 그대로, 재계산 시점만 동일).
|
|
_env_generation: int = 0
|
|
|
|
|
|
def _get_db():
|
|
"""TradeDB 싱글톤. 최초 호출 시 MariaDB 연결."""
|
|
global _db_instance
|
|
if _db_instance is None:
|
|
try:
|
|
from database import TradeDB # 프로젝트 루트의 기존 모듈
|
|
_db_instance = TradeDB()
|
|
except Exception as e:
|
|
logger.warning("TradeDB 초기화 실패(%s) → env_config 조회 불가 → os.environ 폴백", e)
|
|
_db_instance = False # 실패 기록 (None과 구분)
|
|
return _db_instance or None
|
|
|
|
|
|
def set_db(db_obj) -> None:
|
|
"""외부에서 이미 만든 TradeDB 인스턴스를 재사용할 때 주입."""
|
|
global _db_instance
|
|
_db_instance = db_obj
|
|
|
|
|
|
def invalidate_merged_env_cache() -> None:
|
|
"""env/config 저장 후 스냅샷 캐시 무효화 (insert_env_snapshot 등)."""
|
|
global _merged_env_cache, _merged_env_cache_ts, _env_generation
|
|
_merged_env_cache = None
|
|
_merged_env_cache_ts = 0.0
|
|
_env_generation += 1
|
|
|
|
|
|
def _merged_env_cache_ttl_sec() -> float:
|
|
"""DB env 스냅샷 메모리 TTL(초). os.environ 만 사용(순환 import 방지)."""
|
|
try:
|
|
return max(0.0, float(os.environ.get("ENV_MERGED_CACHE_TTL_SEC", "60")))
|
|
except (ValueError, TypeError):
|
|
return 60.0
|
|
|
|
|
|
def env_cache_generation() -> int:
|
|
"""현재 env 캐시 세대. invalidate 될 때마다 증가.
|
|
|
|
파생 캐시가 이 값을 키에 포함하면, 설정 저장(무효화) 시 자동으로 재계산되고
|
|
그 전까지는 동일 값을 재사용한다(get_env_from_db 의 병합캐시와 동일 수명).
|
|
"""
|
|
return _env_generation
|
|
|
|
|
|
def _strip_comment(val: Any) -> Any:
|
|
"""DB 컬럼에 `#` 이후 주석이 들어 있을 때 제거. 기존 규칙 유지."""
|
|
if isinstance(val, str) and "#" in val:
|
|
return val.split("#", 1)[0].strip()
|
|
return val
|
|
|
|
|
|
def get_merged_env_dict() -> dict:
|
|
"""env_config + config_scalp/short/momentum/… 최신 행 병합 flat dict."""
|
|
global _merged_env_cache, _merged_env_cache_ts
|
|
ttl = _merged_env_cache_ttl_sec()
|
|
now = time.time()
|
|
if _merged_env_cache is not None and (ttl <= 0 or (now - _merged_env_cache_ts) < ttl):
|
|
return _merged_env_cache
|
|
|
|
db = _get_db()
|
|
if db is None:
|
|
return _merged_env_cache or {}
|
|
try:
|
|
if hasattr(db, "get_merged_env_snapshot"):
|
|
merged = db.get_merged_env_snapshot()
|
|
else:
|
|
row = db.get_latest_env()
|
|
merged = dict((row or {}).get("snapshot") or {})
|
|
_merged_env_cache = dict(merged or {})
|
|
_merged_env_cache_ts = now
|
|
return _merged_env_cache
|
|
except Exception as e:
|
|
logger.debug("merged env 조회 실패: %s", e)
|
|
return _merged_env_cache or {}
|
|
|
|
|
|
def get_strategy_env_dict(strategy_id: str) -> dict:
|
|
"""전략 config_* 테이블 + 공통 env 병합 (전략 키 우선).
|
|
|
|
SCALP → config_scalp, MOMENTUM → config_momentum, SHORT → config_short …
|
|
실매(get_env_from_db) · 웹 · 파라서치가 동일 소스를 쓰도록 한다.
|
|
"""
|
|
merged = get_merged_env_dict()
|
|
db = _get_db()
|
|
if db is None:
|
|
return merged
|
|
try:
|
|
if hasattr(db, "get_strategy_config_snapshot"):
|
|
strat = db.get_strategy_config_snapshot(strategy_id)
|
|
if strat:
|
|
out = dict(merged)
|
|
out.update(strat)
|
|
return out
|
|
except Exception as e:
|
|
logger.debug("strategy env 조회 실패 (%s): %s", strategy_id, e)
|
|
return merged
|
|
|
|
|
|
def get_env_from_db(key: str, default: str = "") -> str:
|
|
"""config_* + env_config 병합 스냅샷 → 키 값. 없으면 os.environ → default."""
|
|
merged = get_merged_env_dict()
|
|
if merged:
|
|
v = _strip_comment(merged.get(key))
|
|
if v not in (None, ""):
|
|
return str(v)
|
|
# os.environ 폴백 (기존 호환)
|
|
return os.environ.get(key, str(default))
|
|
|
|
|
|
def get_env_float(key: str, default: float) -> float:
|
|
raw = get_env_from_db(key, str(default))
|
|
try:
|
|
return float(raw) if raw != "" else float(default)
|
|
except (ValueError, TypeError):
|
|
return float(default)
|
|
|
|
|
|
def get_env_int(key: str, default: int) -> int:
|
|
raw = get_env_from_db(key, str(default))
|
|
try:
|
|
# "1.0" 같은 값이 들어와도 int 캐스팅되도록 float 경유
|
|
return int(float(raw)) if raw != "" else int(default)
|
|
except (ValueError, TypeError):
|
|
return int(default)
|
|
|
|
|
|
def get_env_bool(key: str, default: bool = False) -> bool:
|
|
raw = str(get_env_from_db(key, str(default))).strip().lower()
|
|
if raw in ("true", "1", "yes", "y", "on"):
|
|
return True
|
|
if raw in ("false", "0", "no", "n", "off", ""):
|
|
return False
|
|
return bool(default)
|