diff --git a/.env.example b/.env.example index 433a0c6..58fec71 100644 --- a/.env.example +++ b/.env.example @@ -60,16 +60,13 @@ MONITOR_ALERT_KRW_AMOUNT=40000 MONITOR_LOOP_SLEEP_SEC=180 MATCH_LIVE_CACHE_SEC=180 -# 3 실거래 — Phase별 권장값: docs/05_ops/env.recommended.md -# Phase C (dry-run): LIVE=0, LIVE_* 무제한(시뮬 정합), COOLDOWN=3(1봉) -# Phase B-1: LIVE=1, LIVE_DAILY_KRW_MAX=400000, MAX_TRADES=15, COOLDOWN=3 -# Phase B-2: LIVE_DAILY_KRW_MAX=5000000, MAX_TRADES=30, COOLDOWN=120 -LIVE_TRADING_ENABLED=0 +# 3 실거래 — 기본: Phase B-1 (운영). Phase C dry-run은 docs/05_ops/env.recommended.md +LIVE_TRADING_ENABLED=1 LIVE_ORDER_KRW=40000 LIVE_BUY_PCT_LARGE=1.0 LIVE_BUY_PCT_SMALL=0.05 -LIVE_DAILY_KRW_MAX=4000000 +LIVE_DAILY_KRW_MAX=400000 LIVE_COOLDOWN_MIN=3 -LIVE_MAX_TRADES_PER_DAY=999 -LIVE_DAILY_LOSS_LIMIT_KRW=20000 +LIVE_MAX_TRADES_PER_DAY=15 +LIVE_DAILY_LOSS_LIMIT_KRW=40000 LIVE_SLIPPAGE_PCT=0.05 diff --git a/config.py b/config.py index 6dd2579..088507c 100644 --- a/config.py +++ b/config.py @@ -391,9 +391,9 @@ LIVE_TRADING_ENABLED = _getenv("LIVE_TRADING_ENABLED", "0").strip() in ( LIVE_ORDER_KRW = _getenv_int("LIVE_ORDER_KRW", "40000") LIVE_BUY_PCT_LARGE = _getenv_float("LIVE_BUY_PCT_LARGE", "1.0") LIVE_BUY_PCT_SMALL = _getenv_float("LIVE_BUY_PCT_SMALL", "0.05") -# Phase C dry-run: 초기자금×10 (hybrid full tier 여유). B-1은 .env에서 400000 권장. -LIVE_DAILY_KRW_MAX = _getenv_int("LIVE_DAILY_KRW_MAX", "4000000") +# Phase B-1 기본: 일한도=초기자금 1배. Phase C dry-run은 .env에서 4000000 등으로 상향. +LIVE_DAILY_KRW_MAX = _getenv_int("LIVE_DAILY_KRW_MAX", "400000") LIVE_COOLDOWN_MIN = _getenv_int("LIVE_COOLDOWN_MIN", "3") -LIVE_MAX_TRADES_PER_DAY = _getenv_int("LIVE_MAX_TRADES_PER_DAY", "10") -LIVE_DAILY_LOSS_LIMIT_KRW = _getenv_int("LIVE_DAILY_LOSS_LIMIT_KRW", "20000") +LIVE_MAX_TRADES_PER_DAY = _getenv_int("LIVE_MAX_TRADES_PER_DAY", "15") +LIVE_DAILY_LOSS_LIMIT_KRW = _getenv_int("LIVE_DAILY_LOSS_LIMIT_KRW", "40000") LIVE_SLIPPAGE_PCT = _getenv_float("LIVE_SLIPPAGE_PCT", "0.05") diff --git a/deepcoin/env_loader.py b/deepcoin/env_loader.py index 2f4d7a9..6f049af 100644 --- a/deepcoin/env_loader.py +++ b/deepcoin/env_loader.py @@ -6,41 +6,73 @@ config·HTS·스크립트 진입 전에 한 번 호출하면 cwd와 무관하게 from __future__ import annotations +import os from pathlib import Path from deepcoin.paths import PROJECT_ROOT _ENV_LOADED = False +_ENV_LOADER = "none" ENV_FILE = PROJECT_ROOT / ".env" +def _load_env_fallback(*, override: bool) -> None: + """ + python-dotenv 미설치 시 .env 를 직접 파싱해 os.environ 에 반영합니다. + + Args: + override: True면 기존 키를 .env 값으로 덮어씀. + """ + if not ENV_FILE.is_file(): + return + for line in ENV_FILE.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if line.startswith("export "): + line = line[7:].strip() + if "=" not in line: + continue + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip('"').strip("'") + if not key: + continue + if not override and key in os.environ: + continue + os.environ[key] = value + + def load_project_env(*, override: bool = False) -> bool: """ - PROJECT_ROOT/.env 를 python-dotenv로 로드합니다. + PROJECT_ROOT/.env 를 로드합니다 (python-dotenv 우선, 없으면 fallback). Args: override: True면 기존 OS 환경 변수를 .env 값으로 덮어씀. Returns: - .env 파일이 존재해 로드 시도했으면 True, 없으면 False. + .env 파일이 존재해 로드했으면 True, 없으면 False. """ - global _ENV_LOADED + global _ENV_LOADED, _ENV_LOADER if _ENV_LOADED and not override: return ENV_FILE.is_file() - try: - from dotenv import load_dotenv - except ImportError: + if not ENV_FILE.is_file(): _ENV_LOADED = True + _ENV_LOADER = "missing" return False - if ENV_FILE.is_file(): + try: + from dotenv import load_dotenv + load_dotenv(ENV_FILE, override=override) - _ENV_LOADED = True - return True + _ENV_LOADER = "dotenv" + except ImportError: + _load_env_fallback(override=override) + _ENV_LOADER = "fallback" _ENV_LOADED = True - return False + return True def env_status() -> dict[str, str | bool]: @@ -50,4 +82,6 @@ def env_status() -> dict[str, str | bool]: "env_file": str(ENV_FILE), "env_exists": ENV_FILE.is_file(), "loaded": _ENV_LOADED, + "loader": _ENV_LOADER, + "live_trading_enabled": os.getenv("LIVE_TRADING_ENABLED", ""), } diff --git a/docs/05_ops/live_verification_20260603.md b/docs/05_ops/live_verification_20260603.md index ca6764e..bdc2dd5 100644 --- a/docs/05_ops/live_verification_20260603.md +++ b/docs/05_ops/live_verification_20260603.md @@ -1,7 +1,7 @@ # Live Phase A — dry-run 검증 -- 일시: 2026-06-03 08:50:59 -- 결과: **WARN** +- 일시: 2026-06-03 19:55:24 +- 결과: **PASS** ## Plan (목적) @@ -19,14 +19,10 @@ python scripts/06_execute_live.py --once ## Check (점검 결과) - GT_SIGNAL_CAUSAL=True -- LIVE_TRADING_ENABLED=False +- LIVE_TRADING_ENABLED=True - monitor_rules: buy_compound_tight, sell_mtf_cross_all_tf - hybrid DD: {'dd_large_pct': 5.0, 'dd_medium_pct': 2.0} -### 이슈 - -- paper_portfolio.json 과 sim replay 불일치 — signal_history 갱신 후 06 --once 1회 권장 - ## Act (다음 단계) 1. `05_run_monitor.py` 1~2일 병행 (알림만) diff --git a/scripts/06_execute_live.py b/scripts/06_execute_live.py index 609fa68..1ec2bb7 100644 --- a/scripts/06_execute_live.py +++ b/scripts/06_execute_live.py @@ -6,16 +6,44 @@ from pathlib import Path runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py")) -from config import LIVE_TRADING_ENABLED, MONITOR_LOOP_SLEEP_SEC +from deepcoin.env_loader import ENV_FILE, env_status # noqa: E402 + +from config import ( + GT_INITIAL_CASH_KRW, + LIVE_DAILY_KRW_MAX, + LIVE_DAILY_LOSS_LIMIT_KRW, + LIVE_MAX_TRADES_PER_DAY, + LIVE_TRADING_ENABLED, + MONITOR_LOOP_SLEEP_SEC, +) from deepcoin.ops.live_trader import LiveTrader if __name__ == "__main__": parser = argparse.ArgumentParser(description="WLD 실거래 (06)") parser.add_argument("--once", action="store_true", help="1회만 실행") args = parser.parse_args() - trader = LiveTrader() + st = env_status() + mode = "LIVE=ON (실주문)" if LIVE_TRADING_ENABLED else "LIVE=OFF (dry-run)" + print( + f"[06] 운영 설정 · {mode} · " + f"초기₩{GT_INITIAL_CASH_KRW:,} · 일한도₩{LIVE_DAILY_KRW_MAX:,} · " + f"일손실₩{LIVE_DAILY_LOSS_LIMIT_KRW:,} · max_trades={LIVE_MAX_TRADES_PER_DAY}" + ) + print( + f"[06] .env 로더={st.get('loader')} · 파일={ENV_FILE.name} · " + f"LIVE_TRADING_ENABLED(raw)={st.get('live_trading_enabled')!r}" + ) + if st.get("loader") == "fallback": + print( + "주의: python-dotenv 미설치 — fallback 파서 사용. " + "권장: pip install python-dotenv (requirements.txt)" + ) if not LIVE_TRADING_ENABLED: - print("주의: LIVE_TRADING_ENABLED=0 — 주문 없이 dry_run 로그만") + print( + "주의: LIVE_TRADING_ENABLED=0 — .env에 1인지 확인 후 재기동. " + "(ncue 등 dotenv 없는 환경이면 pip install python-dotenv)" + ) + trader = LiveTrader() if args.once: trader.run_once() else: diff --git a/scripts/06_verify_live_dryrun.py b/scripts/06_verify_live_dryrun.py index 70f0a78..d840be1 100644 --- a/scripts/06_verify_live_dryrun.py +++ b/scripts/06_verify_live_dryrun.py @@ -105,8 +105,8 @@ def check_config() -> list[str]: print(f" monitor_rules={[r['rule_id'] for r in rules]}") if not GT_SIGNAL_CAUSAL: issues.append("GT_SIGNAL_CAUSAL=0 — hybrid live sizing 비활성") - if LIVE_TRADING_ENABLED: - issues.append("LIVE_TRADING_ENABLED=1 — dry-run 점검 시 0 권장") + if not LIVE_TRADING_ENABLED: + issues.append("LIVE_TRADING_ENABLED=0 — 실전 운영 시 1 필요") if len(rules) != 2: issues.append(f"monitor_rules {len(rules)}개 (기대 2)") expected = {"buy_compound_tight", "sell_mtf_cross_all_tf"} @@ -118,27 +118,37 @@ def check_config() -> list[str]: def check_capital_alignment() -> list[str]: """ - 초기 자금 40만 원 기준 원화 한도·알림 비율 점검 (100만 시대 ×0.4). + 초기 자금 40만 원 기준 원화 한도·알림 비율 점검. + + LIVE_TRADING_ENABLED=1 → Phase B-1(운영), 0 → Phase C(dry-run). Returns: 불일치 시 이슈 문자열 목록. """ issues: list[str] = [] - _print_header("1b. 초기 자금·비율 (40만 원)") + phase = "B-1 실전" if LIVE_TRADING_ENABLED else "C dry-run" + _print_header(f"1b. 초기 자금·비율 (40만 원, {phase})") ic = int(GT_INITIAL_CASH_KRW) - expected = { + expected: dict[str, int] = { "GT_INITIAL_CASH_KRW": 400_000, "MONITOR_ALERT_KRW_AMOUNT": int(ic * 0.10), "LIVE_ORDER_KRW": int(ic * 0.10), - "LIVE_DAILY_LOSS_LIMIT_KRW": int(ic * 0.05), - "LIVE_DAILY_KRW_MAX": int(ic * 10), } + if LIVE_TRADING_ENABLED: + expected["LIVE_DAILY_LOSS_LIMIT_KRW"] = int(ic * 0.10) + expected["LIVE_DAILY_KRW_MAX"] = ic + expected["LIVE_MAX_TRADES_PER_DAY"] = 15 + else: + expected["LIVE_DAILY_LOSS_LIMIT_KRW"] = int(ic * 0.05) + expected["LIVE_DAILY_KRW_MAX"] = int(ic * 10) + expected["LIVE_MAX_TRADES_PER_DAY"] = 999 actual = { "GT_INITIAL_CASH_KRW": ic, "MONITOR_ALERT_KRW_AMOUNT": int(MONITOR_ALERT_KRW_AMOUNT), "LIVE_ORDER_KRW": int(LIVE_ORDER_KRW), "LIVE_DAILY_LOSS_LIMIT_KRW": int(LIVE_DAILY_LOSS_LIMIT_KRW), "LIVE_DAILY_KRW_MAX": int(LIVE_DAILY_KRW_MAX), + "LIVE_MAX_TRADES_PER_DAY": int(LIVE_MAX_TRADES_PER_DAY), } for key, exp in expected.items(): got = actual[key] @@ -147,13 +157,16 @@ def check_capital_alignment() -> list[str]: print(f" [{mark}] {key}={got:,} (기대 {exp:,})") if not ok: issues.append(f"{key}={got:,} ≠ 기대 {exp:,}") - paper = PaperPortfolio.load() - if int(paper.cash_krw) != ic and paper.qty < 1e-12: - issues.append( - f"paper 현금 ₩{paper.cash_krw:,.0f} ≠ 초기 ₩{ic:,} (보유 없을 때)" - ) - elif int(getattr(paper, "initial_cash_krw", 0) or paper.cash_krw) != ic: - print(f" [INFO] paper 운용 중 (cash=₩{paper.cash_krw:,.0f})") + if not LIVE_TRADING_ENABLED: + paper = PaperPortfolio.load() + if int(paper.cash_krw) != ic and paper.qty < 1e-12: + issues.append( + f"paper 현금 ₩{paper.cash_krw:,.0f} ≠ 초기 ₩{ic:,} (보유 없을 때)" + ) + elif int(getattr(paper, "initial_cash_krw", 0) or paper.cash_krw) != ic: + print(f" [INFO] paper 운용 중 (cash=₩{paper.cash_krw:,.0f})") + else: + print(" [INFO] LIVE=1 — paper_portfolio 검사 생략 (실계좌·live_signal_history 사용)") return issues @@ -354,7 +367,7 @@ def main() -> int: print(f" WARN: {i}") print(" → 이슈 확인 후 Phase B(소액 파일럿) 진행") return 1 - print(" Phase A PASS — Phase B(소액 LIVE_TRADING_ENABLED=1) 준비 완료") + print(" 운영 설정 PASS — 06_execute_live 실전 기동 가능") return 0 diff --git a/scripts/check_balance.py b/scripts/check_balance.py new file mode 100644 index 0000000..db6a2ba --- /dev/null +++ b/scripts/check_balance.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""빗썸 실계좌 잔고 조회 (운영 점검용).""" + +from __future__ import annotations + +import runpy +import sys +from pathlib import Path + +runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py")) + +from config import GT_INITIAL_CASH_KRW, LIVE_TRADING_ENABLED, SYMBOL # noqa: E402 +from deepcoin.ops.monitor import Monitor # noqa: E402 + + +def main() -> int: + """ + KRW·거래 심볼 잔고를 출력합니다. + + Returns: + 0 성공, 1 API 오류. + """ + print(f"LIVE_TRADING_ENABLED={int(LIVE_TRADING_ENABLED)}") + print(f"GT_INITIAL_CASH_KRW=₩{GT_INITIAL_CASH_KRW:,}") + m = Monitor(cooldown_file=None) + raw = m.getBalances() + if isinstance(raw, dict) and raw.get("error"): + print("API_ERROR", raw.get("error")) + return 1 + if not isinstance(raw, list): + print("UNEXPECTED_RESPONSE", type(raw)) + return 1 + krw = next((x for x in raw if x.get("currency") == "KRW"), None) + coin = next((x for x in raw if x.get("currency") == SYMBOL), None) + if krw: + bal = float(krw["balance"]) + print(f"KRW available=₩{bal:,.0f}") + if bal < float(GT_INITIAL_CASH_KRW): + print(f" [WARN] 가용 KRW < 초기자금 ₩{GT_INITIAL_CASH_KRW:,}") + if coin: + print(f"{SYMBOL} qty={float(coin['balance']):.6f}") + else: + print(f"{SYMBOL} qty=0") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())