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
- 🌐 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 (TwelveData, Finnhub, 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)
- Market Data API Key (one of the following):
- Twelve Data API Key - Default provider (8 credits/min free tier)
- Finnhub API Key - Alternative provider (60 req/min free tier)
- YFinance Market Data Service - Self-hosted Python microservice (no API key required, supports batch)
| Provider | Batch API | Rate Limits (Free) | Notes |
|---|---|---|---|
| TwelveData | ✅ Yes | 8 credits/min, 800/day | Each symbol = 1 credit |
| Finnhub | ❌ No | 60 req/min, 30 req/s | Uses concurrent fallback |
| YFinance | ✅ Yes | Self-hosted (no limit) | Best for batch operations |
- 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.
- 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: "twelvedata" (default), "finnhub", or "yfinance"
MARKET_DATA_PROVIDER=twelvedata
# TwelveData API Key (required if MARKET_DATA_PROVIDER=twelvedata)
TWELVE_DATA_API_KEY=your_key
# Finnhub API Key (required if MARKET_DATA_PROVIDER=finnhub)
# FINNHUB_API_KEY=your_key
# YFinance Service URL (required if MARKET_DATA_PROVIDER=yfinance)
# 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 when supported by the market data provider (YFinance), or falls back to concurrent processing (Finnhub/TwelveData).
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/portfolioEnvironment variables (see .env.example):
| Variable | Description | Default |
|---|---|---|
MARKET_DATA_PROVIDER |
Market data provider (twelvedata, finnhub, or yfinance) |
twelvedata |
TWELVE_DATA_API_KEY |
API key for Twelve Data (required if provider is twelvedata) | - |
FINNHUB_API_KEY |
API key for Finnhub (required if provider is finnhub) | - |
YFINANCE_BASE_URL |
URL for yfinance microservice (required if provider is yfinance) | 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: Unlike TwelveData or Finnhub, 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