Full-stack · Cloud AI

Zenfolio

A unified portfolio dashboard across trading platforms — upload a screenshot of your trading history and an AI parser turns it into structured buy, sell and dividend records.

Year
2025
Role
Solo — software engineer across two Go services and a React client
Zenfolio's transaction history: a filterable, sortable table of trades across two brokers, with buy, sell and dividend rows colour-coded and each showing price, quantity, amount and currency.
3
Services
Go
Backend language

Overview

Anyone holding stocks across more than one broker has the same problem: each platform shows you its own slice, and none of them show you the whole position. Zenfolio pulls those slices into one dashboard — total assets, realised and unrealised return, annualised rate via XIRR, and gain/loss broken down per holding.

The awkward part is getting the data in. Brokers export inconsistently and many don’t export usefully at all, so Zenfolio takes the path users actually have available: upload a screenshot of your trading history and let the system read it.

My Role

Solo — the React client, both Go services, the data model, and the container setup.

Architecture

Three containerised services behind Docker Compose.

The React client (Vite, Redux Toolkit, shadcn/ui, with components developed in Storybook) talks to a single REST API.

The core API in Go and Gin handles transactions, portfolio state, chart metrics and authentication, persisting through GORM to MySQL. It is also where screenshot parsing lives: uploaded images go to Gemini 2.5 Flash behind a small ModelType abstraction, so the model is a swappable implementation rather than something welded into the handlers.

The price service is a separate Go microservice. It owns all contact with the external stock-price API and fronts it with a Redis cache, which is the reason it is separate at all — price data is shared across every user and every holding, changes on its own schedule, and is rate-limited upstream. Caching it inside the core API would have meant coupling portfolio logic to somebody else’s quota.

Choosing Go here was partly deliberate range: the other backends in this portfolio are Node and Python, and I wanted the concurrency model and deployment story of a compiled service to be something I had actually shipped rather than read about.