Skip to content

Commit 29e3642

Browse files
illenne77claude
andcommitted
merge(feat/kis-auto-fetch): feat/kis-auto-fetch -> main
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2 parents 0c434db + a49960f commit 29e3642

3 files changed

Lines changed: 116 additions & 29 deletions

File tree

pages/2_포트폴리오_대시보드.py

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
import os
910
from decimal import Decimal
1011

1112
import pandas as pd
@@ -28,23 +29,97 @@
2829
help="올해 이미 실현한 양도차익. 0이면 기본공제 250만원 전액 미사용.",
2930
)
3031

32+
# ── KIS 계좌 자동 조회 (로컬 전용) ───────────────────────────
33+
with st.expander("🔗 KIS 증권계좌 자동 조회 (로컬 실행 전용)", expanded=False):
34+
st.caption(
35+
"KIS OpenAPI를 통해 보유 종목을 자동으로 불러옵니다.\n\n"
36+
"⚠️ Streamlit Cloud에서는 KIS API 접근이 제한됩니다. 로컬에서만 동작합니다.\n\n"
37+
"사전 준비: `.env` 파일에 KIS 키 설정 후 `python scripts/kis_token.py live` 실행 필요."
38+
)
39+
40+
_kis_key = os.environ.get("KIS_APP_KEY") or os.environ.get("KIS_LIVE_APP_KEY")
41+
_kis_secret = os.environ.get("KIS_APP_SECRET") or os.environ.get("KIS_LIVE_APP_SECRET")
42+
_kis_account = os.environ.get("KIS_ACCOUNT")
43+
_kis_ready = bool(_kis_key and _kis_secret and _kis_account)
44+
45+
if not _kis_ready:
46+
st.warning("KIS_APP_KEY, KIS_APP_SECRET, KIS_ACCOUNT 환경변수를 설정해야 합니다.")
47+
else:
48+
from sentinelq.adapters.kis_history import SECRETS_DIR
49+
50+
_token_ok = (SECRETS_DIR / "kis_token_live.json").exists()
51+
if not _token_ok:
52+
st.warning(
53+
"KIS 토큰이 없습니다. 먼저 터미널에서 실행하세요:\n"
54+
"```\npython scripts/kis_token.py live\n```"
55+
)
56+
57+
if st.button("KIS 잔고 조회 🔄", disabled=not _token_ok):
58+
if not os.environ.get("KIS_LIVE_APP_KEY"):
59+
os.environ["KIS_LIVE_APP_KEY"] = _kis_key
60+
if not os.environ.get("KIS_LIVE_APP_SECRET"):
61+
os.environ["KIS_LIVE_APP_SECRET"] = _kis_secret
62+
if not os.environ.get("KIS_LIVE_BASE_URL"):
63+
os.environ["KIS_LIVE_BASE_URL"] = "https://openapi.koreainvestment.com:9443"
64+
os.environ["SENTINELQ_LIVE_ALLOW"] = "1"
65+
66+
from sentinelq.adapters.kis_history import fetch_balance
67+
68+
with st.spinner("KIS 잔고 조회 중..."):
69+
try:
70+
_auto_holdings = fetch_balance(env="live", confirm_live=True)
71+
except Exception as exc:
72+
st.error(f"조회 오류: {exc}")
73+
_err = str(exc).lower()
74+
if any(k in _err for k in ("timed out", "network", "urlopen", "connection")):
75+
st.info(
76+
"💡 Streamlit Cloud에서는 KIS API 접근이 제한됩니다. "
77+
"로컬에서 실행하세요:\n"
78+
"```bash\nstreamlit run streamlit_app.py\n```"
79+
)
80+
st.stop()
81+
82+
if not _auto_holdings:
83+
st.info("조회된 잔고가 없습니다.")
84+
else:
85+
st.success(f"{len(_auto_holdings)}개 종목 조회 완료. 아래 표에 자동 입력됩니다.")
86+
st.session_state["kis_holdings"] = _auto_holdings
87+
st.rerun()
88+
89+
# ── 보유 종목 입력 테이블 ─────────────────────────────────────
3190
st.subheader("📋 보유 종목 입력")
32-
st.caption("아래 표에 직접 입력하거나 편집하세요.")
33-
34-
# ── 기본 예시 데이터 ─────────────────────────────────────────
35-
_DEFAULT = pd.DataFrame(
36-
{
37-
"종목코드": ["005930", "AAPL"],
38-
"종목명": ["삼성전자", "Apple Inc"],
39-
"시장": ["KR", "US"],
40-
"수량": [10, 5],
41-
"평균단가(원)": [70_000, 1_800_000],
42-
"현재가(원)": [78_000, 2_100_000],
43-
}
44-
)
91+
92+
if "kis_holdings" in st.session_state:
93+
st.caption("KIS 자동 조회 데이터가 로드됐습니다. 수정 후 계산하기를 누르세요.")
94+
_h = st.session_state["kis_holdings"]
95+
_initial = pd.DataFrame(
96+
[
97+
{
98+
"종목코드": h.ticker,
99+
"종목명": h.name,
100+
"시장": h.market,
101+
"수량": h.quantity,
102+
"평균단가(원)": int(h.avg_price_krw),
103+
"현재가(원)": int(h.current_price_krw),
104+
}
105+
for h in _h
106+
]
107+
)
108+
else:
109+
st.caption("아래 표에 직접 입력하거나 편집하세요.")
110+
_initial = pd.DataFrame(
111+
{
112+
"종목코드": ["005930", "AAPL"],
113+
"종목명": ["삼성전자", "Apple Inc"],
114+
"시장": ["KR", "US"],
115+
"수량": [10, 5],
116+
"평균단가(원)": [70_000, 1_800_000],
117+
"현재가(원)": [78_000, 2_100_000],
118+
}
119+
)
45120

46121
edited = st.data_editor(
47-
_DEFAULT,
122+
_initial,
48123
num_rows="dynamic",
49124
use_container_width=True,
50125
column_config={
@@ -60,7 +135,6 @@
60135
st.warning("종목을 1개 이상 입력해 주세요.")
61136
st.stop()
62137

63-
# HoldingRecord 생성
64138
from sentinelq.adapters.kis_history import HoldingRecord
65139
from sentinelq.portfolio.after_tax import calculate_after_tax
66140

@@ -134,7 +208,6 @@
134208
else:
135209
st.info("보유 종목 없음")
136210

137-
# 세션 상태에 포트폴리오 저장 (리밸런싱 페이지 연동)
138211
st.session_state["portfolio"] = portfolio
139212
st.caption("※ 이 포트폴리오 데이터가 리밸런싱 계산기 페이지에서 자동 로드됩니다.")
140213

sentinelq/adapters/kis_history.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,12 +659,26 @@ def inquire_overseas_balance(
659659
return results
660660

661661

662+
def fetch_balance(
663+
*,
664+
env: Env = "live",
665+
account: str | None = None,
666+
confirm_live: bool = False,
667+
) -> list[HoldingRecord]:
668+
"""국내·해외 잔고 통합 조회 (포트폴리오 대시보드용)."""
669+
kr = inquire_domestic_balance(env=env, account=account, confirm_live=confirm_live)
670+
us = inquire_overseas_balance(env=env, account=account, confirm_live=confirm_live)
671+
return kr + us
672+
673+
662674
__all__ = [
675+
"SECRETS_DIR",
663676
"Env",
664677
"HoldingRecord",
665678
"KisApiError",
666679
"ProfitRecord",
667680
"Transaction",
681+
"fetch_balance",
668682
"inquire_domestic_balance",
669683
"inquire_domestic_daily_trans",
670684
"inquire_overseas_balance",

tests/test_nts_form.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -238,15 +238,15 @@ def test_summary_csv_has_header_and_key_fields_ac9():
238238
csv_str = export_summary_csv(form)
239239

240240
lines = csv_str.strip().splitlines()
241-
assert lines[0] == "field,value"
241+
assert lines[0] == "항목,금액(원)"
242242

243243
fields = {row.split(",")[0] for row in lines[1:]}
244244
for key in (
245-
"tax_year",
246-
"total_proceeds_krw",
247-
"national_tax_krw",
248-
"local_tax_krw",
249-
"total_tax_krw",
245+
"과세연도",
246+
"양도가액",
247+
"국세(양도소득세)",
248+
"지방소득세",
249+
"납부세액 합계",
250250
):
251251
assert key in fields, f"summary CSV missing field: {key}"
252252

@@ -263,13 +263,13 @@ def test_detail_csv_per_line_columns_ac10():
263263
lines = csv_str.strip().splitlines()
264264
header_cols = lines[0].split(",")
265265
for col in (
266-
"market",
267-
"ticker",
268-
"sell_date",
269-
"quantity",
270-
"proceeds_krw",
271-
"acquisition_cost_krw",
272-
"realized_gain_krw",
266+
"시장",
267+
"종목코드",
268+
"매도일",
269+
"수량",
270+
"양도가액(원)",
271+
"취득가액(원)",
272+
"양도차익(원)",
273273
):
274274
assert col in header_cols
275275

0 commit comments

Comments
 (0)