All apps · 0 apps
stockinfo
Docker app from UnraidUser-3287583290's Repository
Overview
Timezone: the "Timezone" setting (TZ) defaults to UTC — set it to your local zone for correct log timestamps. Valid values (TZ identifier column): https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
OpenFIGI (optional): resolves an ISIN to the ticker at your preferred exchange (see "Default exchange"). It works without an API key, but only within OpenFIGI's low anonymous rate limit — once exceeded, resolution falls back to Yahoo search, which may return a different exchange or currency. A free key raises the limit and makes resolution reliable; enter it under "OpenFIGI API key". Register and create a key: https://www.openfigi.com/api
Readme
View on GitHubStockInfo
A small app that serves stock and ETF quotes via a REST API and caches them in a SQLite database. Query by ISIN or by symbol + exchange; the response is JSON with price, currency, timestamp, name and — for ETFs — extras such as TER, provider and fund size.
It ships with a web dashboard (Vue): asset overview with sortable columns, configuration view, exchange legend, 8 switchable themes, German/English UI, a price chart (intraday and real end-of-day closes) that docks at the bottom of the viewport, plus per-asset actions (refresh, delete, add ISIN) and links (extraETF, Yahoo Finance, JSON export).

What's new in 0.6.0
All eight ETF metrics are maintainable by hand — provider, replication, fund size, fund domicile and fund currency join TER, volatility and accumulating. Maintained in an expandable detail area per row, not in the table cell.

Stored metrics survive an outage. A failed justETF request no longer wipes what was already there — "asked and empty" is now distinguishable from "could not ask".
METADATA_TTL_DAYSfinally does what it always claimed: justETF is scraped once per cycle instead of on every quote.Merging duplicate instruments no longer loses data. Manual values and the daily-sync watermark move to the surviving row instead of being cascaded away.
/ready— a readiness probe that actually touches the database;/healthstays cheap. The Docker healthcheck now uses/ready.Stricter API contract — symbols and time ranges are validated (
422instead of a wrong result), and an unresolvable ISIN answers404instead of502.
Contents
- What can it do?
- How quotes are fetched
- Requirements
- Quick start
- Security model
- The REST API
- Configuration (.env)
- Dashboard
- Docker
- Unraid
- Tests
- Project layout
- License
What can it do?
- Fetch quotes — European and US stocks/ETFs, by ISIN or symbol.
- Cache — every response is stored in SQLite; repeated requests are served from the cache instead of hitting the internet again.
- Stay up to date automatically — a background job refreshes all known instruments at a configurable interval.
- History — two views: the intraday curve built from collected ticks, and real end-of-day closes (EOD from Yahoo, cached incrementally — each request only fetches the missing delta).
- Rich data — besides price and timestamp: name, currency, volume, volatility, and for ETFs TER, provider, replication method, fund size, fund domicile, fund currency and accumulating/distributing.
- Fill the gaps yourself — whatever justETF does not deliver can be entered by hand in the expandable detail area of a row. The source always wins: a manual value fills a gap, it never overwrites. It stays stored while the source covers it and applies again as soon as the source goes quiet.
- Dashboard — overview with column sorting, docked price chart, 8 themes, German/English UI, exchange legend, profile links, JSON export.
How quotes are fetched
The app combines three free data sources:
| Source | Used for |
|---|---|
| yfinance (Yahoo Finance) | price, currency, volume, name, EOD closes (basis of the computed volatility) — stocks & ETFs, EU & US |
| justETF | ETF extras: TER, provider, replication, fund size, 1-year volatility, distribution policy |
| OpenFIGI | resolves an ISIN to the listing at your preferred exchange (default: Xetra → EUR) |
A note on currency: the exchange suffix selects the exchange, not the currency — the currency always comes from the live quote. Example: the same ISIN trades on Xetra in EUR, in London often in pence (GBp). The app returns the currency verbatim.
Requirements
- Python 3.11+ (the app creates its own
.venv) - make (drives start/stop of the services)
- optional Docker (to run the container)
- optional Node.js 20+ (dashboard only)
The project uses shared ecosystem helpers under .libs/ (MakeLib, BashLib). For the
Makefile the environment variable DEV_MAKE must point to the MakeLib.
Quick start
# 1. Create the configuration
cp .env.example .env
# 2. Install dependencies (once)
python3.11 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt
# 3. Start the server
make dev # backend only, foreground (auto-reload)
# or
make start # backend only, background → make stop / make logs
The whole stack (backend and dashboard) at once — via overmind:
make dev-up # backend :8000 + dashboard :5173
make dev-down # stop both · make dev-logs for logs
The server then runs at http://localhost:8000. Interactive API docs (Swagger UI,
dark theme) live at http://localhost:8000/docs (use http:// in the browser, not
https://).
First query:
curl http://localhost:8000/quote/IE00B3RBWM25
make help lists all available commands, make hints shows useful URLs.
Security model
StockInfo has no authentication, and it is not meant to have one. Every endpoint is open to anyone who can reach the port — including the ones that change or destroy data:
DELETE /instruments/{isin}removes an instrument together with its entire price history.PUT /instruments/{symbol}/isinandPUT /instruments/{symbol}/overrideschange stored data.POST /refreshandGET /analyzetrigger live requests to Yahoo and justETF and write their results to the database.
The default bind address is 0.0.0.0, so in Docker the port is reachable from
the whole network the container is attached to.
Run it on a network you trust. In practice that means one of:
- bind it to loopback only (
HOST=127.0.0.1) and reach it through an SSH tunnel; - keep the published port inside your LAN and off the internet (the usual Unraid setup);
- or put an authenticating reverse proxy in front of it if it must be exposed.
CORS_ORIGINS is not a protection. It restricts what a browser on another
origin may do — it does nothing about curl, a script, or any server-to-server
call.
Two limits do exist, and they are about load rather than access: the global
refresh takes a non-blocking lock so two runs cannot overlap, and justETF is
only scraped once every METADATA_TTL_DAYS (an explicit single refresh still
forces it).
The REST API

| Method & path | Purpose |
|---|---|
GET /health |
liveness — answers as long as the process is alive |
GET /ready |
readiness — also checks the database; 503 when it is unreachable |
GET /quote/{isin} |
quote by ISIN (prefers Xetra/EUR) |
GET /quote?symbol=VGWL.DE |
quote by full Yahoo symbol (suffix = exchange) |
GET /quote/{isin}/history |
intraday history (collected ticks) |
GET /quote/{isin}/daily?period=1w|1m|3m|1y|max |
real end-of-day closes (EOD, cached) |
GET /instruments |
all cached instruments with their latest quote |
GET /env |
current configuration (secrets masked) |
POST /refresh · POST /refresh/{isin} |
refresh all / a single instrument |
PUT /instruments/by-symbol/{symbol}/isin |
add an ISIN after the fact |
DELETE /instruments/{isin} |
delete an instrument including its history |
For instruments without an ISIN there is a …/by-symbol/{symbol} variant of
each endpoint (quote, history, daily, refresh, delete).
Example response (GET /quote/IE00B3RBWM25):
{
"isin": "IE00B3RBWM25",
"symbol": "VGWL.DE",
"exchange": "Xetra",
"name": "Vanguard FTSE All-World UCITS ETF",
"type": "etf",
"currency": "EUR",
"price": 160.98,
"quote_time": "2026-07-10T15:35:46+00:00",
"volume": 14403,
"ter": 0.19,
"provider": "Vanguard",
"replication": "Physical(Optimized sampling)",
"fund_size": 22638.0,
"volatility": 9.95,
"accumulating": false,
"source": "yfinance+justetf",
"cached": false,
"stale": false,
"fetched_at": "2026-07-12T18:16:28+00:00"
}
volatility is the 1-year volatility in percent — from justETF for ETFs, otherwise
computed (annualized) from the cached EOD closes. accumulating states whether an
ETF accumulates (true) or distributes (false).
Fields that cannot be determined are null (e.g. ter for individual stocks). If a
live fetch fails but an old value exists in the cache, that value is returned with
"stale": true instead of an error. Unknown ISIN → 404.
Configuration (.env)
All values can be overridden via .env (cp .env.example .env):
| Variable | Meaning | Default |
|---|---|---|
HOST / PORT |
server address | 0.0.0.0 / 8000 |
DATABASE_PATH |
path of the SQLite file | data/stockinfo.db |
CACHE_TTL_HOURS |
age at which a quote is re-fetched on request | 6 |
REFRESH_INTERVAL_HOURS |
interval of the background refresh | 6 |
METADATA_TTL_DAYS |
refresh cadence for ETF metadata | 7 |
DEFAULT_EXCHANGE |
preferred exchange for ISIN queries (MIC) | XETR (Xetra) |
OPENFIGI_API_KEY |
optional key for a higher OpenFIGI rate limit | empty |
EXTRAETF_ETF_URL / EXTRAETF_STOCK_URL |
profile link templates (placeholder {isin}) |
extraetf.com/… |
YAHOO_URL |
Yahoo link template (placeholder {symbol}) |
de.finance.yahoo.com/… |
CORS_ORIGINS |
allowed dashboard origin(s) | http://localhost:5173 |
Dashboard
A standalone web frontend lives in dashboard/ (Vue 3 + Vite +
TypeScript + SCSS) with a fixed header (deep-linkable tab navigation, DE/EN
language switch — persisted, initial language follows the browser) and a status
bar (health traffic light green/orange/red + version):
- Assets — overview with the latest quote and sortable columns (click a
header: ascending → descending → off; persisted); add (ISIN/symbol), refresh,
delete, add ISIN; per row links to extraETF, Yahoo Finance and a
JSON popup (URL + result copyable — works on plain
http://too). - Chart — selecting a row docks the price history at the bottom of the
viewport (always visible, even with long asset lists). Range switch
1D · 1W · 1M · 3M · 1Y · Max:1D= intraday curve (collected ticks), the rest are real end-of-day closes (EOD). True time axis, compact ticks. - Exchanges — legend of the Yahoo suffixes (exchange, region, currency).
- Environment — current configuration incl. a note on the automatic refresh; Themes — 8 selectable, persisted themes.
cd dashboard
npm install
npm run dev # http://localhost:5173
The Vite dev proxy forwards API requests to the backend (http://localhost:8000)
automatically — VITE_API_BASE_URL is not needed in dev (optionally overridable,
see dashboard/vite.config.ts).
The backend must run in parallel. Both together: make dev-up (see Quick start).
Docker
Backend and dashboard run in a single image on one port. Built with
docker/build.sh (ecosystem convention, versioned via gitDockerTag):
make build # build the image (docker/build.sh --build)
make up # start the container → http://localhost:8000/
make down # stop & remove
make docker-logs # follow logs
FastAPI serves the dashboard itself (relative API calls) — no separate web server
required. The cache lives in the stockinfo-data volume (/data inside the
container). The container runs as a non-root user (UID 99 / GID 100 — Unraid's
nobody:users).
Push to a registry:
make push # docker/build.sh --push (TARGET=dockerhub, default)
TARGET=ghcr make push # alternatively GitHub Container Registry
Unraid
A ready-made container template lives in the dedicated CA repo
MikeMitterer/unraid-templates
— image mangolila/stockinfo:latest (Docker Hub), WebUI port 8000, path
/mnt/user/appdata/stockinfo → /data, plus the most important settings as
variables (refresh interval, TTLs, exchange, OpenFIGI key, timezone).
Install it as a user template (run on the Unraid box):
wget -O /boot/config/plugins/dockerMan/templates-user/my-stockinfo.xml https://raw.githubusercontent.com/MikeMitterer/unraid-templates/master/templates/stockinfo.xml
Then: Docker → Add Container → pick “stockinfo” under User templates. Support: GitHub Issues.
Tests
make test # backend (pytest)
cd dashboard && npm run test # dashboard (Vitest)
The tests run without network access — external data sources are mocked.
Project layout
app/ # FastAPI backend
main.py # app setup, routes, scheduler startup
config.py # configuration (.env)
db.py, repository.py # SQLite: schema and data access
resolver.py # ISIN → symbol/exchange (OpenFIGI + Yahoo)
providers/ # data sources: yfinance, justETF, OpenFIGI
services/ # quote fetching + cache/TTL logic
scheduler.py # periodic background refresh
routers/ # HTTP endpoints (quotes, dashboard)
docs.py # dark-themed Swagger UI (/docs)
dashboard/ # Vue dashboard (standalone app)
tests/ # backend tests (pytest)
docker/ # Dockerfile, build.sh (single-image build)
unraid/screenshots/ # dashboard, detail area + swagger (README + CA template)
Makefile # service start/stop (make help)
Technical details and design decisions: see
docs/superpowers/specs/.
License
MIT © Mike Mitterer
Media gallery
1 / 2Install Stockinfo on Unraid in a few clicks.
Find Stockinfo in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.
Categories
Download Statistics
Related apps
Explore more like this
Explore allDetails
mangolila/stockinfo:latestRuntime arguments
- Web UI
http://[IP]:[PORT:8000]/- Network
bridge- Shell
bash- Privileged
- false
Template configuration
Host port for dashboard + API (container listens internally on 8000).
- Target
- 8000
- Default
- 8000
- Value
- 8000
Persistent path for the SQLite cache DB (survives container updates).
- Target
- /data
- Default
- /mnt/user/appdata/stockinfo
- Value
- /mnt/user/appdata/stockinfo
Interval of the periodic background refresh of all instruments.
- Target
- REFRESH_INTERVAL_HOURS
- Default
- 6
- Value
- 6
Age at which a quote is re-fetched on request.
- Target
- CACHE_TTL_HOURS
- Default
- 6
- Value
- 6
Refresh cycle for ETF metadata (TER, provider …).
- Target
- METADATA_TTL_DAYS
- Default
- 7
- Value
- 7
Preferred exchange for ISIN lookups, e.g. XETR (Xetra).
- Target
- DEFAULT_EXCHANGE
- Default
- XETR
- Value
- XETR
Optional key for a higher OpenFIGI rate limit (empty = without).
- Target
- OPENFIGI_API_KEY
Container timezone for log timestamps. Default UTC — set your local zone, e.g. Europe/Vienna or America/New_York.
- Target
- TZ
- Default
- UTC
- Value
- UTC