Post

잔고 및 손익 관리

잔고 및 손익 관리 모듈 개발 가이드

개요

본 문서는 ‘자동 주식 매매 프로그램 개발 계획’의 기능 4: 보유 주식 손익 관리 및 웹 대시보드를 구현하기 위한 개발 가이드입니다. Streamlit 앱의 핵심 기능인 포트폴리오(잔고) 조회, 실시간 손익 계산, 그리고 이를 웹 대시보드에 직관적으로 표시하는 로직을 modules/portfolio.py에 구현하는 방법을 설명합니다.

잔고 정보는 modules/trader.pyKisTrader.get_balance() 함수를 통해 한국투자증권 API로부터 가져온 데이터를 기반으로 처리됩니다.

1. 잔고 및 손익 관리의 중요성

항목상세 설명
데이터 소스한국투자증권 API (KisTrader.get_balance())의 잔고 데이터
목표매입 단가와 현재가 비교를 통한 실시간 절대 손익수익률 계산
UI 통합ui/dashboard.py의 탭 3(포트폴리오 및 손익 현황)에 시각화하여 표시

2. modules/trader.py 수정 (선행 작업)

먼저, 잔고 조회 시 보유 종목 리스트(output1) 뿐만 아니라 계좌 총 평가액/예수금(output2) 정보도 필요합니다. 기존 get_balance 메서드를 살짝 수정하여 두 가지 데이터를 모두 반환하도록 합니다.

modules/trader.pyget_balance 메서드 수정:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def get_balance(self):
        """
        주식 잔고 및 계좌 총액 조회
        Returns: (holdings_list, total_summary)
        """
        url = f"{self.base_url}/uapi/domestic-stock/v1/trading/inquire-balance"
        
        # 모의투자와 실전투자 TR ID가 다름
        tr_id = "VTTC8434R" if self.mode == "VIRTUAL" else "TTTC8434R"
        headers = self._get_common_headers(tr_id)
        
        params = {
            "CANO": self.account_no,
            "ACNT_PRDT_CD": self.account_code,
            "AFHR_FLPR_YN": "N",
            "OFL_YN": "",
            "INQR_DVSN": "02",
            "UNPR_DVSN": "01",
            "FUND_STTL_ICLD_YN": "N",
            "FNCG_AMT_AUTO_RDPT_YN": "N",
            "PRCS_DVSN": "00",
            "CTX_AREA_FK100": "",
            "CTX_AREA_NK100": ""
        }
        
        try:
            res = requests.get(url, headers=headers, params=params)
            data = res.json()
            if res.status_code == 200 and data['rt_cd'] == '0':
                # output1: 보유 종목 리스트, output2: 계좌 총 자산 현황
                return data['output1'], data['output2'] 
            else:
                print(f"잔고 조회 실패: {data.get('msg1')}")
                return [], []
        except Exception as e:
            print(f"잔고 조회 에러: {e}")
            return [], []

3. modules/portfolio.py 모듈 구성

API에서 받은 원본 데이터(JSON)는 보기에 불편하므로, UI에 표시하기 좋게 Pandas DataFrame으로 가공하는 분석 로직을 작성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# modules/portfolio.py
import pandas as pd

class PortfolioManager:
    """
    계좌 잔고 데이터를 가공하고 분석하는 클래스
    """
    def __init__(self, trader):
        self.trader = trader

    def get_portfolio_status(self):
        """
        현재 계좌 상태를 조회하여 보기 좋은 포맷으로 반환합니다.
        """
        holdings, summary = self.trader.get_balance()
        
        # 1. 계좌 요약 정보 처리
        if not summary:
            account_info = {
                "total_asset": 0,
                "total_profit": 0,
                "profit_rate": 0.0,
                "deposit": 0
            }
        else:
            # summary는 리스트 형태이며 첫 번째 요소에 데이터가 있음
            s_data = summary[0]
            account_info = {
                "total_asset": int(s_data.get('tot_evlu_amt', 0)), # 총 평가 금액
                "total_profit": int(s_data.get('evlu_pfls_smtl_amt', 0)), # 평가 손익 합계
                "profit_rate": float(s_data.get('evlu_pfls_rt', 0.0)), # 수익률
                "deposit": int(s_data.get('dnca_tot_amt', 0)) # 예수금
            }

        # 2. 보유 종목 리스트 처리
        if not holdings:
            df = pd.DataFrame()
        else:
            # 필요한 컬럼만 추출 및 이름 변경
            df = pd.DataFrame(holdings)
            df = df[['prdt_name', 'hldg_qty', 'pchs_avg_pric', 'prpr', 'evlu_pfls_amt', 'evlu_pfls_rt']]
            df.columns = ['종목명', '보유수량', '매입가', '현재가', '평가손익', '수익률(%)']
            
            # 숫자형 변환 (API는 문자열로 줌)
            df['보유수량'] = df['보유수량'].astype(int)
            df['매입가'] = df['매입가'].astype(float)
            df['현재가'] = df['현재가'].astype(float)
            df['평가손익'] = df['평가손익'].astype(int)
            df['수익률(%)'] = df['수익률(%)'].astype(float)

        return account_info, df

4. UI/UX 개선: 대시보드 통합

4.1. ui/portfolio_ui.py 작성 (시각화)

포트폴리오 대시보드를 그리는 UI 코드입니다. modules/portfolio.py에서 가공한 데이터를 받아 화면에 출력합니다. **파이 차트(Pie Chart)**를 통해 자산 비중을 시각화합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# ui/portfolio_ui.py
import streamlit as st
import plotly.express as px

def render_portfolio_dashboard(account_info, df):
    """
    포트폴리오 현황을 시각화합니다.
    """
    st.header("💰 나의 자산 현황")

    # 1. 계좌 요약 (Metrics)
    col1, col2, col3, col4 = st.columns(4)
    
    col1.metric("총 평가 자산", f"{account_info['total_asset']:,}")
    col2.metric("예수금 (주문가능)", f"{account_info['deposit']:,}")
    
    # 수익이면 빨강(한국 기준), 손실이면 파랑
    profit_color = "normal" 
    if account_info['total_profit'] > 0: profit_color = "off" # Streamlit delta logic

    col3.metric("총 평가 손익", f"{account_info['total_profit']:,}", 
                delta=f"{account_info['profit_rate']}%")

    st.markdown("---")

    # 2. 보유 종목 분석
    if not df.empty:
        col_chart, col_table = st.columns([1, 2])
        
        with col_chart:
            st.subheader("📊 자산 비중")
            # 평가금액 기준 파이 차트
            df['평가금액'] = df['현재가'] * df['보유수량']
            fig = px.pie(df, values='평가금액', names='종목명', hole=0.4)
            fig.update_layout(showlegend=False, margin=dict(t=0, b=0, l=0, r=0))
            st.plotly_chart(fig, use_container_width=True)

        with col_table:
            st.subheader("📝 보유 종목 상세")
            # 스타일링: 수익률에 따라 색상 표시
            st.dataframe(
                df.style.format({
                    "매입가": "{:,.0f}",
                    "현재가": "{:,.0f}",
                    "평가손익": "{:,.0f}",
                    "수익률(%)": "{:.2f}%"
                }).background_gradient(subset=['수익률(%)'], cmap='RdYlGn', vmin=-10, vmax=10),
                use_container_width=True,
                height=300
            )
    else:
        st.info("현재 보유 중인 주식이 없습니다.")

4.2. main.py 최종 통합

이제 main.py를 수정하여 [종목 분석] 탭과 [나의 포트폴리오] 탭으로 화면을 구분합니다. 이렇게 하면 사용자가 분석과 자산 관리를 편리하게 오갈 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# app.py

import time
import streamlit as st
from modules.scraper import StockScraper, fetch_stock_history, fetch_stock_info
from modules.trader import KisTrader
from modules.portfolio import PortfolioManager  # 추가됨
from ui.sidebar import render_sidebar
from ui.dashboard import render_dashboard
from ui.portfolio_ui import render_portfolio_dashboard # 추가됨

st.set_page_config(page_title="AutoTrade Pro", page_icon="📈", layout="wide")

# ... (기존 캐싱 함수 get_trader 등은 유지) ...
@st.cache_resource
def get_trader():
    return KisTrader()

if 'bought_status' not in st.session_state:
    st.session_state['bought_status'] = {}

def main():
    config = render_sidebar()
    ticker = config['ticker']
    period = config['period']
    is_auto = config['is_auto']

    st.title("📈 AI Stock Trading Dashboard")

    # [탭 구성] 기능 분리
    tab_analysis, tab_portfolio = st.tabs(["📊 종목 분석 & 자동매매", "💰 나의 포트폴리오"])

    # -----------------------------------------------------
    # TAB 1: 종목 분석 및 자동 매매 (기존 로직)
    # -----------------------------------------------------
    with tab_analysis:
        if ticker:
            # ... (기존 데이터 수집 및 render_dashboard 호출 코드) ...
            with st.spinner('데이터 수집 중...'):
                df = fetch_stock_history(ticker, period)
                info = fetch_stock_info(ticker)
                scraper = StockScraper(ticker)
                news = scraper.get_news()

            if info and not df.empty:
                render_dashboard(df, info, news)
                current_price = df['Close'].iloc[-1]
                
                # ... (기존 자동 매매 로직은 여기에 위치) ...
                if is_auto:
                   # (자동 매매 로직 코드 생략 - 이전에 작성한 내용 그대로 사용)
                   # ...
                   pass
            else:
                st.error("데이터 오류")

    # -----------------------------------------------------
    # TAB 2: 포트폴리오 관리 (신규 기능)
    # -----------------------------------------------------
    with tab_portfolio:
        trader = get_trader()
        
        # 포트폴리오 매니저 초기화
        portfolio_manager = PortfolioManager(trader)
        
        # 버튼을 눌러야 갱신되도록 (API 호출 절약)
        if st.button("내 자산 현황 조회 (새로고침)"):
            with st.spinner("증권사 계좌 정보를 불러오는 중..."):
                account_info, holdings_df = portfolio_manager.get_portfolio_status()
                render_portfolio_dashboard(account_info, holdings_df)
        else:
            # 초기 로드 시 자동 실행을 원하면 이 else문을 지우고 위 코드를 밖으로 빼세요
            st.info("버튼을 누르면 최신 잔고 정보를 불러옵니다.")

    # -----------------------------------------------------
    # TAB 3: 관심 종목 목록
    # -----------------------------------------------------
    with tab_watchlist:
        # ... 기존 코드 ...
        pass

if __name__ == "__main__":
    main()

최종 결과물

이제 Streamlit 앱을 실행하면 다음과 같은 완전한 기능을 갖추게 됩니다.

  1. 탭 1 (종목 분석): 특정 종목의 차트, 뉴스, 기업 정보를 확인하고 자동 매매를 걸어둘 수 있습니다.
  2. 탭 2 (나의 포트폴리오): 한국투자증권 계좌의 총 자산, 예수금, 수익률을 확인하고, 보유 종목들의 비중을 파이 차트로 볼 수 있습니다.
  3. UI: Pandas Styler를 적용하여 수익률이 플러스면 빨간색 배경, 마이너스면 파란색 배경으로 직관적으로 표시됩니다.

5. 개발 일정 (잔고 및 손익 관리)

본 일정은 ‘자동 주식 매매 프로그램 개발 계획’ 문서의 단계 3(기능 3, 4 구현 및 API 연동)에 포함되는 세부 작업입니다.

단계시작일완료일주요 작업 내용
데이터 연동DateDateKisTrader.get_balance()를 호출하여 잔고 데이터 획득 및 get_portfolio_data() 구현
손익 계산 로직DateDatecalculate_realtime_profit() 함수 내 실시간 현재가 조회 및 절대/상대 손익 계산 로직 구현
UI 시각화DateDateui/dashboard.py에 st.metric 및 st.bar_chart를 활용한 포트폴리오 대시보드 구성
통합 테스트DateDate자동 매매(매수/매도) 후 잔고가 정상적으로 업데이트되는지 확인

6. 연락처 및 참고 사항

잔고 및 손익 관리 모듈 구현에 대한 문의 사항은 Person 또는 백흠경 에게 연락 주시기 바랍니다.

KIS API의 잔고 조회 응답 데이터 구조에 대한 자세한 정보는 File (한국투자증권 API 연동 상세 문서) 파일을 참고합니다.

포트폴리오 관리 최종 검토 및 시연 미팅은 Calendar event 일정에 진행될 예정입니다.