728x90
주요기능
Tkinter 기반 GUI
- Python 표준 GUI 라이브러리인 Tkinter(ttk)를 사용해 윈도우, 탭, 콤보박스, 리스트박스, 버튼 등으로 구성된 인터페이스 제공
- Notebook 탭으로 여러 차트를 손쉽게 전환 가능
설정 파일로 관리되는 종목 리스트
- stocks.properties 파일을 읽어 [STOCK] 섹션에 정의된 종목 코드와 이름을 로드
- 콤보박스에서 미리 정의된 종목 선택 또는 직접 코드 입력으로 조회
FinanceDataReader를 이용한 데이터 취득
- 지정한 종목의 최근 2년치 일별 시세(종가)를 FinanceDataReader로 한 번에 받아옴
- 데이터를 캐시에 저장해, 동일 종목을 다시 조회할 때 네트워크 호출을 최소화
4가지 핵심 기술적 지표 계산 및 시각화
- Bollinger Bands: 이동평균±2표준편차 밴드로 과매수·과매도 구간 파악
- Z-score: 가격이 이동평균으로부터 표준편차 단위로 얼마나 벗어났는지 수치화
- Percent B (%B): 현재가가 밴드 내에서 어느 위치에 있는지 백분율로 표시
- Mean Reversion Probability: 가격이 평균으로 회귀할 확률을 표준정규분포로 추정
차트 탭별 대화형 시각화
- 각 탭마다 Matplotlib 차트를 Tkinter 위에 내장(TkAgg)
- 툴바로 확대·이동·저장 기능 제공
- 최신 지표 값을 제목 또는 라벨에 표시해 한눈에 파악
종목 순서 편집기
- “종목 순서 변경” 버튼으로 별도 창을 띄워 종목 리스트를 추가·순서 변경·저장
- 저장 시 stocks.properties를 덮어써서 다음 실행에 반영
코드 분석
모듈 및 라이브러리 로딩
Tkinter & ttk
- tkinter와 ttk 위젯을 이용해 데스크탑 GUI를 구성합니다.
- messagebox로 에러/정보 팝업을 띄웁니다.
Pandas & FinanceDataReader
- pandas로 시계열 데이터 처리 (DataFrame).
- FinanceDataReader (fdr) 로 한국 주식 시세를 손쉽게 가져옵니다.
날짜/시간, 설정 파일
- datetime, timedelta로 날짜 계산.
- configparser로 stocks.properties에서 종목 목록을 읽고 저장합니다.
Matplotlib 설정
- 백엔드로 TkAgg를 사용해 Tkinter 위젯 안에 그래프를 그립니다.
- 한글 폰트(맑은 고딕)와 음수 기호 깨짐 방지 설정을 수행합니다.
fetch_data 함수
def fetch_data(symbol: str, period: int = 20) -> pd.DataFrame:
...
입력:
- symbol: "035420.KS" 또는 "035420" 형태
- period: 기본 20일(이동평균 계산 기간)
처리 흐름:
- .KS 등 마켓 구분자 제거
- 오늘 날짜 기준 2년 전부터 데이터 조회
- Close 종가 시리즈 추출
- 이동평균(MA) & 표준편차(Std) 계산
- Z-score, Percent B, Mean-Reversion Probability(MR Prob) 계산
- Z-score = (현재가 – MA) / Std
- %B = (현재가 – ( MA – 2 Std )) / (4 Std)
- MR Prob = 2·Φ(|Z|) – 1 (표준 정규분포 CDF 이용)
- 출력: MA, Std, Z-score, %B, MR Prob 컬럼이 추가된 DataFrame
StockApp 클래스: GUI 애플리케이션
1. 초기화 (__init__)
- 윈도우 설정
- 제목, 초기 크기(1000×700)
- 설정 파일 로딩
- stocks.properties의 [STOCK] 섹션을 읽어
- self.stock_map: {코드: 이름}
- self.symbols, self.names 리스트 생성
- stocks.properties의 [STOCK] 섹션을 읽어
- 스타일
- ttk.Style로 탭 글꼴, 선택된 탭 색 지정
- 데이터 캐시:
- self.data_cache에 종목별 DataFrame 저장 → 재조회 방지
- 설명 문구 매핑
- 탭별로 아래 설명을 self.desc_map에 저장
- 예) 'Bollinger': “볼린저 밴드는 이동평균선에서 ±2σ …”
- 탭별로 아래 설명을 self.desc_map에 저장
- 상단 컨트롤
- 콤보박스: 설정 파일 기반 종목 선택
- Entry: 직접 종목 코드 입력
- 버튼: 조회 및 “종목 순서 변경”
- 탭 위젯
- Notebook에 4개 탭 등록: %B, Bollinger, Z-score, MR Prob
- 각 탭에 차트 캔버스(FigureCanvasTkAgg), 툴바, 설명 라벨을 동적으로 생성
2. 데이터 캐시 관리 (get_cached_data)
- 키(symbol_period)로 캐시 확인
- 없으면 fetch_data 호출, 실패 시 빈 DataFrame 저장 및 에러 팝업
3. UI 갱신 (update)
콤보박스 선택 또는 엔터/버튼 클릭 시 호출
절차:
- 캐시 초기화
- 코드/이름 결정 (entry 우선)
- 각 탭별로
- 기존 캔버스·툴바·라벨 제거
- 차트 함수 호출(create_*_figure) → Figure 반환
- FigureCanvasTkAgg와 NavigationToolbar2Tk 배치
- 하단에 설명 라벨 생성
4. 차트 생성 메서드
공통:
- get_cached_data로 DataFrame 가져옴
- Figure(figsize=(8,4), dpi=100) → ax = fig.add_subplot(111)
- 선/영역 그리기, 제목·범례·그리드 설정, fig.tight_layout()
create_bollinger_figure
- 종가, MA, ±2σ 선
- 밴드 영역(fill_between) 반투명 채우기
create_zscore_figure
- Z-score 선
- ±2 기준선(axhline)
create_percentb_figure
- %B 선, 기준선 1과 0
- 제목에 최신 %B 값 표시
create_mrprob_figure
- MR Prob 선, Y축 [0,1] 제한
- 제목에 최신 확률 값 표시
5. OrderEditor: 종목 순서 편집기
- Toplevel 창
- 리스트박스에 현재 종목 순서 표시
- 추가
- 코드/이름 입력 후 “추가” 버튼
- 중복 검사, 리스트 갱신
- 순서 이동
- “위로”/“아래로” 버튼으로 선택 항목 교환
- 저장
- configparser로 [STOCK] 섹션 재작성
- 파일에 출력 → “프로그램 재시작 시 적용” 팝업
6. 실행 흐름
if __name__ == "__main__":
main()
- main()에서 tk.Tk() 생성 후 StockApp 인스턴스 초기화
- root.mainloop()로 이벤트 루프 진입
728x90
stock_chart.py
import tkinter as tk
from tkinter import ttk, messagebox
import pandas as pd
# 국내 주식 데이터를 편리하게 가져오기 위한 FinanceDataReader 라이브러리
import FinanceDataReader as fdr
from datetime import datetime, timedelta
import configparser
# Matplotlib 설정 (TkAgg 백엔드 사용)
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.figure import Figure
import matplotlib.font_manager as fm
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import math
# ——— 설정 파일 읽기 (stocks.properties) ———
# 파일 예시 (stocks.properties):
# [STOCK]
# 035420=NAVER
# 323410=카카오뱅크
# 326030=SK바이오팜
# 005610=SPC삼립
# 017670=SK텔레콤
# ——— 한글 폰트 설정 (Windows) ———
font_path = r'C:\Windows\Fonts\malgun.ttf' # '맑은 고딕' 폰트 경로
font_name = fm.FontProperties(fname=font_path).get_name()
# 전역 폰트 패밀리로 설정
matplotlib.rc('font', family=font_name)
# 음수 기호 깨짐 방지
matplotlib.rcParams['axes.unicode_minus'] = False
PROP_FILE = 'stocks.properties'
def fetch_data(symbol: str, period: int = 20) -> pd.DataFrame:
"""
오늘까지 2년치 국내 주식 데이터를 가져오고,
볼린저 밴드와 Z-score, %B, Mean Reversion Probability 계산
"""
# symbol에 '.KS' 등 붙었으면 제거하여 종목 코드만 추출
code = symbol.split('.')[0] if '.' in symbol else symbol
# 날짜 설정: 오늘까지, 2년 전부터
end_date = datetime.today().date()
start_date = end_date - pd.DateOffset(years=2)
# FinanceDataReader로 국내 주식 시세 조회
df = fdr.DataReader(code, start_date, end_date)
if df.empty:
raise ValueError(f"데이터를 불러올 수 없습니다: {code}")
close = df['Close'] # 종가 Series
# 이동평균과 표준편차 계산 (기본 기간=20일)
ma = close.rolling(window=period).mean()
std = close.rolling(window=period).std()
# DataFrame에 지표 컬럼 추가
df['MA'] = ma
df['Std'] = std
df['Z-score'] = (close - ma) / std
# Percent B: (Price - lower_band) / (upper_band - lower_band)
df['%B'] = (close - (ma - 2 * std)) / (4 * std)
# Mean Reversion Probability: 2*Phi(|Z|)-1
# 표준 정규분포 CDF Phi(x) = 0.5*(1+erf(x/sqrt(2)))
df['MR Prob'] = 2 * 0.5 * (1 + df['Z-score'].abs().apply(lambda x: math.erf(x / math.sqrt(2)))) - 1
return df
class StockApp:
"""메인 GUI 애플리케이션 클래스"""
def __init__(self, master):
self.master = master
master.title("주식 지표 대시보드")
master.geometry('1000x700') # 초기 창 크기 설정
self.config = configparser.ConfigParser()
self.config.read(PROP_FILE, encoding='utf-8')
self.stock_map = dict(self.config.items('STOCK'))
self.symbols = list(self.stock_map.keys())
self.names = list(self.stock_map.values())
style = ttk.Style()
style.theme_use('default')
style.configure(
'TNotebook.Tab',
padding=[10,5],
font=('맑은 고딕', 11),
)
style.map(
'TNotebook.Tab',
background=[('selected','#0078D7')],
forground=[('selected','white')],
)
# 탭별 툴바 및 캔버스 저장용 딕셔너리
self.toolbars = {}
self.canvases = {}
self.labels = {}
self.data_cache = {} # 종목 코드별 데이터 캐시 저장소
# 설명 문구 매핑
self.desc_map = {
'Bollinger': '볼린저 밴드는 이동평균선에서 ±2σ 범위를 표시하여 과매수/과매도 구간을 파악합니다.',
'Z-score': 'Z-score는 현재 가격이 이동평균에서 몇 표준편차 떨어져 있는지를 나타냅니다.',
'%B': '주가가 볼린저 밴드 내 어느 위치에 있는지를 퍼센트로 표시합니다.',
'MR Prob': '평균 회귀 확률은 가격이 평균으로 회귀할 확률을 추정합니다.'
}
# 상단 프레임: 종목 선택 및 입력
top = ttk.Frame(master)
top.pack(fill='x', padx=10, pady=5)
# 콤보박스: 설정 파일 기반 종목 리스트
ttk.Label(top, text="종목 선택:", font=(None,11)).pack(side='left')
# 콤보박스 설정
self.combo = ttk.Combobox(
top,
values=self.names,
state='readonly',
font=('맑은 고딕', 11),
)
self.combo.current(0)
self.combo.pack(side='left', padx=5)
self.combo.bind('<<ComboboxSelected>>', self.update)
# 직접 종목 코드 입력 가능
ttk.Label(
top,
text=" 또는 코드 입력:",
font=('맑은 고딕', 11),
).pack(side='left')
self.entry = ttk.Entry(top, font=('맑은 고딕', 11), width=12)
self.entry.pack(side='left', padx=5)
self.entry.bind('<Return>', self.update)
ttk.Button(top, text="조회", command=self.update).pack(side='left', padx=5)
ttk.Button(
top,
text="종목 순서 변경",
command=self.open_editor,
).pack(side='right', padx=5)
# 탭 위젯 생성
self.nb = ttk.Notebook(master)
self.nb.pack(fill='both', expand=True)
# 탭 구성: 제목과 Figure 생성 함수 매핑
tabs = [
("%B", self.create_percentb_figure),
("Bollinger", self.create_bollinger_figure),
("Z-score", self.create_zscore_figure),
("MR Prob", self.create_mrprob_figure)
]
# 각 탭 프레임 추가 및 초기화
for title, _ in tabs:
frame = ttk.Frame(self.nb)
self.nb.add(frame, text=title)
self.toolbars[title] = None
self.canvases[title] = None
self.labels[title] = None
self.tabs = dict(tabs)
# 초기 차트 로드
self.update()
def get_cached_data(self, symbol: str, period: int = 20) -> pd.DataFrame:
"""종목 데이터 캐시: 이미 있으면 재사용, 없으면 fetch"""
key = f"{symbol}_{period}"
if key not in self.data_cache:
try:
self.data_cache[key] = fetch_data(symbol, period)
except Exception as e:
messagebox.showerror("데이터 오류", str(e))
self.data_cache[key] = pd.DataFrame()
return self.data_cache[key]
def update(self, event=None):
"""콤보박스 또는 입력창 변경 시 호출되어 모든 탭 차트 갱신"""
# 입력창 우선, 없으면 콤보박스 선택값 사용
self.data_cache.clear()
code = self.entry.get().strip() or self.symbols[self.combo.current()]
name = code if self.entry.get().strip() else self.names[self.combo.current()]
# 각 탭 갱신
for title, func in self.tabs.items():
frame = self.nb.nametowidget(self.nb.tabs()[list(self.tabs.keys()).index(title)])
# 기존 요소 제거
if self.canvases[title]:
self.canvases[title].get_tk_widget().destroy()
if self.toolbars[title]:
self.toolbars[title].destroy()
if self.labels[title]:
self.labels[title].destroy()
# 차트 생성 및 배치
fig = func(code, name)
canvas = FigureCanvasTkAgg(fig, master=frame)
toolbar = NavigationToolbar2Tk(canvas, frame)
toolbar.update()
toolbar.pack(side='top', fill='x')
canvas.get_tk_widget().pack(fill='both', expand=True)
# 설명 라벨 추가
lbl = ttk.Label(frame, text=self.desc_map[title], wraplength=600, font=(None,10))
lbl.pack(side='bottom', fill='x', pady=5)
# 참조 저장
self.canvases[title] = canvas
self.toolbars[title] = toolbar
self.labels[title] = lbl
def create_bollinger_figure(self, symbol: str, name: str, period=20) -> Figure:
"""단일 종목의 볼린저 밴드 차트 Figure 생성"""
df = self.get_cached_data(symbol, period)
# Figure 객체 생성 (크기: 8x4인치, 해상도:100dpi)
fig = Figure(figsize=(8, 4), dpi=100)
ax = fig.add_subplot(111) # 단일 플롯
# 차트 그리기: 종가, 이동평균, 상단/하단 밴드
ax.plot(df.index, df['Close'], label='종가')
ax.plot(df.index, df['MA'], label=f'MA{period}')
ax.plot(df.index, df['MA'] + 2*df['Std'], linestyle='--', label='+2σ')
ax.plot(df.index, df['MA'] - 2*df['Std'], linestyle='--', label='-2σ')
ax.fill_between(
df.index,
df['MA'] - 2*df['Std'],
df['MA'] + 2*df['Std'],
alpha=0.1 # 반투명 채우기
)
# 제목, 범례, 격자 설정
ax.set_title(f"{name} - Bollinger Bands", fontsize=14, fontweight='bold', pad=8)
ax.legend(loc='upper left', fontsize='small')
ax.grid(True, linestyle=':', linewidth=0.5, alpha=0.7)
# 레이아웃 최적화
fig.tight_layout(pad=2.0)
return fig
def create_zscore_figure(self, symbol: str, name: str, period=20) -> Figure:
"""단일 종목의 Z-score 차트 Figure 생성"""
df = self.get_cached_data(symbol, period)
fig = Figure(figsize=(8, 4), dpi=100)
ax = fig.add_subplot(111)
# Z-score 선 그리기
ax.plot(df.index, df['Z-score'], label='Z-score', linewidth=1)
# 과매수/과매도 기준선 (±2σ)
ax.axhline(2, linestyle='--', label='+2σ', linewidth=0.8, color='red')
ax.axhline(-2, linestyle='--', label='-2σ', linewidth=0.8, color='blue')
# 제목, 축 레이블, 범례, 격자
ax.set_title(f"{name} - Z-score", fontsize=14, fontweight='bold', pad=8)
ax.set_ylabel('Z-score')
ax.legend(loc='upper right', fontsize='small')
ax.grid(True, linestyle=':', linewidth=0.5, alpha=0.7)
fig.tight_layout(pad=2.0)
return fig
def create_percentb_figure(self, symbol: str, name: str, period=20) -> Figure:
"""Percent B 차트 Figure 생성"""
df = self.get_cached_data(symbol, period)
last = df.iloc[-1] # 마지막 행 (오늘 데이터)
fig = Figure(figsize=(8, 4), dpi=100)
ax = fig.add_subplot(111)
# %B 선 그리기 및 기준선
ax.plot(df.index, df['%B'], label='%B')
ax.axhline(1, linestyle='--', color='red', label='1')
ax.axhline(0, linestyle='--', color='blue', label='0')
# 제목에 마지막 %B 값 표시
ax.set_title(
f"{name} - Percent B ({last.name.date()}: {last['%B']:.2f})",
fontsize=14, fontweight='bold'
)
ax.set_ylabel('%B')
ax.legend(loc='upper right', fontsize='small')
ax.grid(True, linestyle=':', linewidth=0.5, alpha=0.7)
fig.tight_layout()
return fig
def create_mrprob_figure(self, symbol: str, name: str, period=20) -> Figure:
"""평균 회귀 확률 차트 Figure 생성"""
df = self.get_cached_data(symbol, period)
last = df.iloc[-1]
fig = Figure(figsize=(8, 4), dpi=100)
ax = fig.add_subplot(111)
# MR Prob 선 그리기
ax.plot(df.index, df['MR Prob'], label='MR Prob')
ax.set_ylim(0, 1) # 확률 범위
# 제목에 마지막 MR Prob 값 표시
ax.set_title(
f"{name} - Mean Reversion Prob ({last.name.date()}: {last['MR Prob']:.2f})",
fontsize=14, fontweight='bold'
)
ax.set_ylabel('Probability')
ax.legend(loc='upper right', fontsize='small')
ax.grid(True, linestyle=':', linewidth=0.5, alpha=0.7)
fig.tight_layout()
return fig
def open_editor(self):
OrderEditor(self)
class OrderEditor:
def __init__(self, app):
self.app = app
self.top = tk.Toplevel(app.master)
self.top.title("종목 순서 편집기")
self.top.geometry("350x600")
self.stocks = list(self.app.stock_map.items())
self.listbox = tk.Listbox(self.top, font=("맑은 고딕", 11))
self.listbox.pack(fill='both', expand=True, padx=10, pady=10)
for code, name in self.stocks:
self.listbox.insert(tk.END, f"{code} - {name}")
# 추가 입력창
entry_frame = ttk.Frame(self.top)
entry_frame.pack(pady=5)
self.code_entry = ttk.Entry(entry_frame, width=10)
self.code_entry.pack(side='left', padx=5)
self.name_entry = ttk.Entry(entry_frame, width=15)
self.name_entry.pack(side='left', padx=5)
ttk.Button(entry_frame, text="추가", command=self.add_stock).pack(side='left')
btn_frame = ttk.Frame(self.top)
btn_frame.pack()
ttk.Button(btn_frame, text="위로", command=self.move_up).pack(side='left', padx=5)
ttk.Button(btn_frame, text="아래로", command=self.move_down).pack(side='left', padx=5)
ttk.Button(btn_frame, text="저장", command=self.save).pack(side='left', padx=5)
def add_stock(self):
code = self.code_entry.get().strip()
name = self.name_entry.get().strip()
if not code or not name:
messagebox.showwarning("입력 오류", "종목 코드와 이름을 모두 입력하세요.")
return
if code in dict(self.stocks):
messagebox.showwarning("중복", f"{code}는 이미 존재합니다.")
return
self.stocks.append((code, name))
self.refresh(len(self.stocks) - 1)
self.code_entry.delete(0, tk.END)
self.name_entry.delete(0, tk.END)
def move_up(self):
idx = self.listbox.curselection()
if idx and idx[0] > 0:
i = idx[0]
self.stocks[i-1], self.stocks[i] = self.stocks[i], self.stocks[i-1]
self.refresh(i-1)
def move_down(self):
idx = self.listbox.curselection()
if idx and idx[0] < len(self.stocks) - 1:
i = idx[0]
self.stocks[i], self.stocks[i+1] = self.stocks[i+1], self.stocks[i]
self.refresh(i+1)
def refresh(self, selected_idx=0):
self.listbox.delete(0, tk.END)
for code, name in self.stocks:
self.listbox.insert(tk.END, f"{code} - {name}")
self.listbox.select_set(selected_idx)
def save(self):
config = self.app.config
config.remove_section('STOCK')
config.add_section('STOCK')
for code, name in self.stocks:
config.set('STOCK', code, name)
with open(PROP_FILE, 'w', encoding='utf-8') as f:
config.write(f)
messagebox.showinfo("저장 완료", "종목 순서가 저장되었습니다.\n프로그램을 다시 시작하면 적용됩니다.")
def main():
# 애플리케이션 실행 진입점
root = tk.Tk()
StockApp(root)
root.mainloop()
if __name__ == "__main__":
main()
stocks.properties
[STOCK]
035420 = NAVER
323410 = 카카오뱅크
005930 = 삼성전자
066570 = LG전자
...
728x90
'Python for Beginners' 카테고리의 다른 글
Ant Colony Optimization (ACO, 개미 군집 최적화) 알고리즘 (0) | 2025.04.30 |
---|---|
Flet 메모장 (1) | 2024.11.14 |
Flet로 만든 별다방 키오스크(Kiosk) (0) | 2024.11.11 |
Flet GridView로 만든 계산기 (1) | 2024.11.08 |
Flet ListTitle (0) | 2024.11.07 |