fix: .env 미로드(ncue) fallback 및 B-1 운영 설정 정합

python-dotenv 없을 때 env_loader가 .env를 직접 파싱하도록 해
LIVE_TRADING_ENABLED=1이 dry-run으로 보이던 문제를 해결한다.
06 기동 시 로더·LIVE 상태를 출력하고, B-1 한도 검증·잔고 점검 스크립트를 추가한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-06-03 20:26:23 +09:00
parent 893f830aaf
commit 1fc560744d
7 changed files with 163 additions and 47 deletions

View File

@@ -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:

View File

@@ -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=1dry-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

48
scripts/check_balance.py Normal file
View File

@@ -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())