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:
- Load Portfolio – import a CSV file of holdings.
- Refresh Prices – fetch current prices from the Kraken API.
- 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)
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()
Example portfolio.csv:
symbol,amount
BTC,0.05
ETH,0.7
DOGE,1000
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)
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)
Example config.ini with Kraken symbol mappings:
[KrakenSymbols]
BTC=XXBTZUSD
ETH=XETHZUSD
DOGE=XDGUSD
USDT=USDTZUSD
SOL=SOLUSD
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])))
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)