Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions config/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ def _resolve_auto_entry(settings: dict) -> dict:

env_raw = os.environ.get("QUANT_AUTO_ENTRY")

# YAML 값은 엄격 파서로 해석 — bool("false")==True 같은 문자열 함정 차단.
# (따옴표 하나로 auto_entry 마스터 스위치가 뒤집히면 live에서 실돈 자동매수다)
yaml_resolved = _coerce_bool_setting(
yaml_value, default=False, key="trading.auto_entry",
)

if env_raw is not None:
normalized = env_raw.strip().lower()
if normalized in _BOOL_TRUE:
Expand All @@ -172,20 +178,25 @@ def _resolve_auto_entry(settings: dict) -> dict:
)
source = "ENV"
else:
resolved = bool(yaml_value)
resolved = yaml_resolved
source = "YAML"

# live 모드에서는 환경변수 오버라이드 무시
# live 모드에서는 ENV의 '켜는 방향' 오버라이드 무시 (끄는 방향은 fail-safe라 존중).
# 주의: 정식 live 경로는 YAML mode=paper로 로드 후 mode를 플립하므로 이 분기만으로는
# 부족하다 — run_live_trading이 플립 직후 enforce_live_auto_entry_policy()를 호출해
# 같은 정책을 다시 적용한다(아래 보존 값 사용).
if mode == "live" and resolved and source == "ENV":
_log.warning(
"QUANT_AUTO_ENTRY=true 이지만 live 모드에서는 무시됩니다. "
"live 모드의 auto_entry는 YAML 설정(%s)을 따릅니다.", yaml_value,
)
resolved = bool(yaml_value)
resolved = yaml_resolved
source = "YAML (live override)"

trading["auto_entry"] = resolved
trading["_auto_entry_source"] = source
# 모드 플립 후 재적용을 위해 YAML 원천 값을 보존(엄격 파싱 결과)
trading["_auto_entry_yaml"] = yaml_resolved

_log.info(
"auto_entry resolved: %s (source=%s, yaml=%s, env=%s, mode=%s)",
Expand Down Expand Up @@ -456,6 +467,28 @@ def auto_entry_source(self) -> str:
"""auto_entry 값의 출처: 'ENV', 'YAML', 'YAML (live override)'."""
return self.trading.get("_auto_entry_source", "YAML")

def enforce_live_auto_entry_policy(self) -> None:
"""live 진입 시 auto_entry의 ENV '켜는 방향' 오버라이드를 YAML 값으로 강제 복귀.

_resolve_auto_entry의 live-ignore 분기는 '로드 시점 YAML mode'를 보므로,
정식 live 경로(YAML mode=paper로 로드 → run_live_trading이 mode 플립)에서는
발동하지 않는다 — paper 실험용 QUANT_AUTO_ENTRY=true가 셸/.env에 남아 있으면
signal-only 설정의 live가 자동매수하게 되는 구멍. 모드 플립 직후 이 메서드를
호출해 같은 정책을 재적용한다. 끄는 방향(ENV false)은 fail-safe라 존중.
"""
trading = self.trading
if (
trading.get("_auto_entry_source") == "ENV"
and trading.get("auto_entry")
):
yaml_resolved = bool(trading.get("_auto_entry_yaml", False))
logging.getLogger("config_loader").warning(
"live 진입: QUANT_AUTO_ENTRY=true(ENV)는 무시되고 YAML 값(%s)을 따릅니다.",
yaml_resolved,
)
trading["auto_entry"] = yaml_resolved
trading["_auto_entry_source"] = "YAML (live override)"

@property
def yaml_hash(self) -> str:
"""YAML 파일 원본 해시 (동결 확인용)."""
Expand Down
5 changes: 5 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,11 @@ def run_live_trading(args):

old_mode = config.trading.get("mode", "paper")
config._settings.setdefault("trading", {})["mode"] = "live"
# 모드 플립 직후 auto_entry의 ENV '켜기' 오버라이드를 YAML 값으로 강제 복귀 —
# 로드 시점(YAML mode=paper)에는 _resolve_auto_entry의 live-ignore 분기가
# 발동하지 않아, paper 실험용 QUANT_AUTO_ENTRY=true 잔존 시 signal-only 설정의
# live가 자동매수하게 되는 구멍을 막는다.
config.enforce_live_auto_entry_policy()

try:
# 토큰 사전 발급 (필수 환경변수 미설정 시 명확히 종료)
Expand Down
66 changes: 66 additions & 0 deletions tests/test_config_auto_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,69 @@ def test_scheduler_default_no_env(self):
os.environ.pop("QUANT_AUTO_ENTRY", None)
scheduler = Scheduler(strategy_name="scoring")
assert scheduler.auto_entry is False


def test_yaml_string_false_is_not_truthy(monkeypatch):
"""YAML에 따옴표로 'false'를 쓰면 bool('false')==True 함정 — 엄격 파싱으로 False여야 한다.
(auto_entry 마스터 스위치가 따옴표 하나로 뒤집히면 live에서 실돈 자동매수)"""
from config.config_loader import _resolve_auto_entry

monkeypatch.delenv("QUANT_AUTO_ENTRY", raising=False)
s = _resolve_auto_entry({"trading": {"mode": "paper", "auto_entry": "false"}})
assert s["trading"]["auto_entry"] is False

s = _resolve_auto_entry({"trading": {"mode": "live", "auto_entry": "false"}})
assert s["trading"]["auto_entry"] is False


def test_enforce_live_policy_reverts_env_enable_after_mode_flip(monkeypatch):
"""정식 live 경로(YAML mode=paper 로드 → mode 플립)에서 ENV=true 잔존 시,
enforce_live_auto_entry_policy가 YAML 값으로 강제 복귀해야 한다 —
로드 시점 live-ignore 분기는 이 경로에서 발동하지 않는 죽은 코드였다."""
from config.config_loader import Config, _resolve_auto_entry

monkeypatch.setenv("QUANT_AUTO_ENTRY", "true")
settings = _resolve_auto_entry({"trading": {"mode": "paper", "auto_entry": False}})
# 로드 시점: paper라 ENV가 이긴다 (기존 동작 — 여기까진 의도)
assert settings["trading"]["auto_entry"] is True
assert settings["trading"]["_auto_entry_source"] == "ENV"

cfg = Config.__new__(Config)
cfg._settings = settings
# run_live_trading의 모드 플립 재현
cfg._settings["trading"]["mode"] = "live"
cfg.enforce_live_auto_entry_policy()

assert cfg.trading["auto_entry"] is False # YAML 값으로 복귀
assert cfg.trading["_auto_entry_source"] == "YAML (live override)"


def test_enforce_live_policy_respects_env_disable(monkeypatch):
"""끄는 방향(ENV=false)은 fail-safe라 live에서도 존중한다."""
from config.config_loader import Config, _resolve_auto_entry

monkeypatch.setenv("QUANT_AUTO_ENTRY", "false")
settings = _resolve_auto_entry({"trading": {"mode": "paper", "auto_entry": True}})
assert settings["trading"]["auto_entry"] is False

cfg = Config.__new__(Config)
cfg._settings = settings
cfg._settings["trading"]["mode"] = "live"
cfg.enforce_live_auto_entry_policy()

assert cfg.trading["auto_entry"] is False # 그대로 꺼짐 유지


def test_enforce_live_policy_noop_for_yaml_source(monkeypatch):
"""ENV 미설정(YAML 소스)이면 정책 재적용은 무변화."""
from config.config_loader import Config, _resolve_auto_entry

monkeypatch.delenv("QUANT_AUTO_ENTRY", raising=False)
settings = _resolve_auto_entry({"trading": {"mode": "paper", "auto_entry": True}})
cfg = Config.__new__(Config)
cfg._settings = settings
cfg._settings["trading"]["mode"] = "live"
cfg.enforce_live_auto_entry_policy()

assert cfg.trading["auto_entry"] is True
assert cfg.trading["_auto_entry_source"] == "YAML"
4 changes: 4 additions & 0 deletions tests/test_live_status_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ def __init__(self, *, mode="paper", active_strategy="scoring"):
self.auto_entry = False
self.auto_entry_source = "test"

def enforce_live_auto_entry_policy(self):
# 실제 Config 인터페이스 반영 — live 진입 시 ENV auto_entry 켜기 무시 정책(no-op 더블)
pass


def test_run_live_trading_blocks_when_registry_disallows_live(monkeypatch):
import main as main_mod
Expand Down
Loading