DEV Community

Nik
Nik

Posted on

Building a Crypto Portfolio Tracker with PyQt6

I’ve been experimenting with the PyQt6 library by building a small pet project: a lightweight Crypto Portfolio Tracker. The idea was to move one of my command-line scripts into a proper desktop app — load a CSV with my holdings, fetch live prices from Kraken, and show everything in a table.

It turned out to be a fun way to explore PyQt6’s layouts, threading, and event handling.

Project Overview

The app provides three main actions:

  1. Load Portfolio – import a CSV file of holdings.
  2. Refresh Prices – fetch current prices from the Kraken API.
  3. Export CSV – save the updated data back to disk.

Along the way, I added multithreading (so the UI stays responsive), a logging system, and a config file for mapping portfolio symbols to Kraken’s trading pairs.

The Core UI

The main window is made up of:

  • A QTableWidget for portfolio data

  • Buttons for Load, Refresh, and Export.

def setUI(self):
    self.setWindowTitle("CryptoTracker")

    self.portfolio = pd.DataFrame(columns=["symbol", "amount", "price", "total_value"])

    self.table = QTableWidget()
    self.btn_load = QPushButton("Load Portfolio CSV")
    self.btn_load.clicked.connect(self.load_portfolio)

    self.btn_refresh = QPushButton("Refresh Kraken Prices")
    self.btn_refresh.clicked.connect(self.refresh_prices)

    self.btn_export = QPushButton("Export to CSV")
    self.btn_export.clicked.connect(self.export_csv)

    layout = QVBoxLayout()
    layout.addWidget(self.table)
    layout.addWidget(self.btn_load)
    layout.addWidget(self.btn_refresh)
    layout.addWidget(self.btn_export)

    container = QWidget()
    container.setLayout(layout)
    self.setCentralWidget(container)
Enter fullscreen mode Exit fullscreen mode

Loading the Portfolio

A simple CSV file drives the app. Clicking Load Portfolio opens a file dialog, loads the data into a pandas.DataFrame, and displays it in the table.

def load_portfolio(self):
    file_name, _ = QFileDialog.getOpenFileName(self, "Open CSV", "", "CSV Files (*.csv)")
    if file_name:
        self.portfolio = pd.read_csv(file_name)
        self.logger.info(f"Loaded file {file_name}")
        self.update_table()
Enter fullscreen mode Exit fullscreen mode

Example portfolio.csv:

symbol,amount
BTC,0.05
ETH,0.7
DOGE,1000
Enter fullscreen mode Exit fullscreen mode

Fetching Prices (with Multithreading)

One of the most important lessons in GUI programming: never block the UI thread.
Since calling the Kraken API can take time, I used QThreadPool with a custom Worker class:

class Worker(QRunnable):
    def __init__(self, fn, *args, **kwargs):
        super().__init__()
        self.fn = fn
        self.args = args
        self.kwargs = kwargs

    @pyqtSlot()
    def run(self):
        self.fn(*self.args, **self.kwargs)

Enter fullscreen mode Exit fullscreen mode

When the user clicks Refresh Prices, each symbol is processed in a separate worker thread:

def refresh_prices(self):
    if self.portfolio.empty:
        self.logger.error("No data found in portfolio")
        return

    for sym in self.portfolio["symbol"]:
        kraken_pair = self.config['KrakenSymbols'][sym.upper()]
        worker = Worker(self.update_pair_info, kraken_pair, sym)
        self.threadPool.start(worker)
Enter fullscreen mode Exit fullscreen mode

Example config.ini with Kraken symbol mappings:

[KrakenSymbols]
BTC=XXBTZUSD
ETH=XETHZUSD
DOGE=XDGUSD
USDT=USDTZUSD
SOL=SOLUSD

Enter fullscreen mode Exit fullscreen mode

Updating the Table

After fetching prices, the app updates both the DataFrame and the visible table:

def update_table(self):
    self.table.setRowCount(len(self.portfolio))
    self.table.setColumnCount(len(self.portfolio.columns))
    self.table.setHorizontalHeaderLabels(self.portfolio.columns)

    for i, row in self.portfolio.iterrows():
        for j, col in enumerate(self.portfolio.columns):
            self.table.setItem(i, j, QTableWidgetItem(str(row[col])))

Enter fullscreen mode Exit fullscreen mode

Each row then shows:

  • symbol (e.g., BTC)
  • amount (from CSV)
  • price (from Kraken)
  • total_value (amount × price)

Logging

To keep track of what’s happening, I added a simple logger that writes both to app.log and the console. This was especially helpful when debugging API responses and PyInstaller packaging.

Lessons Learned

  • Threading is crucial: without it, the UI froze while waiting for Kraken responses.
  • PyQt6 layouts are flexible, but nested layouts take some trial and error.
  • PyInstaller works, but you need to handle bundled resources like config.ini carefully (sys._MEIPASS trick).
  • Combining pandas with a QTableWidget makes it easy to keep the GUI and data in sync.

Demo and the source code

Here you can find how it looks like: https://youtu.be/YUqXW-J0R_c
The source code could be found here: https://github.com/kolyaiks/crypto_tracker

Top comments (0)