Benson Osei-Mensah

Selected work · 02 of 04

GSETracker

A calm read of the Ghana Stock Exchange, built on a market that publishes no API.

The problem

Most people investing on the Ghana Stock Exchange do it from a phone, often for the first time, with broker statements in PDF and prices that are hard to find. They don’t need a trading terminal. They need a calm, credible answer to “how am I doing, and what moved?”

The exchange publishes no developer API. Live prices exist only as rendered web pages, and the authoritative closing price sits on a separate page that is rendered in JavaScript and protected by Cloudflare. Everything else is built on that.

The shape of it

GSETracker data flow from the exchange to the readerExchange websiteno APIScheduletrading hours, GMTScrapersCelery workersNewsannouncements, RSSTimescaleDBprices since 2007Redislatest tick, cooldownsAPIFastAPIWebMobilePublic market pageAssistantreads freely, writes by consent
Scrapers feed one time-series store. Every surface reads through the same API.

During trading hours a worker reads the market every thirty minutes. At 15:35 GMT a headless browser opens the closing-price page and records the day’s settled numbers. Prices land in TimescaleDB, where daily candles are kept up to date automatically; the latest tick also goes into Redis so alerts never wait on the database. A FastAPI service serves the web app, an Expo mobile app, and a public market page that works without an account.

Decisions that mattered

Rank the sources, never average them. Intraday prices come from the live market page. The closing price comes only from the settlement page, which is authoritative. A third source exists purely as a fallback. Knowing which number wins in a disagreement is the whole job of a price service.

Assume the exchange will be wrong sometimes. One afternoon the closing page served data from 2023 a minute after the close. Now a stale result triggers retries at thirty, sixty and a hundred and twenty minutes; if all fail, I get an email and the gap is recorded. A nightly check compares the closing history with itself and reports problems but never repairs them. A tool that quietly fixes data also quietly hides why it broke.

Thin markets need care. Some stocks don’t trade for days. Their last real price carries forward, a zero is never mistaken for a price, and a scrape that suddenly finds a fifth fewer stocks than usual raises a warning instead of passing as a quiet day.

The assistant asks before it acts. The in-app assistant, built on Claude, can read a user’s own portfolio freely. Anything that would change it is only a proposal: it is staged, the user confirms it within fifteen minutes, and it runs exactly once. For testing, a scripted fake model plays back refusals, mid-stream failures and action cards on demand, and it is blocked from ever running in production.

Measure before fixing. Users kept getting signed out. Instead of guessing, I read the session records: the refresh window only extended when someone visited, so anyone who skipped a week was logged out. The refresh token moved into a thirty-day, HTTP-only cookie with CSRF protection, and the mobile app keeps its own token in the device’s secure store.

Telemetry can’t break the product. Analytics events are written inside a savepoint, so a failed event can never roll back the trade or alert it describes.

What broke, and what it taught

  • A backfill wrote the right prices to the wrong days. A one-off script built rows by position in an array and shifted 108 of them onto other trading dates. Every row is now validated by shape, not position, and the nightly check would catch it within a day.
  • Production ran with development mounts for months. A deploy override that was meant to clear a setting merged with it instead, without any error. The fix was an explicit override and a check after every deploy that asserts what production actually looks like.
  • Two copies of the same rule drifted apart. Alert thresholds lived in two places and stopped agreeing. One was deleted. A rule gets one home.
  • One busy feed type crowded out the rest. A query meant to show the latest item of each kind let the noisiest kind fill the feed. Plain ordering by time fixed it, and a migration repaired the history.

Where it stands

Price history back to 2007 across every listed stock, with about 113,000 settled daily closes and a market page anyone can open. The schedule runs itself; I only hear about it when the exchange does something new.