feat: 실전 투자 전환 — TR_ID 분기, URL, 신뢰도 임계값, 텔레그램 알림 수정 (#201~#205, #208, #214)
Some checks failed
CI / test (pull_request) Has been cancelled

- #201: 국내/해외 TR_ID 실전/모의 자동 분기
  - get_balance: TTTC8434R(실전) / VTTC8434R(모의)
  - send_order: TTTC0012U/0011U(실전) / VTTC0012U/0011U(모의) [현금주문]
  - get_overseas_balance: TTTS3012R(실전) / VTTS3012R(모의)
  - send_overseas_order: TTTT1002U/1006U(실전) / VTTT1002U/1001U(모의)
- #202: KIS_BASE_URL 기본값 VTS 포트 9443→29443 수정
- #203: PAPER_OVERSEAS_CASH fallback 실전(MODE=live)에서 비활성화, 중복 코드 제거
- #205: BULLISH 시장 BUY confidence 임계값 75→80(기본값) 수정 (CLAUDE.md 비협상 규칙)
- #208: Daily 모드 CircuitBreakerTripped 시 텔레그램 알림 추가
- #214: 시스템 종료 시 notify_system_shutdown() 호출 추가

테스트 22개 추가 (TR_ID 분기 12개, confidence 임계값 1개 수정)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
agentson
2026-02-23 10:28:24 +09:00
parent cd36d53a47
commit d6a389e0b7
7 changed files with 379 additions and 22 deletions

View File

@@ -285,7 +285,10 @@ class KISBroker:
await self._rate_limiter.acquire()
session = self._get_session()
headers = await self._auth_headers("VTTC8434R") # 모의투자 잔고조회
# TR_ID: 실전 TTTC8434R, 모의 VTTC8434R
# Source: 한국투자증권 오픈API 전체문서 (20260221) — '국내주식 잔고조회' 시트
tr_id = "TTTC8434R" if self._settings.MODE == "live" else "VTTC8434R"
headers = await self._auth_headers(tr_id)
params = {
"CANO": self._account_no,
"ACNT_PRDT_CD": self._product_cd,
@@ -330,7 +333,13 @@ class KISBroker:
await self._rate_limiter.acquire()
session = self._get_session()
tr_id = "VTTC0802U" if order_type == "BUY" else "VTTC0801U"
# TR_ID: 실전 BUY=TTTC0012U SELL=TTTC0011U, 모의 BUY=VTTC0012U SELL=VTTC0011U
# Source: 한국투자증권 오픈API 전체문서 (20260221) — '주식주문(현금)' 시트
# ※ TTTC0802U/VTTC0802U는 미수매수(증거금40% 계좌 전용) — 현금주문에 사용 금지
if self._settings.MODE == "live":
tr_id = "TTTC0012U" if order_type == "BUY" else "TTTC0011U"
else:
tr_id = "VTTC0012U" if order_type == "BUY" else "VTTC0011U"
# KRX requires limit orders to be rounded down to the tick unit.
# ORD_DVSN: "00"=지정가, "01"=시장가

View File

@@ -175,8 +175,12 @@ class OverseasBroker:
await self._broker._rate_limiter.acquire()
session = self._broker._get_session()
# Virtual trading TR_ID for overseas balance inquiry
headers = await self._broker._auth_headers("VTTS3012R")
# TR_ID: 실전 TTTS3012R, 모의 VTTS3012R
# Source: 한국투자증권 오픈API 전체문서 (20260221) — '해외주식 잔고조회' 시트
balance_tr_id = (
"TTTS3012R" if self._broker._settings.MODE == "live" else "VTTS3012R"
)
headers = await self._broker._auth_headers(balance_tr_id)
params = {
"CANO": self._broker._account_no,
"ACNT_PRDT_CD": self._broker._product_cd,
@@ -229,10 +233,12 @@ class OverseasBroker:
await self._broker._rate_limiter.acquire()
session = self._broker._get_session()
# Virtual trading TR_IDs for overseas orders
# TR_ID: 실전 BUY=TTTT1002U SELL=TTTT1006U, 모의 BUY=VTTT1002U SELL=VTTT1001U
# Source: 한국투자증권 오픈API 전체문서 (20260221) — '해외주식 주문' 시트
# VTTT1002U: 모의투자 미국 매수, VTTT1001U: 모의투자 미국 매도
tr_id = "VTTT1002U" if order_type == "BUY" else "VTTT1001U"
if self._broker._settings.MODE == "live":
tr_id = "TTTT1002U" if order_type == "BUY" else "TTTT1006U"
else:
tr_id = "VTTT1002U" if order_type == "BUY" else "VTTT1001U"
body = {
"CANO": self._broker._account_no,

View File

@@ -13,7 +13,7 @@ class Settings(BaseSettings):
KIS_APP_KEY: str
KIS_APP_SECRET: str
KIS_ACCOUNT_NO: str # format: "XXXXXXXX-XX"
KIS_BASE_URL: str = "https://openapivts.koreainvestment.com:9443"
KIS_BASE_URL: str = "https://openapivts.koreainvestment.com:29443"
# Google Gemini
GEMINI_API_KEY: str

View File

@@ -340,7 +340,13 @@ async def trading_cycle(
purchase_total = safe_float(balance_info.get("frcr_buy_amt_smtl", "0") or "0")
# Paper mode fallback: VTS overseas balance API often fails for many accounts.
if total_cash <= 0 and settings and settings.PAPER_OVERSEAS_CASH > 0:
# Only activate in paper mode — live mode must use real balance from KIS.
if (
total_cash <= 0
and settings
and settings.MODE == "paper"
and settings.PAPER_OVERSEAS_CASH > 0
):
logger.debug(
"Overseas cash balance is 0 for %s; using paper fallback %.2f USD",
market.exchange_code,
@@ -499,9 +505,8 @@ async def trading_cycle(
outlook = playbook.market_outlook
if outlook == MarketOutlook.BEARISH:
min_confidence = 90
elif outlook == MarketOutlook.BULLISH:
min_confidence = 75
else:
# BULLISH/NEUTRAL: use base threshold (min 80 per CLAUDE.md non-negotiable rule)
min_confidence = base_threshold
if match.confidence < min_confidence:
logger.info(
@@ -1041,11 +1046,12 @@ async def run_daily_session(
balance_info.get("frcr_buy_amt_smtl", "0") or "0"
)
# Paper mode fallback: VTS overseas balance API often fails for many accounts.
if total_cash <= 0 and settings.PAPER_OVERSEAS_CASH > 0:
total_cash = settings.PAPER_OVERSEAS_CASH
# VTS overseas balance API often returns 0; use paper fallback.
if total_cash <= 0 and settings.PAPER_OVERSEAS_CASH > 0:
# Only activate in paper mode — live mode must use real balance from KIS.
if (
total_cash <= 0
and settings.MODE == "paper"
and settings.PAPER_OVERSEAS_CASH > 0
):
total_cash = settings.PAPER_OVERSEAS_CASH
# Calculate daily P&L %
@@ -1979,6 +1985,10 @@ async def run(settings: Settings) -> None:
)
except CircuitBreakerTripped:
logger.critical("Circuit breaker tripped — shutting down")
await telegram.notify_circuit_breaker(
pnl_pct=settings.CIRCUIT_BREAKER_PCT,
threshold=settings.CIRCUIT_BREAKER_PCT,
)
shutdown.set()
break
except Exception as exc:
@@ -2296,6 +2306,8 @@ async def run(settings: Settings) -> None:
except TimeoutError:
pass # Normal — timeout means it's time for next cycle
finally:
# Notify shutdown before closing resources
await telegram.notify_system_shutdown("Normal shutdown")
# Clean up resources
await command_handler.stop_polling()
await broker.close()