이미지 로딩 중...

Python으로 알고리즘 트레이딩 봇 만들기 13편 - Streamlit 대시보드 구축 - 슬라이드 1/11
A

AI Generated

2025. 11. 12. · 4 Views

Python으로 알고리즘 트레이딩 봇 만들기 13편 - Streamlit 대시보드 구축

실시간 트레이딩 데이터를 시각화하고 모니터링할 수 있는 Streamlit 대시보드를 구축합니다. 차트, 지표, 포트폴리오 현황을 한눈에 볼 수 있는 인터랙티브한 웹 인터페이스를 만들어봅니다.


목차

  1. Streamlit 기본 설정과 페이지 구조 - 대시보드의 기초를 세우기
  2. 실시간 데이터 연동과 자동 갱신 - 살아있는 대시보드 만들기
  3. Plotly 차트로 주가 데이터 시각화 - 인터랙티브한 차트 만들기
  4. 실시간 메트릭과 KPI 표시 - 핵심 지표 모니터링
  5. 데이터테이블과 필터링 - 상세 정보 탐색하기
  6. 실시간 로그와 알림 시스템 - 중요 이벤트 모니터링
  7. 매매 신호 시각화와 백테스트 결과 - 전략 성과 분석
  8. 세션 상태 관리와 설정 저장 - 사용자 경험 향상
  9. 멀티페이지 구조와 내비게이션 - 기능 확장하기
  10. 성능 최적화와 캐싱 전략 - 빠른 대시보드 만들기

1. Streamlit 기본 설정과 페이지 구조 - 대시보드의 기초를 세우기

시작하며

여러분이 알고리즘 트레이딩 봇을 만들었는데, 실시간으로 무슨 일이 일어나고 있는지 확인할 방법이 없다면 어떨까요? 터미널에 출력되는 로그만 보면서 매매 상황을 파악하기란 정말 어렵습니다.

실제로 많은 개발자들이 봇을 만든 후 모니터링의 중요성을 뒤늦게 깨닫습니다. 수익률은 어떻게 되는지, 어떤 주식을 보유하고 있는지, 지금 매수/매도 신호가 발생했는지 등을 실시간으로 확인하지 못하면 적절한 대응이 불가능합니다.

바로 이럴 때 필요한 것이 Streamlit입니다. 단 몇 줄의 코드만으로 전문적인 웹 대시보드를 만들 수 있어, 여러분의 트레이딩 봇에 눈을 달아줄 수 있습니다.

개요

간단히 말해서, Streamlit은 데이터 과학자와 개발자를 위한 오픈소스 Python 프레임워크로, HTML/CSS/JavaScript 없이도 인터랙티브한 웹 앱을 만들 수 있습니다. 트레이딩 대시보드에서는 실시간 차트, 포트폴리오 현황, 거래 내역 등을 시각화해야 하는데, 일반적인 웹 개발 방식으로는 시간이 너무 오래 걸립니다.

예를 들어, 실시간으로 업데이트되는 주가 차트를 Flask나 Django로 만들려면 WebSocket, 프론트엔드 개발 등 복잡한 작업이 필요합니다. 기존에는 대시보드를 위해 별도의 프론트엔드 개발자가 필요했다면, 이제는 Python만 알면 누구나 전문적인 대시보드를 만들 수 있습니다.

Streamlit의 핵심 특징은 세 가지입니다. 첫째, 순수 Python만으로 UI를 구성할 수 있습니다.

둘째, 코드 변경 시 자동으로 새로고침되어 개발 속도가 빠릅니다. 셋째, 데이터 시각화 라이브러리(Plotly, Matplotlib 등)와 완벽하게 통합됩니다.

이러한 특징들이 트레이딩 대시보드를 빠르게 구축하는 데 결정적인 역할을 합니다.

코드 예제

import streamlit as st
import pandas as pd
from datetime import datetime

# 페이지 설정 - 와이드 모드와 제목 지정
st.set_page_config(page_title="알고리즘 트레이딩 대시보드", layout="wide")

# 제목과 설명
st.title("📈 알고리즘 트레이딩 대시보드")
st.markdown("실시간 트레이딩 봇 모니터링 시스템")

# 사이드바에 컨트롤 패널 생성
with st.sidebar:
    st.header("⚙️ 설정")
    refresh_rate = st.slider("새로고침 주기(초)", 1, 60, 5)
    st.info(f"마지막 업데이트: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

# 메인 레이아웃을 3개 컬럼으로 분할
col1, col2, col3 = st.columns(3)

with col1:
    st.metric("총 자산", "₩10,500,000", "+5.2%")

설명

이것이 하는 일: Streamlit을 사용해 트레이딩 대시보드의 기본 골격을 만들고, 사용자가 쉽게 정보를 확인할 수 있도록 레이아웃을 구성합니다. 첫 번째로, st.set_page_config()를 사용해 페이지의 전체적인 설정을 합니다.

layout="wide"는 대시보드가 브라우저 전체 너비를 사용하도록 만들어, 차트와 데이터를 더 넓게 표시할 수 있습니다. 트레이딩 대시보드는 많은 정보를 동시에 보여줘야 하므로 와이드 레이아웃이 필수적입니다.

그 다음으로, st.sidebar를 사용해 왼쪽에 설정 패널을 만듭니다. 여기에는 새로고침 주기 같은 사용자 설정을 배치하는데, 슬라이더(st.slider)를 통해 1초부터 60초까지 값을 조절할 수 있습니다.

이렇게 하면 사용자가 네트워크 상황이나 선호에 따라 업데이트 빈도를 조정할 수 있습니다. 마지막으로, st.columns(3)으로 메인 영역을 3개의 컬럼으로 나눕니다.

각 컬럼에는 st.metric()을 사용해 중요한 지표를 카드 형태로 표시할 수 있습니다. 메트릭은 값과 함께 변화율도 표시해서, 총 자산이 얼마나 증가했는지 한눈에 파악할 수 있습니다.

여러분이 이 코드를 사용하면 몇 분 안에 전문적인 대시보드 레이아웃을 구축할 수 있습니다. 코드를 저장하고 streamlit run dashboard.py만 실행하면 브라우저가 자동으로 열리면서 대시보드가 나타나며, 코드를 수정하면 즉시 반영되어 개발 속도가 매우 빠릅니다.

실전 팁

💡 st.set_page_config()는 반드시 스크립트의 첫 번째 Streamlit 명령어여야 합니다. 다른 st 함수보다 먼저 호출하지 않으면 에러가 발생하니 주의하세요.

💡 와이드 레이아웃을 사용할 때는 컬럼을 3-4개로 나누는 것이 최적입니다. 너무 많은 컬럼은 오히려 가독성을 떨어뜨립니다.

💡 st.sidebar에는 설정과 필터만 배치하고, 실제 데이터는 메인 영역에 표시하세요. 이렇게 하면 사용자가 정보 계층을 쉽게 파악할 수 있습니다.

💡 개발 중에는 st.write()로 디버깅 정보를 출력하면 편리합니다. 배포 시에는 이를 제거하거나 st.expander()로 감싸서 숨기세요.

💡 st.cache_data 데코레이터를 사용하면 데이터 로딩 함수의 결과를 캐싱해서 성능을 크게 향상시킬 수 있습니다.


2. 실시간 데이터 연동과 자동 갱신 - 살아있는 대시보드 만들기

시작하며

대시보드를 만들었는데 데이터가 고정되어 있다면 의미가 없겠죠? 트레이딩은 초 단위로 상황이 변하는데, 수동으로 새로고침 버튼을 눌러야 한다면 실시간 모니터링이 불가능합니다.

많은 초보 개발자들이 Streamlit으로 대시보드를 만든 후, 어떻게 실시간으로 데이터를 업데이트할지 막막해합니다. 일반적인 웹 앱처럼 WebSocket이나 polling을 직접 구현해야 할까요?

바로 이럴 때 필요한 것이 Streamlit의 자동 갱신 기능과 캐싱 전략입니다. 최소한의 코드로 실시간 데이터 스트리밍을 구현할 수 있습니다.

개요

간단히 말해서, Streamlit의 자동 갱신은 st.rerun()과 캐싱 메커니즘을 활용해 주기적으로 데이터를 업데이트하는 방식입니다. 트레이딩 봇에서는 현재 주가, 포트폴리오 가치, 최근 거래 내역 등이 계속 변하기 때문에 이를 실시간으로 반영해야 합니다.

예를 들어, 5초마다 API를 호출해서 최신 주가를 가져오고, 이를 차트에 반영해야 하는데, 매번 전체 페이지를 다시 로드하면 성능이 떨어집니다. 기존에는 setInterval과 AJAX를 사용해서 복잡한 비동기 처리를 했다면, 이제는 Streamlit의 @st.cache_data로 데이터를 캐싱하고 st.rerun()으로 주기적으로 갱신할 수 있습니다.

핵심은 두 가지입니다. 첫째, @st.cache_data를 사용해서 데이터베이스나 API 호출 결과를 캐싱하여 불필요한 요청을 줄입니다.

둘째, time.sleep()st.rerun()을 조합해서 자동 갱신 루프를 만듭니다. 이를 통해 서버 부하를 최소화하면서도 실시간 업데이트를 구현할 수 있습니다.

코드 예제

import streamlit as st
import time
import pandas as pd
from datetime import datetime

# 데이터 로딩 함수 - TTL 5초로 캐싱
@st.cache_data(ttl=5)
def load_portfolio_data():
    # 실제로는 데이터베이스나 API에서 가져옴
    return pd.DataFrame({
        '종목': ['삼성전자', 'SK하이닉스', 'NAVER'],
        '보유량': [10, 5, 3],
        '현재가': [71000, 123000, 210000],
        '수익률': [5.2, -2.3, 8.7]
    })

@st.cache_data(ttl=5)
def load_recent_trades():
    # 최근 거래 내역 로드
    return pd.DataFrame({
        '시간': [datetime.now()] * 3,
        '종목': ['삼성전자', 'NAVER', 'SK하이닉스'],
        '유형': ['매수', '매도', '매수'],
        '가격': [71000, 210000, 123000]
    })

# 자동 갱신 설정
refresh_rate = st.sidebar.slider("새로고침 주기(초)", 1, 60, 5)

# 데이터 로드 및 표시
st.subheader("📊 포트폴리오 현황")
portfolio_df = load_portfolio_data()
st.dataframe(portfolio_df, use_container_width=True)

st.subheader("📝 최근 거래")
trades_df = load_recent_trades()
st.dataframe(trades_df, use_container_width=True)

# 자동 갱신 구현
time.sleep(refresh_rate)
st.rerun()

설명

이것이 하는 일: 트레이딩 데이터를 주기적으로 가져와서 대시보드에 자동으로 반영하며, 캐싱을 통해 성능을 최적화합니다. 첫 번째로, @st.cache_data(ttl=5) 데코레이터를 사용해 데이터 로딩 함수를 캐싱합니다.

TTL(Time To Live)을 5초로 설정하면, 5초 동안은 캐시된 데이터를 사용하고 그 이후에는 함수를 다시 실행해서 새로운 데이터를 가져옵니다. 이렇게 하면 불필요한 데이터베이스 쿼리나 API 호출을 줄일 수 있어 성능이 크게 향상됩니다.

그 다음으로, 실제 데이터 소스(데이터베이스, API, 파일 등)에서 포트폴리오와 거래 내역을 가져옵니다. 이 예제에서는 간단히 DataFrame을 반환하지만, 실제로는 SQLite나 MongoDB에서 데이터를 조회하거나 증권사 API를 호출하게 됩니다.

st.dataframe()을 사용하면 데이터를 인터랙티브한 테이블로 표시할 수 있고, use_container_width=True로 컨테이너 전체 너비를 사용합니다. 마지막으로, time.sleep(refresh_rate)로 지정된 시간만큼 대기한 후 st.rerun()을 호출해서 전체 스크립트를 다시 실행합니다.

이것이 자동 갱신의 핵심 메커니즘입니다. 사용자가 설정한 주기마다 대시보드가 자동으로 새로고침되며, 캐시 TTL이 만료되었다면 새로운 데이터를 가져와서 표시합니다.

여러분이 이 코드를 사용하면 실시간으로 변하는 트레이딩 데이터를 모니터링할 수 있습니다. 사용자는 슬라이더로 갱신 주기를 조절할 수 있어, 네트워크 상황이나 중요도에 따라 업데이트 빈도를 최적화할 수 있습니다.

또한 캐싱 덕분에 서버와 데이터베이스에 과부하를 주지 않으면서도 빠른 응답 속도를 유지합니다.

실전 팁

💡 TTL 값은 데이터의 중요도와 변경 빈도에 따라 조절하세요. 주가는 5초, 일간 통계는 60초 등 다르게 설정할 수 있습니다.

💡 st.rerun()은 전체 스크립트를 재실행하므로, 무거운 계산은 반드시 캐싱해야 합니다. 그렇지 않으면 성능이 크게 저하됩니다.

💡 실시간 갱신 중에는 사용자 입력(버튼 클릭, 텍스트 입력)이 초기화될 수 있으니, st.session_state로 상태를 유지하세요.

💡 개발 중에는 자동 갱신을 끄고 싶다면, if st.sidebar.checkbox("자동 갱신"): st.rerun() 같은 조건을 추가하세요.

💡 API 호출 시에는 rate limit을 고려해서 TTL을 설정하고, 예외 처리를 반드시 추가해서 API 장애 시에도 대시보드가 작동하도록 만드세요.


3. Plotly 차트로 주가 데이터 시각화 - 인터랙티브한 차트 만들기

시작하며

숫자로만 나열된 주가 데이터를 보면 트렌드를 파악하기 어렵습니다. "삼성전자가 어제보다 올랐나?

최근 한 달 동안은?" 같은 질문에 답하려면 표를 한참 들여다봐야 합니다. 트레이딩에서 시각화는 선택이 아닌 필수입니다.

캔들스틱 차트, 이동평균선, 거래량 등을 한눈에 보지 못하면 매매 타이밍을 놓치기 쉽습니다. 단순한 정적 이미지가 아닌, 확대/축소하고 특정 시점의 값을 확인할 수 있는 인터랙티브한 차트가 필요합니다.

바로 이럴 때 필요한 것이 Plotly입니다. Streamlit과 완벽하게 통합되어 있어, 클릭 몇 번으로 전문적인 금융 차트를 만들 수 있습니다.

개요

간단히 말해서, Plotly는 인터랙티브한 차트를 만들 수 있는 Python 라이브러리로, Streamlit에서 st.plotly_chart()를 통해 쉽게 사용할 수 있습니다. 트레이딩 대시보드에서는 주가의 변화 추이, 여러 종목의 비교, 거래량과 가격의 관계 등을 시각화해야 합니다.

예를 들어, 최근 30일간의 주가 움직임을 캔들스틱 차트로 보여주고, 사용자가 마우스를 올리면 정확한 시가/고가/저가/종가를 확인할 수 있어야 합니다. 기존에는 Matplotlib으로 정적 차트를 그렸다면, 이제는 Plotly로 확대/축소, 팬, 호버 툴팁 등의 기능이 있는 동적 차트를 만들 수 있습니다.

Plotly의 핵심 장점은 세 가지입니다. 첫째, 별도의 JavaScript 코딩 없이 Python만으로 D3.js 수준의 인터랙티브 차트를 만들 수 있습니다.

둘째, 금융 차트에 특화된 plotly.graph_objects를 제공해서 캔들스틱, OHLC 차트를 쉽게 그릴 수 있습니다. 셋째, 여러 차트를 하나의 figure에 서브플롯으로 배치할 수 있어 가격과 거래량을 동시에 표시할 수 있습니다.

코드 예제

import streamlit as st
import plotly.graph_objects as go
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

@st.cache_data(ttl=10)
def load_stock_data(ticker, days=30):
    # 실제로는 야후 파이낸스나 증권사 API에서 가져옴
    dates = pd.date_range(end=datetime.now(), periods=days, freq='D')
    base_price = 70000
    data = pd.DataFrame({
        'date': dates,
        'open': base_price + np.random.randn(days) * 1000,
        'high': base_price + np.random.randn(days) * 1000 + 500,
        'low': base_price + np.random.randn(days) * 1000 - 500,
        'close': base_price + np.random.randn(days) * 1000,
        'volume': np.random.randint(1000000, 5000000, days)
    })
    return data

# 종목 선택
ticker = st.sidebar.selectbox("종목 선택", ["삼성전자", "SK하이닉스", "NAVER"])
df = load_stock_data(ticker)

# 캔들스틱 차트 생성
fig = go.Figure(data=[go.Candlestick(
    x=df['date'],
    open=df['open'],
    high=df['high'],
    low=df['low'],
    close=df['close'],
    name=ticker
)])

# 레이아웃 설정
fig.update_layout(
    title=f"{ticker} 주가 차트 (최근 30일)",
    yaxis_title="가격 (원)",
    xaxis_title="날짜",
    height=500,
    template="plotly_dark"  # 다크 테마 적용
)

st.plotly_chart(fig, use_container_width=True)

설명

이것이 하는 일: 주가 데이터를 캔들스틱 차트로 변환하여 가격의 변동 추이를 직관적으로 보여주며, 인터랙티브한 기능으로 상세 분석을 가능하게 합니다. 첫 번째로, load_stock_data() 함수에서 특정 종목의 과거 주가 데이터를 가져옵니다.

실제 프로젝트에서는 야후 파이낸스(yfinance), 한국투자증권 API, 또는 자체 데이터베이스에서 OHLC(시가, 고가, 저가, 종가) 데이터와 거래량을 조회합니다. 이 데이터는 캐싱되어 있어서 같은 종목을 다시 선택해도 10초 동안은 즉시 로드됩니다.

그 다음으로, go.Candlestick()을 사용해 캔들스틱 차트 객체를 생성합니다. 캔들스틱은 하루의 주가 움직임을 한 개의 '캔들'로 표현하는데, 몸통은 시가와 종가를, 꼬리는 고가와 저가를 나타냅니다.

상승한 날은 녹색(또는 빨간색), 하락한 날은 빨간색(또는 파란색)으로 자동 표시되어 트렌드를 직관적으로 파악할 수 있습니다. 마지막으로, fig.update_layout()으로 차트의 외관을 커스터마이징합니다.

제목, 축 라벨, 높이를 설정하고 template="plotly_dark"로 다크 테마를 적용해서 장시간 모니터링 시 눈의 피로를 줄입니다. st.plotly_chart()로 Streamlit에 표시하면, 사용자는 마우스 휠로 확대/축소하거나, 드래그해서 특정 기간을 자세히 볼 수 있고, 캔들 위에 마우스를 올리면 정확한 OHLC 값을 확인할 수 있습니다.

여러분이 이 코드를 사용하면 전문 트레이딩 플랫폼 수준의 차트를 얻을 수 있습니다. 사용자는 차트를 직접 조작하면서 패턴을 찾고, 특정 날짜의 가격을 확인하며, 스크린샷을 찍어 공유할 수도 있습니다.

또한 Plotly는 반응형이라서 모바일에서도 잘 작동합니다.

실전 팁

💡 캔들스틱 차트에 이동평균선을 추가하려면 fig.add_trace(go.Scatter(...))로 선 차트를 추가하세요. 20일선, 60일선 등을 함께 표시하면 트렌드 분석이 쉬워집니다.

💡 거래량을 함께 표시하려면 make_subplots()를 사용해서 위에는 가격, 아래에는 거래량 막대 차트를 배치하세요. 이것이 표준적인 주식 차트 레이아웃입니다.

💡 fig.update_xaxes(rangeslider_visible=False)를 추가하면 하단의 범위 슬라이더를 숨길 수 있어 차트 공간을 더 확보할 수 있습니다.

💡 다크 테마 외에도 plotly, plotly_white, ggplot2 등 다양한 테마가 있으니 대시보드 분위기에 맞게 선택하세요.

💡 차트 데이터가 너무 많으면(1년 이상) 성능이 저하될 수 있으니, 기본적으로 최근 30-90일 데이터만 로드하고 더 긴 기간은 옵션으로 제공하세요.


4. 실시간 메트릭과 KPI 표시 - 핵심 지표 모니터링

시작하며

대시보드를 열었을 때 가장 먼저 보고 싶은 것은 무엇일까요? "내 계좌에 얼마가 있지?

오늘 수익률은?" 같은 핵심 정보를 빠르게 확인하고 싶을 겁니다. 많은 대시보드가 차트와 표로 가득 차 있지만, 정작 가장 중요한 숫자들은 찾기 어렵습니다.

스크롤을 내려야 하거나, 표 안에 숨어있어서 한눈에 파악이 안 됩니다. 특히 트레이딩에서는 초 단위로 변하는 총 자산, 수익률, 당일 손익 같은 KPI를 즉시 확인해야 합니다.

바로 이럴 때 필요한 것이 Streamlit의 메트릭 컴포넌트입니다. 큰 숫자로 핵심 지표를 표시하고, 증감률을 색상과 화살표로 시각적으로 보여줄 수 있습니다.

개요

간단히 말해서, st.metric()은 숫자 지표를 카드 형태로 표시하는 컴포넌트로, 값과 함께 변화량(델타)을 자동으로 계산해서 보여줍니다. 트레이딩 대시보드의 최상단에는 총 자산, 당일 수익률, 보유 종목 수, 실현 손익 같은 KPI가 한눈에 보여야 합니다.

예를 들어, 총 자산이 1천만원에서 1천50만원으로 증가했다면 "+5.0% ↑" 같은 표시와 함께 녹색으로 강조되어야 좋은 소식임을 즉시 알 수 있습니다. 기존에는 이런 메트릭 카드를 만들려면 HTML/CSS를 직접 작성해야 했다면, 이제는 st.metric()한 줄로 전문적인 KPI 카드를 만들 수 있습니다.

메트릭의 핵심 특징은 세 가지입니다. 첫째, 라벨, 값, 델타를 명확하게 구분해서 표시합니다.

둘째, 델타가 양수면 녹색 상향 화살표, 음수면 빨간색 하향 화살표를 자동으로 표시합니다. 셋째, delta_color="off" 또는 delta_color="inverse"로 색상 규칙을 커스터마이징할 수 있습니다(예: 비용이 줄어든 경우 음수여도 좋은 의미).

코드 예제

import streamlit as st
import random

@st.cache_data(ttl=5)
def get_account_metrics():
    # 실제로는 데이터베이스에서 계좌 정보 조회
    total_asset = 10500000
    prev_asset = 10000000
    asset_change = total_asset - prev_asset
    asset_change_pct = (asset_change / prev_asset) * 100

    return {
        'total_asset': total_asset,
        'asset_change': asset_change,
        'asset_change_pct': asset_change_pct,
        'day_profit': 150000,
        'day_profit_pct': 1.5,
        'holdings_count': 5,
        'realized_profit': 850000
    }

# 메트릭 데이터 로드
metrics = get_account_metrics()

# 상단에 4개의 컬럼으로 주요 지표 배치
st.subheader("💰 계좌 현황")
col1, col2, col3, col4 = st.columns(4)

with col1:
    st.metric(
        label="총 자산",
        value=f"₩{metrics['total_asset']:,}",
        delta=f"{metrics['asset_change_pct']:.1f}%"
    )

with col2:
    st.metric(
        label="당일 손익",
        value=f"₩{metrics['day_profit']:,}",
        delta=f"{metrics['day_profit_pct']:.1f}%"
    )

with col3:
    st.metric(
        label="보유 종목",
        value=f"{metrics['holdings_count']}개",
        delta="+1"  # 오늘 추가된 종목 수
    )

with col4:
    st.metric(
        label="누적 실현손익",
        value=f"₩{metrics['realized_profit']:,}",
        delta=None  # 델타 표시 안 함
    )

설명

이것이 하는 일: 트레이딩 계좌의 핵심 지표를 카드 형태로 시각화하고, 전일 대비 또는 이전 기간 대비 변화를 직관적으로 보여줍니다. 첫 번째로, get_account_metrics() 함수에서 계좌 관련 데이터를 가져옵니다.

실제 시스템에서는 데이터베이스에 저장된 거래 내역을 집계해서 총 자산, 실현 손익, 미실현 손익 등을 계산합니다. 또한 전일 종가 기준 자산과 비교해서 변화율을 계산하는데, 이 변화율이 메트릭의 델타로 표시됩니다.

그 다음으로, st.columns(4)로 화면을 4등분해서 각 컬럼에 하나씩 메트릭 카드를 배치합니다. 이렇게 하면 가로로 나란히 정렬되어 한 화면에 여러 지표를 동시에 볼 수 있습니다.

각 메트릭은 label(제목), value(주요 값), delta(변화량)로 구성됩니다. 마지막으로, st.metric()이 자동으로 델타의 부호를 판단해서 색상을 적용합니다.

양수면 녹색 상향 화살표가 표시되고, 음수면 빨간색 하향 화살표가 나타나, 사용자가 숫자를 읽지 않아도 증가/감소를 즉시 인식할 수 있습니다. 델타가 필요 없는 경우(누적 실현손익처럼 절대값만 중요한 경우)는 delta=None을 설정합니다.

여러분이 이 코드를 사용하면 대시보드를 열자마자 계좌 상태를 3초 안에 파악할 수 있습니다. 총 자산이 늘었는지, 오늘 얼마나 벌었는지, 몇 개 종목을 보유했는지 모든 정보가 상단에 크게 표시되어, 스크롤 없이 모니터링할 수 있습니다.

실시간 갱신과 결합하면 주가 변동에 따라 메트릭이 계속 업데이트되어 살아있는 대시보드가 됩니다.

실전 팁

💡 금액은 천 단위 구분자(,)를 사용해서 가독성을 높이세요. f"₩{value:,}"처럼 포매팅하면 10000000이 아닌 10,000,000으로 표시됩니다.

💡 델타에는 절대값 대신 퍼센트를 표시하는 것이 더 직관적입니다. "₩500,000"보다 "5.0%"가 의미를 빠르게 파악할 수 있습니다.

💡 비용이나 손실 같이 줄어드는 것이 좋은 지표는 delta_color="inverse"를 사용하세요. 그러면 음수일 때 녹색으로 표시됩니다.

💡 메트릭이 너무 많으면(6개 이상) 중요도에 따라 2줄로 나누세요. 첫 줄에는 핵심 지표, 두 번째 줄에는 보조 지표를 배치하는 것이 좋습니다.

💡 전일 대비 외에 "시작 시점 대비", "월초 대비" 같은 다양한 비교 기준을 탭으로 제공하면 더 유용합니다.


5. 데이터테이블과 필터링 - 상세 정보 탐색하기

시작하며

차트와 메트릭으로 전체적인 상황은 파악했는데, 이제 "어떤 종목이 가장 많이 올랐지? 손실 중인 종목은?" 같은 세부 정보를 알고 싶습니다.

많은 대시보드가 예쁜 차트는 많지만, 정작 원본 데이터를 확인하거나 특정 조건으로 필터링하기는 어렵습니다. 특히 트레이딩에서는 수십 개의 거래 내역이나 보유 종목을 정렬하고 검색할 수 있어야 의사결정을 내릴 수 있습니다.

바로 이럴 때 필요한 것이 Streamlit의 데이터테이블과 필터링 기능입니다. 사용자가 직접 데이터를 정렬하고, 검색하고, 특정 조건에 맞는 항목만 볼 수 있게 만들 수 있습니다.

개요

간단히 말해서, Streamlit의 st.dataframe()은 인터랙티브한 테이블을 제공하고, 위젯들(st.selectbox, st.multiselect 등)과 조합해서 동적 필터링을 구현할 수 있습니다. 트레이딩 대시보드에서는 포트폴리오의 모든 종목을 표로 보여주되, 사용자가 "수익 종목만", "기술주만", "거래량 많은 순" 같은 조건으로 필터링하고 정렬할 수 있어야 합니다.

예를 들어, 손실이 5% 이상인 종목만 빨간색으로 강조해서 보여주면, 리스크 관리가 쉬워집니다. 기존에는 테이블에 필터 기능을 추가하려면 JavaScript와 복잡한 프론트엔드 로직이 필요했다면, 이제는 Pandas DataFrame 필터링과 Streamlit 위젯만으로 강력한 데이터 탐색 기능을 만들 수 있습니다.

핵심은 세 가지입니다. 첫째, st.dataframe()은 기본적으로 정렬 기능이 내장되어 있어 컬럼 헤더를 클릭하면 오름차순/내림차순으로 정렬됩니다.

둘째, Pandas의 조건 필터링(df[df['수익률'] > 0])을 사용해서 동적으로 데이터를 필터링합니다. 셋째, st.dataframe()의 스타일링 기능으로 특정 셀에 색상을 적용해서 중요한 정보를 강조할 수 있습니다.

코드 예제

import streamlit as st
import pandas as pd
import numpy as np

@st.cache_data(ttl=5)
def load_portfolio_data():
    # 실제로는 데이터베이스에서 조회
    return pd.DataFrame({
        '종목명': ['삼성전자', 'SK하이닉스', 'NAVER', '카카오', '현대차'],
        '섹터': ['반도체', '반도체', 'IT서비스', 'IT서비스', '자동차'],
        '보유량': [10, 5, 3, 15, 8],
        '평균단가': [68000, 125000, 195000, 52000, 180000],
        '현재가': [71000, 123000, 210000, 48000, 190000],
        '평가금액': [710000, 615000, 630000, 720000, 1520000],
        '수익률': [4.41, -1.60, 7.69, -7.69, 5.56],
        '수익금액': [30000, -10000, 45000, -60000, 80000]
    })

df = load_portfolio_data()

# 필터 영역
st.subheader("📊 포트폴리오 상세")

col1, col2, col3 = st.columns(3)
with col1:
    # 섹터 필터
    sectors = ['전체'] + list(df['섹터'].unique())
    selected_sector = st.selectbox("섹터", sectors)

with col2:
    # 수익률 필터
    profit_filter = st.selectbox("수익 상태", ['전체', '수익', '손실'])

with col3:
    # 정렬 기준
    sort_by = st.selectbox("정렬 기준", ['종목명', '수익률', '평가금액'])

# 필터 적용
filtered_df = df.copy()
if selected_sector != '전체':
    filtered_df = filtered_df[filtered_df['섹터'] == selected_sector]

if profit_filter == '수익':
    filtered_df = filtered_df[filtered_df['수익률'] > 0]
elif profit_filter == '손실':
    filtered_df = filtered_df[filtered_df['수익률'] < 0]

# 정렬 적용
filtered_df = filtered_df.sort_values(by=sort_by, ascending=False)

# 스타일링 함수: 수익률에 따라 색상 적용
def color_profit(val):
    color = 'green' if val > 0 else 'red' if val < 0 else 'gray'
    return f'color: {color}'

# 데이터프레임 표시 (스타일 적용)
styled_df = filtered_df.style.applymap(color_profit, subset=['수익률', '수익금액'])
st.dataframe(styled_df, use_container_width=True, height=400)

# 요약 통계
st.caption(f"총 {len(filtered_df)}개 종목 | 평균 수익률: {filtered_df['수익률'].mean():.2f}%")

설명

이것이 하는 일: 포트폴리오의 모든 종목을 테이블로 보여주되, 사용자가 섹터, 수익 상태 등으로 필터링하고 정렬해서 필요한 정보만 볼 수 있게 합니다. 첫 번째로, 포트폴리오 데이터를 DataFrame으로 로드합니다.

실제 시스템에서는 각 종목의 보유량, 매수 평균가, 현재가를 데이터베이스에서 가져와서 평가금액(보유량 × 현재가)과 수익률((현재가 - 평균단가) / 평균단가 × 100)을 계산합니다. 이 데이터는 5초간 캐싱되어 빠르게 로드됩니다.

그 다음으로, 3개의 컬럼에 필터 위젯을 배치합니다. st.selectbox()로 섹터, 수익 상태, 정렬 기준을 선택할 수 있게 하는데, 사용자가 값을 변경하면 즉시 테이블이 업데이트됩니다.

Pandas의 조건 필터링(df[df['섹터'] == selected_sector])을 사용해서 선택된 조건에 맞는 행만 남기고, sort_values()로 정렬합니다. 마지막으로, style.applymap()을 사용해서 수익률과 수익금액 컬럼에 조건부 색상을 적용합니다.

color_profit() 함수가 양수는 녹색, 음수는 빨간색, 0은 회색으로 만들어서, 사용자가 숫자를 읽기 전에 색상만으로 수익/손실을 판단할 수 있습니다. st.dataframe()으로 표시하면 사용자는 컬럼 헤더를 클릭해서 추가로 정렬할 수도 있습니다.

여러분이 이 코드를 사용하면 수십, 수백 개의 종목을 관리할 때도 원하는 정보를 빠르게 찾을 수 있습니다. "반도체 섹터에서 손실 중인 종목만" 같은 복잡한 조건도 클릭 몇 번으로 필터링할 수 있고, 테이블 하단의 요약 통계로 선택된 종목들의 평균 수익률도 즉시 확인할 수 있습니다.

이러한 유연성이 실시간 의사결정을 가능하게 합니다.

실전 팁

💡 st.data_editor()를 사용하면 테이블을 직접 편집할 수도 있습니다. 사용자가 목표가나 메모를 추가할 수 있게 하면 유용합니다.

💡 많은 데이터를 표시할 때는 height 파라미터로 테이블 높이를 제한하면 스크롤이 생겨서 레이아웃이 깔끔해집니다.

💡 스타일링 시 background-color도 적용할 수 있습니다. 수익률 5% 이상은 연한 녹색 배경을 주는 식으로 강조하세요.

💡 st.multiselect()로 여러 섹터를 동시에 선택할 수 있게 하면 더 유연한 필터링이 가능합니다.

💡 큰 DataFrame은 st.dataframe() 대신 st.data_editor()나 AgGrid 같은 서드파티 라이브러리를 사용하면 성능이 더 좋습니다.


6. 실시간 로그와 알림 시스템 - 중요 이벤트 모니터링

시작하며

트레이딩 봇이 돌아가는 동안 "지금 뭐하고 있지? 매수 신호가 발생했나?" 같은 궁금증이 생깁니다.

봇이 조용히 작동하는 것도 좋지만, 중요한 이벤트가 발생했을 때는 즉시 알아야 합니다. 많은 시스템이 로그 파일에만 기록하기 때문에, 무슨 일이 일어났는지 확인하려면 서버에 접속해서 텍스트 파일을 열어봐야 합니다.

특히 매수/매도 체결, API 오류, 비정상적인 가격 변동 같은 중요한 이벤트를 놓치면 큰 손실로 이어질 수 있습니다. 바로 이럴 때 필요한 것이 대시보드의 실시간 로그 표시와 알림 시스템입니다.

최근 이벤트를 시간 순으로 보여주고, 중요도에 따라 색상으로 구분하면 상황을 한눈에 파악할 수 있습니다.

개요

간단히 말해서, 실시간 로그 시스템은 트레이딩 봇의 활동을 기록하고 대시보드에 표시해서, 사용자가 현재 무슨 일이 일어나고 있는지 모니터링할 수 있게 합니다. 트레이딩 봇은 주가 체크, 매매 신호 발생, 주문 체결, API 오류 등 다양한 이벤트를 생성합니다.

예를 들어, "삼성전자 매수 신호 발생 -> 10주 매수 주문 전송 -> 체결 완료" 같은 일련의 과정을 로그로 남기고, 대시보드에서 실시간으로 확인할 수 있어야 합니다. 기존에는 로그를 보려면 tail -f로 터미널을 열어놔야 했다면, 이제는 대시보드의 한 섹션에 최근 로그가 자동으로 표시되고, 에러는 빨간색으로 경고는 노란색으로 강조됩니다.

핵심은 세 가지입니다. 첫째, 로그를 데이터베이스나 파일에 저장하고 최근 N개만 가져와서 표시합니다.

둘째, 로그 레벨(INFO, WARNING, ERROR)에 따라 아이콘과 색상을 다르게 적용해서 중요도를 시각화합니다. 셋째, st.expander()나 별도 탭을 사용해서 로그가 다른 콘텐츠를 가리지 않게 배치합니다.

코드 예제

import streamlit as st
import pandas as pd
from datetime import datetime, timedelta
import random

@st.cache_data(ttl=3)
def load_recent_logs(limit=20):
    # 실제로는 데이터베이스나 로그 파일에서 읽어옴
    log_levels = ['INFO', 'WARNING', 'ERROR', 'SUCCESS']
    messages = [
        '주가 데이터 업데이트 완료',
        '매수 신호 발생: 삼성전자',
        'API 호출 실패 - 재시도 중',
        '10주 매수 체결 완료',
        '손절가 도달: SK하이닉스',
        '일일 거래 한도 도달'
    ]

    logs = []
    for i in range(limit):
        logs.append({
            'timestamp': datetime.now() - timedelta(minutes=i*2),
            'level': random.choice(log_levels),
            'message': random.choice(messages),
            'ticker': random.choice(['삼성전자', 'NAVER', 'SK하이닉스', None])
        })
    return pd.DataFrame(logs)

# 로그 레벨별 아이콘과 색상 매핑
def get_log_style(level):
    styles = {
        'INFO': ('ℹ️', 'blue'),
        'WARNING': ('⚠️', 'orange'),
        'ERROR': ('❌', 'red'),
        'SUCCESS': ('✅', 'green')
    }
    return styles.get(level, ('📝', 'gray'))

# 로그 섹션
st.subheader("📋 활동 로그")

# 로그 필터
col1, col2 = st.columns([1, 3])
with col1:
    log_filter = st.multiselect(
        "필터",
        ['INFO', 'WARNING', 'ERROR', 'SUCCESS'],
        default=['WARNING', 'ERROR', 'SUCCESS']
    )

# 로그 로드
logs_df = load_recent_logs()
if log_filter:
    logs_df = logs_df[logs_df['level'].isin(log_filter)]

# 로그 표시 (최신순)
with st.container(height=400):
    for _, log in logs_df.iterrows():
        icon, color = get_log_style(log['level'])
        timestamp_str = log['timestamp'].strftime('%H:%M:%S')

        # 티커 정보가 있으면 함께 표시
        ticker_info = f"[{log['ticker']}] " if log['ticker'] else ""

        st.markdown(
            f"{icon} **{timestamp_str}** | :{color}[{log['level']}] | "
            f"{ticker_info}{log['message']}"
        )

설명

이것이 하는 일: 트레이딩 봇이 생성하는 모든 로그를 수집해서 대시보드에 시간 순으로 표시하고, 레벨에 따라 시각적으로 구분하여 중요한 이벤트를 강조합니다. 첫 번째로, load_recent_logs() 함수가 최근 로그를 가져옵니다.

실제 시스템에서는 Python의 logging 모듈을 사용해서 로그를 파일이나 데이터베이스(SQLite, MongoDB 등)에 저장하고, 여기서 최근 20-50개를 쿼리합니다. 각 로그에는 타임스탬프, 레벨, 메시지, 관련 종목 같은 정보가 포함됩니다.

그 다음으로, st.multiselect()로 로그 레벨 필터를 제공합니다. 기본적으로 WARNING, ERROR, SUCCESS만 선택되어 있어 중요한 이벤트만 보여주지만, 사용자가 INFO를 추가로 선택하면 모든 활동을 볼 수 있습니다.

이렇게 하면 평소에는 노이즈를 줄이고, 디버깅 시에는 상세 정보를 확인할 수 있습니다. 마지막으로, 각 로그를 순회하면서 st.markdown()으로 표시합니다.

get_log_style() 함수가 로그 레벨에 따라 적절한 이모지와 색상을 반환하고, Streamlit의 색상 문법(:color[text])을 사용해서 레벨을 강조합니다. 시간은 "HH:MM:SS" 형식으로 표시되어 어떤 순서로 이벤트가 발생했는지 추적할 수 있고, 종목명이 있으면 대괄호로 감싸서 어떤 주식 관련 이벤트인지 명확히 합니다.

여러분이 이 코드를 사용하면 트레이딩 봇을 완전히 투명하게 모니터링할 수 있습니다. "왜 매수가 안 되지?" 같은 의문이 생기면 로그를 확인해서 API 오류가 있었는지, 잔고가 부족했는지 즉시 파악할 수 있습니다.

또한 에러 로그가 빨간색으로 튀어서 문제를 놓치지 않고, SUCCESS 로그로 체결 완료를 확인하면 안심할 수 있습니다.

실전 팁

💡 로그가 너무 많으면 st.container(height=400)으로 스크롤 가능한 영역을 만들어서 공간을 절약하세요.

💡 중요한 이벤트(에러, 체결)가 발생하면 st.toast() 또는 st.balloons()로 화면에 알림을 띄울 수 있습니다. 단, 너무 자주 사용하면 방해가 되니 주의하세요.

💡 로그를 CSV로 다운로드할 수 있는 버튼(st.download_button())을 추가하면, 사용자가 나중에 분석하거나 공유할 수 있어 편리합니다.

💡 Python의 logging 모듈과 통합할 때는 커스텀 핸들러를 만들어서 로그를 데이터베이스에 자동으로 저장하도록 설정하세요.

💡 로그에 종목명, 주문ID 같은 메타데이터를 포함시키면, 나중에 특정 거래를 추적하거나 디버깅할 때 매우 유용합니다.


7. 매매 신호 시각화와 백테스트 결과 - 전략 성과 분석

시작하며

여러분의 트레이딩 전략이 실제로 잘 작동하는지 어떻게 알 수 있을까요? "이 전략으로 과거 1년간 거래했다면 얼마나 벌었을까?" 같은 질문에 답하려면 백테스트 결과가 필요합니다.

많은 트레이더들이 전략을 만들고 바로 실전에 투입했다가 손실을 봅니다. 과거 데이터로 시뮬레이션해보지 않아서 전략의 승률, 최대 낙폭, 평균 수익률 같은 중요한 지표를 모르기 때문입니다.

또한 매수/매도 신호가 언제 발생했는지 차트 위에 표시되지 않으면, 전략이 실제로 좋은 타이밍에 거래하는지 검증하기 어렵습니다. 바로 이럴 때 필요한 것이 백테스트 결과 시각화입니다.

주가 차트에 매수/매도 포인트를 마커로 표시하고, 누적 수익률 곡선, 승률 같은 성과 지표를 함께 보여주면 전략의 품질을 객관적으로 평가할 수 있습니다.

개요

간단히 말해서, 백테스트 시각화는 과거 데이터에 전략을 적용한 결과를 차트와 지표로 표현해서, 전략의 수익성과 리스크를 분석할 수 있게 합니다. 트레이딩 전략은 보통 기술적 지표(이동평균, RSI, MACD 등)를 기반으로 매수/매도 신호를 생성합니다.

예를 들어, "단기 이동평균이 장기 이동평균을 상향 돌파하면 매수, 하향 돌파하면 매도" 같은 규칙을 과거 1년 데이터에 적용하면, 어느 날에 매수/매도가 발생했을지 계산할 수 있습니다. 기존에는 백테스트 결과를 엑셀 표로만 보았다면, 이제는 주가 차트 위에 초록색 삼각형(매수)과 빨간색 역삼각형(매도)을 표시하고, 별도 차트로 포트폴리오 가치의 변화를 그려서 전략의 성과를 직관적으로 이해할 수 있습니다.

핵심은 세 가지입니다. 첫째, Plotly의 add_trace()로 기존 주가 차트에 매수/매도 마커를 추가합니다.

둘째, 백테스트 엔진이 계산한 포트폴리오 가치를 시간에 따라 그려서 Buy & Hold 전략과 비교합니다. 셋째, 승률, Sharpe Ratio, 최대 낙폭(MDD) 같은 성과 지표를 메트릭으로 표시합니다.

코드 예제

import streamlit as st
import plotly.graph_objects as go
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

@st.cache_data
def run_backtest(ticker, start_date, end_date):
    # 실제로는 백테스트 엔진 실행
    days = (end_date - start_date).days
    dates = pd.date_range(start=start_date, end=end_date, freq='D')

    # 주가 데이터 생성 (실제로는 API에서 가져옴)
    prices = 70000 + np.cumsum(np.random.randn(len(dates)) * 500)

    # 매수/매도 신호 생성 (단순 예시)
    buy_signals = np.random.choice([True, False], len(dates), p=[0.1, 0.9])
    sell_signals = np.random.choice([True, False], len(dates), p=[0.1, 0.9])

    # 포트폴리오 가치 계산
    portfolio_value = 10000000 + np.cumsum(np.random.randn(len(dates)) * 50000)

    return pd.DataFrame({
        'date': dates,
        'price': prices,
        'buy_signal': buy_signals,
        'sell_signal': sell_signals,
        'portfolio_value': portfolio_value
    })

# 백테스트 설정
st.subheader("🔬 백테스트 결과")

col1, col2, col3 = st.columns(3)
with col1:
    ticker = st.selectbox("종목", ["삼성전자", "SK하이닉스", "NAVER"])
with col2:
    start_date = st.date_input("시작일", datetime.now() - timedelta(days=180))
with col3:
    end_date = st.date_input("종료일", datetime.now())

# 백테스트 실행
if st.button("백테스트 실행", type="primary"):
    with st.spinner("백테스트 실행 중..."):
        result = run_backtest(ticker, start_date, end_date)

        # 성과 지표 계산
        total_return = (result['portfolio_value'].iloc[-1] / result['portfolio_value'].iloc[0] - 1) * 100
        win_rate = 65.5  # 실제로는 거래 내역 분석
        max_drawdown = -12.3  # 실제로는 계산

        # 지표 표시
        col1, col2, col3 = st.columns(3)
        with col1:
            st.metric("총 수익률", f"{total_return:.2f}%")
        with col2:
            st.metric("승률", f"{win_rate:.1f}%")
        with col3:
            st.metric("최대 낙폭(MDD)", f"{max_drawdown:.1f}%", delta_color="inverse")

        # 주가 차트 + 매매 신호
        fig = go.Figure()

        # 주가 라인
        fig.add_trace(go.Scatter(
            x=result['date'], y=result['price'],
            mode='lines', name=ticker,
            line=dict(color='lightblue', width=2)
        ))

        # 매수 신호 마커
        buy_dates = result[result['buy_signal']]['date']
        buy_prices = result[result['buy_signal']]['price']
        fig.add_trace(go.Scatter(
            x=buy_dates, y=buy_prices,
            mode='markers', name='매수',
            marker=dict(symbol='triangle-up', size=12, color='green')
        ))

        # 매도 신호 마커
        sell_dates = result[result['sell_signal']]['date']
        sell_prices = result[result['sell_signal']]['price']
        fig.add_trace(go.Scatter(
            x=sell_dates, y=sell_prices,
            mode='markers', name='매도',
            marker=dict(symbol='triangle-down', size=12, color='red')
        ))

        fig.update_layout(
            title="매매 신호 시각화",
            yaxis_title="가격 (원)",
            height=400
        )
        st.plotly_chart(fig, use_container_width=True)

        # 포트폴리오 가치 변화
        fig2 = go.Figure()
        fig2.add_trace(go.Scatter(
            x=result['date'], y=result['portfolio_value'],
            fill='tozeroy', name='포트폴리오 가치',
            line=dict(color='purple')
        ))
        fig2.update_layout(
            title="포트폴리오 가치 변화",
            yaxis_title="평가액 (원)",
            height=300
        )
        st.plotly_chart(fig2, use_container_width=True)

설명

이것이 하는 일: 트레이딩 전략을 과거 데이터에 적용해서 어떤 수익을 냈을지 시뮬레이션하고, 결과를 차트와 지표로 시각화하여 전략의 유효성을 검증합니다. 첫 번째로, 사용자가 백테스트할 종목과 기간을 선택하고 "백테스트 실행" 버튼을 클릭하면 run_backtest() 함수가 실행됩니다.

이 함수는 실제 백테스트 엔진(예: backtrader, zipline)을 호출하거나, 직접 구현한 로직으로 각 날짜마다 전략의 규칙을 적용해서 매수/매도 신호를 생성하고, 포트폴리오 가치를 계산합니다. 결과는 DataFrame으로 반환되어 차트에 사용됩니다.

그 다음으로, 백테스트 결과에서 총 수익률, 승률, 최대 낙폭 같은 핵심 성과 지표를 계산합니다. 총 수익률은 초기 자본 대비 최종 자본의 증가율이고, 승률은 수익이 난 거래의 비율, MDD는 고점 대비 최대 하락폭입니다.

이 지표들을 st.metric()으로 표시해서 전략이 얼마나 수익성이 있고 리스크가 큰지 한눈에 파악할 수 있습니다. 마지막으로, Plotly를 사용해 두 개의 차트를 그립니다.

첫 번째 차트는 주가 라인에 매수 신호(녹색 상향 삼각형)와 매도 신호(빨간색 하향 삼각형)를 오버레이합니다. 이렇게 하면 전략이 어떤 시점에 거래했는지 시각적으로 확인할 수 있고, "너무 늦게 매수했네" 같은 직관을 얻을 수 있습니다.

두 번째 차트는 시간에 따른 포트폴리오 가치의 변화를 보여줘서, 전략이 꾸준히 성장하는지 아니면 변동이 심한지 판단할 수 있습니다. 여러분이 이 코드를 사용하면 전략을 실전에 투입하기 전에 철저히 검증할 수 있습니다.

여러 기간과 종목에 백테스트를 돌려보고, 수익률이 높으면서도 MDD가 낮은 안정적인 전략을 찾을 수 있습니다. 또한 매매 신호 시각화를 통해 전략의 논리가 실제로 말이 되는지 확인하고, 필요하면 파라미터를 조정해서 최적화할 수 있습니다.

실전 팁

💡 백테스트는 연산이 무거우므로, st.cache_data로 결과를 캐싱하고 같은 설정으로 재실행 시 빠르게 로드되도록 하세요.

💡 여러 전략을 비교하려면 각 전략의 수익률 곡선을 하나의 차트에 그려서 어떤 전략이 더 나은지 시각적으로 비교하세요.

💡 백테스트 결과를 JSON이나 CSV로 저장하는 기능을 추가하면, 나중에 재분석하거나 리포트를 만들 때 유용합니다.

💡 과최적화(overfitting)를 방지하려면 학습 기간과 검증 기간을 나눠서 백테스트하고, 두 기간 모두에서 좋은 성과를 내는지 확인하세요.

💡 수수료와 슬리피지(실제 체결가와 시뮬레이션 가격의 차이)를 백테스트에 반영해야 현실적인 결과를 얻을 수 있습니다. 그렇지 않으면 실전 수익이 백테스트보다 훨씬 낮을 수 있습니다.


8. 세션 상태 관리와 설정 저장 - 사용자 경험 향상

시작하며

대시보드를 사용하다가 새로고침되면 선택했던 종목, 필터 설정이 모두 초기화되어버립니다. 매번 다시 설정해야 한다면 정말 번거롭겠죠?

많은 Streamlit 앱이 상태 관리를 제대로 하지 않아서, 사용자가 버튼을 클릭하거나 페이지가 갱신될 때마다 모든 입력이 사라집니다. 특히 자동 갱신 기능이 있는 대시보드에서는 5초마다 설정이 초기화되면 아예 사용할 수 없습니다.

바로 이럴 때 필요한 것이 st.session_state입니다. 사용자의 선택과 설정을 메모리에 유지해서, 페이지가 재실행되어도 상태가 보존됩니다.

개요

간단히 말해서, st.session_state는 Streamlit의 세션 관리 시스템으로, 페이지 새로고침 사이에 데이터를 유지할 수 있게 해주는 딕셔너리 같은 객체입니다. 트레이딩 대시보드에서는 사용자가 선택한 종목, 기간, 필터 조건, 차트 설정 등을 기억해야 합니다.

예를 들어, 사용자가 "삼성전자"를 선택하고 "수익 종목만 보기"로 필터를 설정했다면, 자동 갱신 후에도 이 설정이 유지되어야 합니다. 기존에는 쿠키나 로컬 스토리지를 사용해서 복잡한 JavaScript 코드를 작성해야 했다면, 이제는 st.session_state['key'] = value 한 줄로 상태를 저장하고 st.session_state['key']로 읽어올 수 있습니다.

세션 상태의 핵심은 세 가지입니다. 첫째, 위젯(selectbox, slider 등)에 key 파라미터를 지정하면 자동으로 세션 상태에 저장됩니다.

둘째, 커스텀 데이터(API 토큰, 계산 결과 등)도 세션 상태에 저장해서 재사용할 수 있습니다. 셋째, 조건부 UI(로그인 여부에 따라 다른 페이지)를 만들 때도 세션 상태로 사용자 상태를 추적합니다.

코드 예제

import streamlit as st
import json
from pathlib import Path

# 세션 상태 초기화 (앱 최초 실행 시)
if 'ticker' not in st.session_state:
    st.session_state['ticker'] = '삼성전자'
if 'refresh_rate' not in st.session_state:
    st.session_state['refresh_rate'] = 5
if 'favorites' not in st.session_state:
    st.session_state['favorites'] = []

# 설정 파일 경로
CONFIG_FILE = Path.home() / '.trading_dashboard_config.json'

def load_config():
    """저장된 설정 불러오기"""
    if CONFIG_FILE.exists():
        with open(CONFIG_FILE, 'r') as f:
            config = json.load(f)
            st.session_state.update(config)
        st.success("이전 설정을 불러왔습니다.")

def save_config():
    """현재 설정 저장하기"""
    config = {
        'ticker': st.session_state['ticker'],
        'refresh_rate': st.session_state['refresh_rate'],
        'favorites': st.session_state['favorites']
    }
    with open(CONFIG_FILE, 'w') as f:
        json.dump(config, f)
    st.success("설정이 저장되었습니다!")

# 사이드바에 설정 관리
with st.sidebar:
    st.header("⚙️ 설정")

    # key를 지정하면 자동으로 세션 상태에 저장됨
    st.selectbox(
        "종목 선택",
        ["삼성전자", "SK하이닉스", "NAVER", "카카오"],
        key='ticker'  # st.session_state['ticker']에 자동 저장
    )

    st.slider(
        "새로고침 주기(초)",
        1, 60, 5,
        key='refresh_rate'  # st.session_state['refresh_rate']에 자동 저장
    )

    # 즐겨찾기 관리
    st.subheader("⭐ 즐겨찾기")
    if st.button("현재 종목 추가"):
        if st.session_state['ticker'] not in st.session_state['favorites']:
            st.session_state['favorites'].append(st.session_state['ticker'])
            st.success(f"{st.session_state['ticker']} 추가됨!")

    # 즐겨찾기 목록 표시
    for fav in st.session_state['favorites']:
        col1, col2 = st.columns([3, 1])
        with col1:
            if st.button(fav, key=f"fav_{fav}"):
                st.session_state['ticker'] = fav
                st.rerun()
        with col2:
            if st.button("❌", key=f"remove_{fav}"):
                st.session_state['favorites'].remove(fav)
                st.rerun()

    st.divider()

    # 설정 저장/불러오기
    col1, col2 = st.columns(2)
    with col1:
        if st.button("💾 저장"):
            save_config()
    with col2:
        if st.button("📂 불러오기"):
            load_config()

# 메인 영역에서 세션 상태 사용
st.write(f"현재 선택된 종목: **{st.session_state['ticker']}**")
st.write(f"새로고침 주기: **{st.session_state['refresh_rate']}초**")

설명

이것이 하는 일: 사용자가 대시보드에서 선택한 종목, 갱신 주기, 즐겨찾기 같은 설정을 메모리와 파일에 저장해서, 페이지 갱신이나 재실행 후에도 계속 유지되도록 만듭니다. 첫 번째로, 앱이 처음 실행될 때 세션 상태를 초기화합니다.

if 'key' not in st.session_state: 패턴을 사용해서, 해당 키가 없을 때만 기본값을 설정합니다. 이렇게 하면 최초 실행 시에는 기본값이 사용되고, 이후에는 사용자가 변경한 값이 유지됩니다.

그 다음으로, 위젯에 key 파라미터를 지정합니다. st.selectbox(..., key='ticker')처럼 하면, 사용자가 값을 변경할 때 자동으로 st.session_state['ticker']에 저장됩니다.

별도로 값을 읽어와서 저장하는 코드를 작성할 필요가 없어서 매우 편리합니다. 즐겨찾기 기능은 리스트를 세션 상태에 저장하고, 버튼 클릭 시 항목을 추가/제거합니다.

마지막으로, save_config()load_config() 함수로 설정을 파일로 저장하고 불러옵니다. 세션 상태는 브라우저 세션 동안만 유지되므로, 사용자가 브라우저를 닫으면 사라집니다.

하지만 JSON 파일로 저장하면 다음에 대시보드를 열었을 때 이전 설정을 복원할 수 있습니다. 파일은 사용자 홈 디렉토리에 숨김 파일(.trading_dashboard_config.json)로 저장됩니다.

여러분이 이 코드를 사용하면 대시보드가 훨씬 사용하기 편해집니다. 자주 보는 종목을 즐겨찾기에 추가해두면 클릭 한 번으로 전환할 수 있고, 선호하는 새로고침 주기를 설정해두면 매번 슬라이더를 조절할 필요가 없습니다.

특히 자동 갱신이 있어도 설정이 초기화되지 않아서, 끊김 없는 모니터링 경험을 제공합니다.

실전 팁

💡 세션 상태의 값을 직접 수정할 때는 st.rerun()을 호출해야 UI에 반영됩니다. 예: st.session_state['count'] += 1; st.rerun()

💡 민감한 정보(API 키, 비밀번호)는 평문 파일에 저장하지 말고, st.secrets나 환경변수를 사용하세요.

💡 세션 상태에 너무 큰 객체(수백 MB DataFrame)를 저장하면 메모리 문제가 발생할 수 있으니, 필요한 데이터만 저장하세요.

💡 여러 탭이나 페이지를 사용하는 멀티페이지 앱에서는 세션 상태가 공유되므로, 키 이름을 명확하게 지어서 충돌을 방지하세요.

💡 로그인 시스템을 만들 때는 st.session_state['authenticated'] 같은 불리언 플래그로 인증 상태를 추적하고, 조건부로 다른 UI를 표시하세요.


9. 멀티페이지 구조와 내비게이션 - 기능 확장하기

시작하며

대시보드에 기능이 늘어나면서 하나의 페이지에 모든 것을 담기 어려워집니다. 차트, 로그, 백테스트, 설정이 한 화면에 있으면 스크롤이 너무 길어지고 정보를 찾기 힘듭니다.

많은 대시보드가 정보 과부하로 사용성이 떨어집니다. "백테스트 결과를 어디서 보지?" 같은 질문에 한참을 스크롤해야 하고, 관련 없는 정보들이 섞여있어 집중하기 어렵습니다.

바로 이럴 때 필요한 것이 멀티페이지 구조입니다. Streamlit의 멀티페이지 기능을 사용하면 기능별로 페이지를 나누고, 사이드바에 내비게이션을 추가해서 원하는 정보로 빠르게 이동할 수 있습니다.

개요

간단히 말해서, Streamlit 멀티페이지는 여러 Python 파일을 페이지로 구성하고, 자동으로 생성되는 내비게이션으로 페이지 간 전환을 가능하게 하는 기능입니다. 트레이딩 대시보드는 여러 기능을 포함합니다: 메인 대시보드(실시간 데이터), 백테스트, 거래 내역, 설정, 도움말 등.

예를 들어, 일상적인 모니터링은 메인 페이지에서 하고, 전략 검증은 백테스트 페이지로, 과거 거래 분석은 거래 내역 페이지로 이동하는 식으로 기능을 분리할 수 있습니다. 기존에는 탭이나 expander로 UI를 나눴지만 한계가 있었다면, 이제는 완전히 독립된 페이지로 만들어서 각 페이지가 자체 로직과 레이아웃을 가질 수 있습니다.

멀티페이지의 핵심은 세 가지입니다. 첫째, pages/ 디렉토리에 Python 파일을 추가하면 자동으로 페이지로 인식됩니다.

둘째, 파일명(또는 docstring)이 사이드바의 내비게이션 항목이 됩니다. 셋째, 각 페이지는 독립적으로 실행되지만 st.session_state로 데이터를 공유할 수 있습니다.

코드 예제

# 프로젝트 구조:
# dashboard.py (메인 파일)
# pages/
#   1_📊_실시간_모니터링.py
#   2_🔬_백테스트.py
#   3_📝_거래_내역.py
#   4_⚙️_설정.py

# === dashboard.py (홈페이지) ===
import streamlit as st

st.set_page_config(
    page_title="알고리즘 트레이딩 대시보드",
    page_icon="📈",
    layout="wide"
)

st.title("📈 알고리즘 트레이딩 대시보드")
st.markdown("---")

# 홈페이지 콘텐츠
col1, col2 = st.columns(2)

with col1:
    st.subheader("🚀 빠른 시작")
    st.markdown("""
    1. 좌측 사이드바에서 페이지를 선택하세요
    2. **실시간 모니터링**: 현재 포트폴리오와 주가 확인
    3. **백테스트**: 전략 성과 검증
    4. **거래 내역**: 과거 거래 분석
    """)

    if st.button("실시간 모니터링 시작", type="primary"):
        st.switch_page("pages/1_📊_실시간_모니터링.py")

with col2:
    st.subheader("📊 오늘의 요약")
    col_a, col_b = st.columns(2)
    with col_a:
        st.metric("총 자산", "₩10,500,000", "+5.2%")
    with col_b:
        st.metric("활성 전략", "3개", "+1")

# === pages/1_📊_실시간_모니터링.py ===
import streamlit as st
import plotly.graph_objects as go

st.title("📊 실시간 모니터링")

# 세션 상태에서 데이터 가져오기
if 'ticker' not in st.session_state:
    st.session_state['ticker'] = '삼성전자'

ticker = st.selectbox("종목 선택", ["삼성전자", "SK하이닉스", "NAVER"], key='ticker')

# 실시간 데이터와 차트 표시
st.subheader(f"{ticker} 실시간 주가")
# ... 차트 코드 ...

# === pages/2_🔬_백테스트.py ===
import streamlit as st

st.title("🔬 백테스트")

st.info("전략을 과거 데이터에 적용하여 성과를 검증합니다.")

# 백테스트 설정 및 실행
# ... 백테스트 코드 ...

설명

이것이 하는 일: 트레이딩 대시보드의 다양한 기능을 독립된 페이지로 분리하고, 자동 생성되는 내비게이션으로 사용자가 원하는 기능으로 빠르게 이동할 수 있게 합니다. 첫 번째로, 프로젝트 루트에 dashboard.py 같은 메인 파일을 만들고, pages/ 디렉토리를 생성합니다.

그런 다음 pages/ 안에 각 기능별로 Python 파일을 만드는데, 파일명 앞에 숫자를 붙이면 순서를 제어할 수 있습니다. 예를 들어, 1_실시간_모니터링.py, 2_백테스트.py 같은 식입니다.

이모지를 파일명에 포함시키면 사이드바 내비게이션에도 표시되어 시각적으로 구분하기 좋습니다. 그 다음으로, 각 페이지 파일은 완전히 독립적인 Streamlit 앱처럼 작동합니다.

st.title(), 위젯, 차트 등을 자유롭게 사용할 수 있고, 메인 파일과는 별개로 실행됩니다. Streamlit이 자동으로 사이드바에 페이지 선택기를 추가해주며, 사용자가 클릭하면 해당 페이지로 전환됩니다.

페이지 간 데이터 공유가 필요하면 st.session_state를 사용하는데, 모든 페이지에서 동일한 세션 상태에 접근할 수 있습니다. 마지막으로, 홈페이지에서 st.switch_page()를 사용해 프로그래밍 방식으로 페이지를 전환할 수도 있습니다.

예를 들어, "시작하기" 버튼을 클릭하면 자동으로 실시간 모니터링 페이지로 이동시킬 수 있습니다. 이렇게 하면 사용자 온보딩이나 워크플로우 가이드를 만들 수 있습니다.

여러분이 이 코드를 사용하면 대시보드를 체계적으로 구성할 수 있습니다. 각 페이지는 하나의 기능에 집중하므로 코드가 깔끔해지고 유지보수가 쉬워지며, 사용자는 필요한 정보만 골라서 볼 수 있어 인지 부하가 줄어듭니다.

또한 새로운 기능을 추가할 때도 pages/ 디렉토리에 파일만 추가하면 되므로 확장성이 뛰어납니다.

실전 팁

💡 페이지 파일명은 숫자_이모지_이름.py 형식을 사용하세요. 예: 1_📊_Dashboard.py. 이렇게 하면 순서와 아이콘이 자동으로 적용됩니다.

💡 각 페이지에 공통으로 필요한 함수는 별도 모듈(utils.py, data.py 등)로 분리해서 임포트하세요. 코드 중복을 줄일 수 있습니다.

💡 페이지 전환 시 상태를 유지하려면 st.session_state를 적극 활용하되, 페이지별 네임스페이스를 만들어 키 충돌을 방지하세요. 예: st.session_state['backtest_config']

💡 홈페이지(메인 파일)는 대시보드 소개와 빠른 링크만 제공하고, 실제 기능은 모두 서브 페이지에 배치하는 것이 좋습니다.

💡 페이지 수가 많아지면(10개 이상) 카테고리별로 서브 디렉토리를 만들 수 있습니다. 예: pages/monitoring/, pages/analysis/ 같은 식으로 구조화하세요.


10. 성능 최적화와 캐싱 전략 - 빠른 대시보드 만들기

시작하며

대시보드가 점점 느려지고 있습니다. 데이터가 많아지고 차트가 복잡해지면서, 페이지 로드에 10초 이상 걸리기도 합니다.

이렇게 느리면 실시간 모니터링이 의미가 없습니다. 많은 대시보드가 매번 데이터베이스를 쿼리하고, 무거운 계산을 반복하며, 불필요한 데이터를 로드해서 성능이 저하됩니다.

특히 자동 갱신 기능이 있으면 5초마다 모든 연산을 다시 하므로 서버와 데이터베이스에 엄청난 부하를 줍니다. 바로 이럴 때 필요한 것이 성능 최적화입니다.

Streamlit의 캐싱, 데이터 샘플링, 비동기 로딩 같은 기법을 사용하면 대시보드를 훨씬 빠르게 만들 수 있습니다.

개요

간단히 말해서, 성능 최적화는 불필요한 연산과 데이터 로딩을 줄이고, 결과를 재사용하며, 사용자에게 필요한 정보만 보여주는 기법들의 조합입니다. 트레이딩 대시보드는 많은 데이터를 다룹니다: 수천 개의 주가 데이터 포인트, 수백 건의 거래 내역, 복잡한 백테스트 계산 등.

예를 들어, 10년치 일봉 데이터를 매번 로드하고 이동평균을 계산하면 느릴 수밖에 없습니다. 하지만 실제로는 최근 3개월 데이터만 보여주고, 이동평균은 한 번 계산해서 캐싱하면 충분합니다.

기존에는 성능 문제를 해결하려면 데이터베이스 인덱싱, CDN, 서버 스케일링 같은 복잡한 작업이 필요했다면, 이제는 Streamlit의 캐싱 데코레이터와 몇 가지 베스트 프랙티스만으로도 10배 이상 빠르게 만들 수 있습니다. 핵심은 네 가지입니다.

첫째, @st.cache_data로 데이터 로딩과 변환 함수를 캐싱합니다. 둘째, 차트에 표시할 데이터 포인트 수를 제한해서(예: 1000개) 렌더링 속도를 높입니다.

셋째, 무거운 계산(백테스트)은 백그라운드 작업으로 분리하거나 결과를 미리 계산해서 저장합니다. 넷째, st.spinner()로 로딩 중임을 표시해서 사용자가 기다릴 수 있게 합니다.

코드 예제

import streamlit as st
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import time

# 데이터 로딩 최적화: TTL 캐싱
@st.cache_data(ttl=300)  # 5분간 캐싱
def load_stock_data(ticker, days=90):
    """주가 데이터 로드 - 캐싱으로 반복 호출 방지"""
    # 실제로는 API나 DB에서 가져옴
    time.sleep(1)  # API 호출 시뮬레이션
    dates = pd.date_range(end=datetime.now(), periods=days, freq='D')
    data = pd.DataFrame({
        'date': dates,
        'close': 70000 + np.cumsum(np.random.randn(days) * 500)
    })
    return data

# 무거운 계산 캐싱: 입력이 같으면 재계산 안 함
@st.cache_data
def calculate_indicators(df):
    """기술적 지표 계산 - 결과를 캐싱"""
    df = df.copy()
    df['MA20'] = df['close'].rolling(window=20).mean()
    df['MA60'] = df['close'].rolling(window=60).mean()
    return df

# 데이터 샘플링: 차트 성능 향상
def downsample_data(df, max_points=1000):
    """데이터 포인트가 너무 많으면 샘플링"""
    if len(df) > max_points:
        # 균등하게 샘플링
        step = len(df) // max_points
        return df.iloc[::step]
    return df

# 메인 대시보드
st.title("⚡ 최적화된 대시보드")

# 설정
ticker = st.selectbox("종목", ["삼성전자", "SK하이닉스", "NAVER"])
days = st.slider("기간(일)", 30, 365, 90)

# 성능 측정
start_time = time.time()

with st.spinner(f"{ticker} 데이터 로딩 중..."):
    # 캐싱된 함수 호출 - 같은 ticker와 days면 즉시 반환
    df = load_stock_data(ticker, days)

    # 지표 계산도 캐싱됨
    df = calculate_indicators(df)

load_time = time.time() - start_time

# 성능 정보 표시
st.caption(f"⏱️ 로딩 시간: {load_time:.3f}초 | 데이터 포인트: {len(df):,}개")

# 차트용 데이터 샘플링
chart_df = downsample_data(df, max_points=500)
st.line_chart(chart_df.set_index('date')['close'])

# 캐시 관리 버튼
if st.button("🔄 캐시 초기화"):
    st.cache_data.clear()
    st.success("캐시가 초기화되었습니다!")
    st.rerun()

# 성능 팁 표시
with st.expander("💡 성능 최적화 팁"):
    st.markdown("""
    - 데이터 로딩 함수는 항상 `@st.cache_data` 사용
    - TTL을 적절히 설정해서 데이터 신선도와 성능 균형
    - 차트에는 최대 1000개 포인트만 표시
    - 무거운 계산은 백그라운드 작업으로 분리
    - 불필요한 컬럼은 DB 쿼리 단계에서 제외
    """)

설명

이것이 하는 일: 데이터 로딩과 계산 결과를 캐싱하고, 차트에 표시할 데이터를 최적화하며, 사용자에게 로딩 상태를 표시해서 대시보드를 빠르고 사용하기 좋게 만듭니다. 첫 번째로, @st.cache_data(ttl=300) 데코레이터를 데이터 로딩 함수에 적용합니다.

이렇게 하면 같은 인자(ticker, days)로 함수를 호출할 때 실제로 실행하지 않고 캐시된 결과를 즉시 반환합니다. TTL(Time To Live)은 300초(5분)로 설정되어, 5분 후에는 자동으로 캐시가 만료되어 새로운 데이터를 가져옵니다.

API나 데이터베이스 호출은 보통 0.5-2초 걸리는데, 캐싱하면 0.001초 미만으로 줄어들어 수백 배 빠릅니다. 그 다음으로, 기술적 지표 계산 같은 무거운 연산도 캐싱합니다.

calculate_indicators() 함수가 이동평균 같은 지표를 계산하는데, DataFrame이 크면 시간이 오래 걸립니다. 하지만 @st.cache_data를 붙이면 같은 DataFrame에 대해서는 한 번만 계산하고 결과를 재사용합니다.

Streamlit은 함수의 인자를 해싱해서 변경 여부를 감지하므로, 데이터가 바뀌면 자동으로 재계산합니다. 마지막으로, downsample_data() 함수로 차트에 표시할 데이터 포인트를 제한합니다.

Plotly나 다른 차트 라이브러리는 수천 개의 포인트를 렌더링하면 느려지는데, 실제로 화면에 표시할 수 있는 해상도는 제한적입니다. 1000개 이상의 포인트가 있으면 균등하게 샘플링해서 500개 정도로 줄이면, 시각적으로는 거의 차이가 없지만 렌더링 속도는 훨씬 빠릅니다.

여러분이 이 코드를 사용하면 대시보드가 즉각 반응하게 됩니다. 같은 종목을 다시 선택하거나 페이지가 자동 갱신될 때 캐시 덕분에 순식간에 로드되고, 차트도 부드럽게 렌더링됩니다.

사용자는 st.spinner()로 로딩 중임을 알 수 있어 기다릴 수 있고, 성능 정보를 보면서 대시보드가 얼마나 최적화되어 있는지 확인할 수 있습니다.

실전 팁

💡 데이터를 수정하는 함수(쓰기 작업)는 @st.cache_data 대신 @st.cache_resource를 사용하거나 캐싱하지 마세요. 예: 데이터베이스 연결 객체는 @st.cache_resource로 캐싱

💡 캐시 크기가 너무 커지면 메모리 문제가 생길 수 있으니, max_entries 파라미터로 캐시 항목 수를 제한하세요. 예: @st.cache_data(max_entries=100)

💡 디버깅 중에는 캐싱이 방해될 수 있으니, 개발 초기에는 캐싱을 끄고 나중에 추가하는 것이 좋습니다.

💡 대용량 DataFrame은 df[['col1', 'col2']] 같이 필요한 컬럼만 선택해서 메모리를 절약하세요.

💡 st.experimental_memost.experimental_singleton은 deprecated되었으니, 최신 버전에서는 @st.cache_data@st.cache_resource를 사용하세요.


#Python#Streamlit#Dashboard#Visualization#Trading#python

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.