A Go-based application for tracking and analyzing financial instruments (ETFs and stocks) using ISIN codes. The application provides real-time price updates and portfolio management through a REST API.
- 🔍 ISIN Lookup: Search for financial instruments by ISIN code
- 💰 Portfolio Management: Add, remove, and track multiple positions
- 📦 Batch Operations: Add multiple positions in a single request with partial failure handling
- 📊 Real-time Updates: Automatic price refresh at configurable intervals
- 📈 P/L Tracking: Calculate profit/loss for individual positions and entire portfolio
- 📉 Visual Dashboard: Portfolio overview with sparkline charts and allocation breakdowns
- 🌐 REST API: HTTP endpoints for easy integration
- 🏗️ Clean Architecture: Domain-driven design with clear separation of concerns
- 🐳 Docker Ready: Full stack containerization with PostgreSQL
The project follows Clean Architecture (DDD) principles:
stock-tracker/
├── cmd/tracker/ # Application entry point
├── internal/
│ ├── domain/ # Pure Business entities and logic
│ ├── application/ # Use cases and orchestration
│ ├── infrastructure/ # Adapter Implementations (PostgreSQL, Market Data)
│ │ ├── marketdata/ # Market data providers (YFinance)
│ │ ├── persistence/ # SQL Repositories (PostgreSQL, Oracle)
│ │ └── config/ # Configuration loading
│ └── interfaces/ # HTTP Ports (Gin Handlers)
└── docker-compose.yml # Infrastructure orchestration
- Go 1.22+
- Docker & Docker Compose (Recommended for full stack)
- PostgreSQL 15+ (Or use the Docker container provided)
- YFinance Market Data Service - Self-hosted Python microservice (no API key required, supports batch)
- Automatic Position Merging: If you add a position for an Instrument (ISIN) that is already in your portfolio, the system will automatically merge it:
Invested Amount: Summed with existing amount.Quantity: Summed with existing quantity.Current Price: Updated to the latest market price.- No Duplicates: A portfolio cannot have two separate entries for the same ISIN.
- Automatic Capture: Price data is captured every 60 seconds into the
price_historytable - Sparkline Data: Historical prices are used to generate sparkline charts for the dashboard endpoint
- Data Retention: Old price history is automatically cleaned up after 90 days
- Sector Information: Instruments include a sector field (may be empty/"N/A" if not provided by market data API)
- Clone the repository:
git clone https://github.com/jmanzanog/stock-tracker.git
cd stock-tracker- Install dependencies:
go mod download- Create a
.envfile from the example:
cp .env.example .env- Edit
.envand add your keys:
# Market Data Provider (only yfinance is supported)
MARKET_DATA_PROVIDER=yfinance
# YFinance Service URL (required for yfinance provider)
YFINANCE_BASE_URL=http://localhost:8000
# Database config is pre-set for local docker devThis starts both the PostgreSQL database and the Application in containers.
docker compose --profile deployment up --build- App URL:
http://localhost:8080 - Database: Persisted in
./postgres_datavolume.
Ideal for development and debugging requiring database.
- Start only the database:
docker compose up -d
- Run the application locally:
go run cmd/tracker/main.go
Note: Ensure your local .env has DB_HOST=localhost for this mode.
Requires a local PostgreSQL instance running.
export DB_DSN="host=localhost user=postgres password=... dbname=stocktracker"
go run cmd/tracker/main.goPOST /api/v1/positions
Content-Type: application/json
{
"isin": "US0378331005",
"invested_amount": "10000",
"currency": "USD"
}Add multiple positions in a single request. The API uses batch operations for better performance.
POST /api/v1/positions/batch
Content-Type: application/json
[
{"isin": "US0378331005", "invested_amount": "10000", "currency": "USD"},
{"isin": "IE00B4L5Y983", "invested_amount": "5000", "currency": "EUR"},
{"isin": "US5949181045", "invested_amount": "8000", "currency": "USD"}
]Response (HTTP 201 for success, 207 for partial success):
{
"successful": [
{"isin": "US0378331005", "position": {...}},
{"isin": "US5949181045", "position": {...}}
],
"failed": [
{"isin": "IE00B4L5Y983", "error": "instrument not found"}
]
}GET /api/v1/positionsGET /api/v1/portfolioReturns portfolio data formatted for visualization with sparkline charts and allocation breakdowns.
GET /api/v1/dashboard?sparklines=7,30,90Query Parameters:
sparklines: Comma-separated list of day ranges for sparkline data (default:7,30,90, max: 365 days per range, max 5 ranges)
Response:
{
"portfolio_id": "default",
"generated_at": "2024-01-15T10:30:00Z",
"total_value": "45000.00",
"total_invested": "30000.00",
"total_pnl": "15000.00",
"pnl_percent": "50.00",
"by_currency": [
{"currency": "USD", "total_value": "30000.00", "percent": "66.67"},
{"currency": "EUR", "total_value": "15000.00", "percent": "33.33"}
],
"by_type": [
{"type": "stock", "total_value": "25000.00", "percent": "55.56"},
{"type": "etf", "total_value": "20000.00", "percent": "44.44"}
],
"by_sector": [
{"sector": "Technology", "total_value": "20000.00", "percent": "44.44"},
{"sector": "Financial", "total_value": "15000.00", "percent": "33.33"},
{"sector": "N/A", "total_value": "10000.00", "percent": "22.22"}
],
"positions": [
{
"id": "pos-1",
"isin": "US0378331005",
"symbol": "AAPL",
"name": "Apple Inc.",
"type": "stock",
"sector": "Technology",
"quantity": "100.00",
"current_price": "150.00",
"current_value": "15000.00",
"invested_amount": "10000.00",
"pnl": "5000.00",
"pnl_percent": "50.00",
"currency": "USD",
"sparklines": {
"7d": [{"date": "2024-01-15T10:00:00Z", "price": "148.50"}, ...],
"30d": [...],
"90d": [...]
}
}
]
}Notes:
- Sparklines show price history captured every 60 seconds
- Price history is retained for 90 days and automatically cleaned up
- Sector field may be "N/A" if the instrument provider doesn't supply sector information
The Go app also serves a lightweight browser dashboard on the root route.
- Start the application.
- Open
http://localhost:8080/in your browser. - The page fetches data from
GET /api/v1/dashboardand renders summary cards, allocation breakdowns, positions, and mini trend sparklines. The page requests 7-day sparklines by default to keep the payload small; this can be changed by editing thesparklinesURL parameter in the page's JavaScript if needed.
Environment variables (see .env.example):
| Variable | Description | Default |
|---|---|---|
MARKET_DATA_PROVIDER |
Market data provider (yfinance) |
yfinance |
YFINANCE_BASE_URL |
URL for yfinance microservice | http://localhost:8000 |
SERVER_PORT |
HTTP server port | 8080 |
SERVER_HOST |
HTTP server host | localhost |
PRICE_REFRESH_INTERVAL |
Auto-refresh interval | 60s |
LOG_LEVEL |
Logging level | info |
DB_DRIVER |
Database Driver | postgres |
DB_DSN |
Connection String | required |
The YFinance provider uses a self-hosted Python microservice that wraps the yfinance library. This is ideal for:
- No API key required: No registration needed
- Global coverage: Supports US, UK, EU, and Asian markets
- Self-hosted: Full control over the service and data
- Clone or deploy the Market Data Service:
# Clone the market-data-service repository
cd market-data-service
docker compose up --build- Configure StockTracker to use it:
MARKET_DATA_PROVIDER=yfinance
YFINANCE_BASE_URL=http://localhost:8000Example K8s deployment for the Market Data Service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: market-data-service
spec:
replicas: 2
selector:
matchLabels:
app: market-data-service
template:
metadata:
labels:
app: market-data-service
spec:
containers:
- name: market-data-service
image: ghcr.io/your-username/market-data-service:latest
ports:
- containerPort: 8000
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
resources:
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: market-data-service
spec:
selector:
app: market-data-service
ports:
- port: 8000
targetPort: 8000
type: ClusterIPThen configure StockTracker:
MARKET_DATA_PROVIDER=yfinance
YFINANCE_BASE_URL=http://market-data-service:8000Note: Integration tests utilize Testcontainers, so you must have Docker installed and running on your machine to execute them successfully.
The project includes comprehensive test coverage with optimized reusable containers for both PostgreSQL and Oracle backends.
Tests use shared containers that start only once per test run:
- PostgreSQL: ~5 seconds startup, ~5ms per test
- Oracle: ~25 seconds startup, ~10ms per test
- Total for all tests: ~35 seconds (vs ~30+ minutes without optimization)
go test ./...- ✅ Runs all tests against both PostgreSQL and Oracle
- ⚡ Fast execution (~35 seconds total)
- 🔄 Containers are started once and reused across all tests
Generate coverage report:
go test -v ./... -race -coverprofile=coverage.txt -covermode=atomicTo ensure your changes pass the CI checks before pushing, you can use the provided verification scripts. These scripts run go mod tidy, go fmt, golangci-lint, and all unit tests.
.\scripts\verify.ps1chmod +x scripts/verify.sh
./scripts/verify.shMIT