트레이딩 프로그램에 GUI가 반드시 필요한 것은 아니다.
백그라운드 자동매매나 종목 스크리너는 CLI 프로그램으로도 충분히 만들 수 있다.
하지만 다음 기능이 필요하다면 데스크톱 GUI가 유용할 수 있다.
Python에서는 Qt 계열 바인딩을 이용해 이런 프로그램을 만들 수 있다.
오래된 예제에서는 PyQt5를 자주 볼 수 있지만, 새 프로젝트라면 현재 사용하려는 PyQt/PySide 버전의 공식 문서와 라이선스 조건을 확인한 뒤 선택하는 것이 좋다.
이 글에서는 구조 설명을 위해 PySide6 스타일을 사용한다.
python -m pip install PySide6 matplotlib
import sys
from PySide6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
)
class MainWindow(
QMainWindow
):
def __init__(self):
super().__init__()
self.setWindowTitle(
"Trading Dashboard"
)
label = QLabel(
"Ready"
)
self.setCentralWidget(
label
)
app = QApplication(
sys.argv
)
window = MainWindow()
window.show()
sys.exit(
app.exec()
)
GUI 프로그램은 일반적인 순차 실행 코드와 다르다.
프로그램 실행
→ 이벤트 루프 시작
→ 클릭 대기
→ 네트워크 이벤트 대기
→ 타이머 이벤트
→ 화면 갱신
Qt의 app.exec()가 이벤트 루프를 시작한다.
버튼 클릭처럼 이벤트가 발생하면 연결된 함수를 실행한다.
from PySide6.QtWidgets import (
QPushButton,
)
button = QPushButton(
"조회"
)
button.clicked.connect(
self.load_data
)
def load_data(self):
print(
"데이터 조회"
)
이벤트 기반 API를 이해할 때도 비슷한 사고방식이 사용된다.
버튼 좌표를 직접 지정하기보다 레이아웃을 사용한다.
from PySide6.QtWidgets import (
QLabel,
QPushButton,
QVBoxLayout,
QWidget,
)
class MainWidget(
QWidget
):
def __init__(self):
super().__init__()
layout = (
QVBoxLayout()
)
layout.addWidget(
QLabel(
"Connection"
)
)
layout.addWidget(
QPushButton(
"Connect"
)
)
self.setLayout(
layout
)
대표적인 레이아웃:
QVBoxLayoutQHBoxLayoutQGridLayoutself.statusBar().showMessage(
"Connected"
)
트레이딩 GUI에서는 다음 상태를 표시할 수 있다.
API 연결 여부
현재 모드: PAPER / LIVE
마지막 시세 수신 시간
DB 상태
마지막 주문 상태
1초마다 현재 시각을 갱신해보자.
from datetime import datetime
from PySide6.QtCore import (
QTimer,
)
self.timer = QTimer(
self
)
self.timer.timeout.connect(
self.update_clock
)
self.timer.start(
1000
)
def update_clock(self):
now = datetime.now()
self.statusBar().showMessage(
now.strftime(
"%H:%M:%S"
)
)
다음 함수가 10초 걸린다고 하자.
def load_all_market_data():
...
버튼 이벤트에서 그대로 실행하면:
def on_click(self):
load_all_market_data()
10초 동안 화면이 멈춘 것처럼 보일 수 있다.
GUI 이벤트 루프가 작업을 처리하지 못하기 때문이다.
오래 걸리는 네트워크·DB 작업은 worker로 분리한다.
개념적인 구조:
Main GUI Thread
↓
Worker
↓
Broker API / DB
↓
Signal
↓
GUI Update
Qt에서는 QThread, QRunnable, QThreadPool 등을 활용할 수 있다.
나쁜 구조:
def buy_button_clicked(self):
requests.post(
"/order",
...
)
좋은 구조:
GUI
↓
OrderController
↓
RiskManager
↓
OrderService
↓
BrokerClient
GUI는 사용자 입력과 출력만 담당한다.
class TradingController:
def __init__(
self,
order_service,
):
self.order_service = (
order_service
)
def buy(
self,
symbol,
quantity,
):
return (
self.order_service
.submit_market_buy(
symbol,
quantity,
)
)
GUI:
def on_buy_clicked(self):
symbol = (
self.symbol_input
.text()
.strip()
)
quantity = (
self.quantity_input
.value()
)
self.controller.buy(
symbol,
quantity,
)
실전 모드라면 확인 다이얼로그를 둘 수 있다.
종목: AAA
수량: 10
주문: 시장가 매수
정말 주문하시겠습니까?
또한:
같은 UI 안전장치를 고려할 수 있다.
간단한 경우 QTableWidget을 사용할 수 있다.
대규모 데이터라면 Qt의 Model/View 구조를 사용하는 편이 좋다.
QTableView
↓
Model
↓
DataFrame / Domain Data
UI와 데이터 저장 구조를 분리할 수 있다.
from matplotlib.backends.backend_qtagg import (
FigureCanvasQTAgg,
)
from matplotlib.figure import (
Figure,
)
class PriceCanvas(
FigureCanvasQTAgg
):
def __init__(self):
figure = Figure()
self.axes = (
figure.subplots()
)
super().__init__(
figure
)
def draw_prices(
self,
dates,
prices,
):
self.axes.clear()
self.axes.plot(
dates,
prices,
)
self.axes.set_title(
"Price"
)
self.draw()
새로운 시세가 들어올 때마다 전체 데이터를 다시 불러오기보다 최근 데이터를 메모리에 유지할 수 있다.
from collections import deque
prices = deque(
maxlen=300
)
def on_quote(
quote,
):
prices.append(
quote.price
)
canvas.draw_prices(
range(
len(prices)
),
prices,
)
실제로는 화면 갱신 빈도를 제한하는 것이 좋다.
시세가 초당 수십 번 들어오는데 매번 그래프를 다시 그리면 UI가 느려질 수 있다.
시세 수신
초당 50회
화면 갱신
초당 2~5회
데이터는 모두 처리하되 GUI는 일정 주기로만 업데이트할 수 있다.
self.chart_timer = (
QTimer(self)
)
self.chart_timer.timeout.connect(
self.refresh_chart
)
self.chart_timer.start(
500
)
GUI 안에 모든 오류를 숨기기보다 로그를 별도 파일에도 남긴다.
import logging
logging.basicConfig(
filename="trading.log",
level=logging.INFO,
)
GUI에는 최근 로그만 표시하고 전체 기록은 파일이나 DB에 남기는 방식이 좋다.
전략 설정 파일을 불러오는 경우:
from PySide6.QtWidgets import (
QFileDialog,
)
path, _ = (
QFileDialog
.getOpenFileName(
self,
"설정 파일 선택",
"",
"JSON (*.json)",
)
)
예:
{
"mode": "paper",
"max_position_ratio": 0.1,
"symbols": [
"AAA",
"BBB"
]
}
Python:
import json
from pathlib import Path
config = json.loads(
Path(
"config.json"
).read_text(
encoding="utf-8"
)
)
trading-app/
├─ gui/
│ ├─ main_window.py
│ ├─ widgets/
│ └─ models/
├─ broker/
│ └─ client.py
├─ services/
│ ├─ market_data.py
│ └─ order.py
├─ strategy/
├─ risk/
├─ storage/
└─ main.py
GUI가 없어도 핵심 로직은 테스트할 수 있어야 한다.
다음 프로그램은 오히려 CLI나 백그라운드 서비스가 적합할 수 있다.
GUI는 목표가 아니라 운영 편의성을 위한 인터페이스다.