Python 데스크톱 트레이딩 대시보드 만들기: Qt와 Matplotlib 구조 이해하기

Alchemist·2026년 10월 6일

KT AIVLE

목록 보기
57/130

트레이딩 프로그램에 GUI가 반드시 필요한 것은 아니다.

백그라운드 자동매매나 종목 스크리너는 CLI 프로그램으로도 충분히 만들 수 있다.

하지만 다음 기능이 필요하다면 데스크톱 GUI가 유용할 수 있다.

  • 연결 상태 확인
  • 관심 종목 조회
  • 계좌 잔고 표시
  • 주문 입력
  • 주문 상태 확인
  • 실시간 로그 출력
  • 가격 차트 표시

Python에서는 Qt 계열 바인딩을 이용해 이런 프로그램을 만들 수 있다.

오래된 예제에서는 PyQt5를 자주 볼 수 있지만, 새 프로젝트라면 현재 사용하려는 PyQt/PySide 버전의 공식 문서와 라이선스 조건을 확인한 뒤 선택하는 것이 좋다.

이 글에서는 구조 설명을 위해 PySide6 스타일을 사용한다.


설치

python -m pip install PySide6 matplotlib

가장 작은 Qt 프로그램

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()
)

Event Loop

GUI 프로그램은 일반적인 순차 실행 코드와 다르다.

프로그램 실행
→ 이벤트 루프 시작
→ 클릭 대기
→ 네트워크 이벤트 대기
→ 타이머 이벤트
→ 화면 갱신

Qt의 app.exec()가 이벤트 루프를 시작한다.


Signal과 Slot

버튼 클릭처럼 이벤트가 발생하면 연결된 함수를 실행한다.

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
        )

대표적인 레이아웃:

  • QVBoxLayout
  • QHBoxLayout
  • QGridLayout

상태 표시

self.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"
        )
    )

GUI 스레드에서 오래 걸리는 작업을 실행하면 안 된다

다음 함수가 10초 걸린다고 하자.

def load_all_market_data():
    ...

버튼 이벤트에서 그대로 실행하면:

def on_click(self):
    load_all_market_data()

10초 동안 화면이 멈춘 것처럼 보일 수 있다.

GUI 이벤트 루프가 작업을 처리하지 못하기 때문이다.


Worker 분리

오래 걸리는 네트워크·DB 작업은 worker로 분리한다.

개념적인 구조:

Main GUI Thread
        ↓
     Worker
        ↓
Broker API / DB
        ↓
     Signal
        ↓
GUI Update

Qt에서는 QThread, QRunnable, QThreadPool 등을 활용할 수 있다.


GUI에서 직접 주문 로직을 만들지 않는다

나쁜 구조:

def buy_button_clicked(self):
    requests.post(
        "/order",
        ...
    )

좋은 구조:

GUI
↓
OrderController
↓
RiskManager
↓
OrderService
↓
BrokerClient

GUI는 사용자 입력과 출력만 담당한다.


View와 서비스 분리

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
주문: 시장가 매수

정말 주문하시겠습니까?

또한:

  • PAPER/LIVE 모드 표시
  • 주문 버튼 색/문구 구분
  • 1회 주문 상한
  • 중복 클릭 방지
  • 주문 중 버튼 비활성화

같은 UI 안전장치를 고려할 수 있다.


테이블로 주문 목록 표시하기

간단한 경우 QTableWidget을 사용할 수 있다.

대규모 데이터라면 Qt의 Model/View 구조를 사용하는 편이 좋다.

QTableView
    ↓
Model
    ↓
DataFrame / Domain Data

UI와 데이터 저장 구조를 분리할 수 있다.


Matplotlib를 Qt에 삽입하기

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)",
    )
)

설정은 GUI 코드에 직접 박지 않는다

예:

{
    "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"
    )
)

트레이딩 GUI 권장 구조

trading-app/
├─ gui/
│  ├─ main_window.py
│  ├─ widgets/
│  └─ models/
├─ broker/
│  └─ client.py
├─ services/
│  ├─ market_data.py
│  └─ order.py
├─ strategy/
├─ risk/
├─ storage/
└─ main.py

GUI가 없어도 핵심 로직은 테스트할 수 있어야 한다.


GUI가 필요 없는 경우

다음 프로그램은 오히려 CLI나 백그라운드 서비스가 적합할 수 있다.

  • 새벽 데이터 수집
  • 정해진 시간의 스크리너
  • 서버 자동매매
  • 백테스트 배치 실행
  • 리포트 생성

GUI는 목표가 아니라 운영 편의성을 위한 인터페이스다.


핵심 정리

  • Qt 기반 Python GUI는 이벤트 루프와 Signal/Slot 구조로 동작한다.
  • 화면 배치는 절대 좌표보다 레이아웃을 사용하는 것이 관리하기 쉽다.
  • 네트워크나 DB처럼 오래 걸리는 작업을 GUI 메인 스레드에서 실행하면 화면이 멈출 수 있다.
  • 주문 로직을 버튼 이벤트에 직접 구현하지 말고 Controller와 Service 계층으로 분리하는 것이 좋다.
  • 실전 주문 GUI에는 PAPER/LIVE 표시, 중복 클릭 방지, 주문 확인 같은 안전장치가 필요하다.
  • Matplotlib의 Qt canvas를 사용하면 가격 차트를 데스크톱 앱에 삽입할 수 있다.
  • 실시간 시세 수신 빈도와 화면 갱신 빈도를 분리하면 GUI 성능을 안정적으로 유지할 수 있다.
  • 자동 데이터 수집이나 서버 실행이 목적이라면 GUI보다 CLI나 백그라운드 서비스가 더 적합할 수 있다.
profile
html_programming_language

0개의 댓글