354 lines
9.0 KiB
Markdown
354 lines
9.0 KiB
Markdown
# Portfolio Engine
|
||
|
||
A lightweight portfolio and financial data backend written in Go. Tracks companies, currencies, revenue reports, and trades across configurable time periods — replacing spreadsheets with a proper database, REST API, and interactive shell.
|
||
|
||
## Features
|
||
|
||
* SQLite database with foreign key enforcement
|
||
* REST API on port `8080`
|
||
* Interactive shell for managing data without an HTTP client
|
||
* Revenue tracking by period (quarterly, half-year, full year)
|
||
* Revenue broken down by custom categories (product, location, segment...)
|
||
* Trade tracking (stocks, options, currency trades)
|
||
* Health endpoint with DB latency, memory, and uptime stats
|
||
|
||
## Project Structure
|
||
|
||
```
|
||
.
|
||
├── main.go # Entry point — HTTP routes + shell loop
|
||
├── app.db # SQLite database (auto-created on first run)
|
||
├── build.sh # Build script
|
||
├── go.mod / go.sum
|
||
└── internal/
|
||
├── database/
|
||
│ └── main.go # Schema init (InitDB) — all CREATE TABLE statements
|
||
├── handlers/
|
||
│ ├── main.go # HealthHandler
|
||
│ ├── currency.go # AddCurrencyHandler, GetCurrenciesHandler
|
||
│ ├── revenue.go # AddRevenueEntryHandler, GetRevenueReportHandler, GetRevenueSumHandler
|
||
│ └── trade.go # AddTradeHandler, GetTradeListHandler
|
||
├── model/
|
||
│ ├── company.go # Company struct + SQL (InsertCompany, GetAllCompanies)
|
||
│ ├── currency.go # Currency struct + SQL (InsertCurrency, GetAllCurrencies)
|
||
│ ├── periode.go # Period struct + helpers (QuarterPeriod, HalfYearPeriod, FullYearPeriod)
|
||
│ ├── revenue.go # Revenue, RevenueReport, RevSum structs + SQL
|
||
│ └── trade.go # Trade struct, TradeType, TradeProduct enums + SQL
|
||
├── service/
|
||
│ ├── main.go # Service wiring
|
||
│ ├── company.go # Company business logic
|
||
│ ├── currency.go # Currency business logic
|
||
│ └── revenue.go # Revenue aggregation logic
|
||
└── shell/
|
||
├── company.go # add-company, list-companies
|
||
├── currency.go # add-currency, list-currency
|
||
└── revenue.go # add-revenue, list-revenue, sum-revenue
|
||
```
|
||
|
||
## Getting Started
|
||
|
||
### Prerequisites
|
||
|
||
* Go 1.21+
|
||
* GCC (required for SQLite CGO bindings)
|
||
+ Ubuntu/Debian: `sudo apt install gcc`
|
||
+ macOS: comes with Xcode Command Line Tools
|
||
|
||
### Install & Run
|
||
|
||
```
|
||
git clone git@git.samantha42.xyz:samantha/Portifolio-Engine.git
|
||
cd Portifolio-Engine
|
||
go mod download
|
||
go run main.go
|
||
```
|
||
|
||
Or build first:
|
||
|
||
```
|
||
chmod +x build.sh
|
||
./build.sh
|
||
./Portifolio
|
||
```
|
||
|
||
On startup:
|
||
|
||
```
|
||
Connected to SQLite database
|
||
Tables ready
|
||
Server running on :8080
|
||
|
||
Shell ready. Type 'help' for commands.
|
||
>
|
||
```
|
||
|
||
The HTTP server and shell run concurrently — the API is live while you type shell commands.
|
||
|
||
---
|
||
|
||
## REST API
|
||
|
||
### `GET /health`
|
||
|
||
Returns server status, DB connection info, memory usage, and uptime.
|
||
|
||
```
|
||
curl http://localhost:8080/health
|
||
```
|
||
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"uptime": "4m32s",
|
||
"database": {
|
||
"status": "ok",
|
||
"latency": "121µs",
|
||
"open_connections": 1,
|
||
"in_use": 0,
|
||
"idle": 1
|
||
},
|
||
"memory": {
|
||
"alloc_mb": 2.31,
|
||
"sys_mb": 9.44,
|
||
"num_gc": 3
|
||
},
|
||
"go_version": "go1.22.0",
|
||
"goroutines": 4
|
||
}
|
||
```
|
||
|
||
Returns `503` with `"status": "degraded"` if the database is unreachable.
|
||
|
||
---
|
||
|
||
### `POST /add/company`
|
||
|
||
```
|
||
curl -X POST http://localhost:8080/add/company \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"name":"Novo Nordisk","shares_outstanding":4442064180,"price":251.00,"currency_id":1}'
|
||
```
|
||
|
||
---
|
||
|
||
### `GET /companies`
|
||
|
||
```
|
||
curl http://localhost:8080/companies
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /add/currency`
|
||
|
||
```
|
||
curl -X POST http://localhost:8080/add/currency \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"code":"DKK","name":"Danish Krone"}'
|
||
```
|
||
|
||
---
|
||
|
||
### `GET /currencies`
|
||
|
||
```
|
||
curl http://localhost:8080/currencies
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /trade/add`
|
||
|
||
Add a new trade. `product` and `type` are integer enums (see tables below).
|
||
|
||
```
|
||
curl -X POST http://localhost:8080/trade/add \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"ticker_id": 1,
|
||
"currency": "DKK",
|
||
"shares": 100,
|
||
"product": 0,
|
||
"type": false,
|
||
"price": 251.00,
|
||
"date": "2025-03-01T00:00:00Z"
|
||
}'
|
||
```
|
||
|
||
**`product` values:**
|
||
|
||
| Value | Meaning |
|
||
|-------|------------------|
|
||
| `0` | StockTrade |
|
||
| `1` | OptionTradeCall |
|
||
| `2` | OptionTradePut |
|
||
| `3` | CurrencyTrade |
|
||
|
||
**`type` values:**
|
||
|
||
| Value | Meaning |
|
||
|---------|---------|
|
||
| `false` | Buy |
|
||
| `true` | Sell |
|
||
|
||
---
|
||
|
||
### `GET /trade/list`
|
||
|
||
Returns all trades as a JSON array.
|
||
|
||
```
|
||
curl http://localhost:8080/trade/list
|
||
```
|
||
|
||
```json
|
||
[
|
||
{
|
||
"Ticker": { "id": 1, "name": "Novo Nordisk", ... },
|
||
"Shares": 100,
|
||
"Product": 0,
|
||
"Type": false,
|
||
"Price": 251.00,
|
||
"Currency": { "id": 1, "code": "DKK", "name": "Danish Krone" },
|
||
"Date": "2025-03-01T00:00:00Z"
|
||
}
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /add/revenue/entry`
|
||
|
||
Add a single revenue line to a report. The period is created automatically if it doesn't exist.
|
||
|
||
```
|
||
curl -X POST http://localhost:8080/add/revenue/entry \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"company_id": 1,
|
||
"currency_id": 1,
|
||
"period_type": "Q",
|
||
"year": 2025,
|
||
"index": 1,
|
||
"category": "product",
|
||
"label": "iPhone",
|
||
"value": 69143
|
||
}'
|
||
```
|
||
|
||
`period_type` is one of:
|
||
|
||
| Value | Meaning | `index` range |
|
||
|-------|-----------|---------------|
|
||
| `Q` | Quarter | 1–4 |
|
||
| `H` | Half-year | 1–2 |
|
||
| `Y` | Full year | 1 |
|
||
|
||
---
|
||
|
||
### `GET /revenue/report`
|
||
|
||
Get all revenue entries for a company and period.
|
||
|
||
```
|
||
curl "http://localhost:8080/revenue/report?company_id=1&period_type=Q&year=2025&index=1"
|
||
```
|
||
|
||
---
|
||
|
||
### `GET /revenue/sum`
|
||
|
||
Sum all revenue entries for a company across all periods of a given type in a year.
|
||
|
||
```
|
||
curl "http://localhost:8080/revenue/sum?company_id=1&period_type=Q&year=2025"
|
||
```
|
||
|
||
---
|
||
|
||
## Shell Commands
|
||
|
||
| Command | Description |
|
||
|------------------|--------------------------------------------------|
|
||
| `add-company` | Add a new company interactively |
|
||
| `list-companies` | List all companies with price and share count |
|
||
| `add-currency` | Add a new currency interactively |
|
||
| `list-currency` | List all currencies with their IDs |
|
||
| `add-revenue` | Add a revenue entry interactively |
|
||
| `list-revenue` | List revenue entries for a company/period |
|
||
| `sum-revenue` | Sum revenue across all periods in a year |
|
||
| `help` | Show all available commands |
|
||
| `exit` | Quit |
|
||
|
||
---
|
||
|
||
## Database Schema
|
||
|
||
```sql
|
||
CREATE TABLE currencies (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
code TEXT NOT NULL UNIQUE,
|
||
name TEXT NOT NULL
|
||
);
|
||
|
||
CREATE TABLE companies (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL UNIQUE,
|
||
shares_outstanding INTEGER NOT NULL,
|
||
price REAL NOT NULL,
|
||
currency_id INTEGER NOT NULL,
|
||
FOREIGN KEY (currency_id) REFERENCES currencies(id)
|
||
);
|
||
|
||
CREATE TABLE periods (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
type TEXT NOT NULL CHECK(type IN ('Q', 'H', 'Y')),
|
||
year INTEGER NOT NULL,
|
||
idx INTEGER NOT NULL,
|
||
start_date TEXT NOT NULL,
|
||
end_date TEXT NOT NULL,
|
||
UNIQUE(type, year, idx)
|
||
);
|
||
|
||
CREATE TABLE revenue_reports (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
company_id INTEGER NOT NULL,
|
||
period_id INTEGER NOT NULL,
|
||
FOREIGN KEY (company_id) REFERENCES companies(id),
|
||
FOREIGN KEY (period_id) REFERENCES periods(id),
|
||
UNIQUE(company_id, period_id)
|
||
);
|
||
|
||
CREATE TABLE revenue_entries (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
report_id INTEGER NOT NULL,
|
||
currency_id INTEGER NOT NULL,
|
||
category TEXT NOT NULL,
|
||
label TEXT NOT NULL,
|
||
value REAL NOT NULL,
|
||
FOREIGN KEY (report_id) REFERENCES revenue_reports(id),
|
||
FOREIGN KEY (currency_id) REFERENCES currencies(id)
|
||
);
|
||
|
||
CREATE TABLE trades (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
company_id INTEGER NOT NULL,
|
||
currency_id INTEGER NOT NULL,
|
||
shares INTEGER NOT NULL,
|
||
product INTEGER NOT NULL CHECK(product IN (0, 1, 2, 3)),
|
||
type INTEGER NOT NULL CHECK(type IN (0, 1)),
|
||
price REAL NOT NULL,
|
||
traded_at DATETIME NOT NULL
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## Roadmap
|
||
|
||
* `GET /company/{id}/revenue` — full revenue history for a company
|
||
* `GET /trade/list?ticker_id=1` — filter trades by company
|
||
* Price update endpoint
|
||
* Market cap calculation (price × shares outstanding)
|
||
* Multi-currency conversion
|
||
* Frontend dashboard |