new readme
This commit is contained in:
266
README.md
266
README.md
@@ -1,36 +1,45 @@
|
|||||||
# Portfolio Engine
|
# Portfolio Engine
|
||||||
|
|
||||||
A lightweight portfolio tracking backend written in Go. Replaces spreadsheets with a proper database, REST API, and an interactive shell — all in a single binary.
|
A lightweight portfolio and financial data backend written in Go. Tracks companies, currencies, and revenue reports across configurable time periods — replacing spreadsheets with a proper database, REST API, and interactive shell.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- SQLite database with foreign key enforcement
|
- SQLite database with foreign key enforcement
|
||||||
- REST API on port `8080`
|
- REST API on port `8080`
|
||||||
- Interactive shell for managing data without an HTTP client
|
- Interactive shell for managing data without an HTTP client
|
||||||
- Tracks companies with share count, price, and currency
|
- Revenue tracking by period (quarterly, half-year, full year)
|
||||||
|
- Revenue broken down by custom categories (product, location, segment...)
|
||||||
|
- Health endpoint with DB latency, memory, and uptime stats
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── main.go # Entry point, HTTP routes, shell loop
|
├── main.go # Entry point — HTTP routes + shell loop
|
||||||
├── app.db # SQLite database (auto-created on first run)
|
├── app.db # SQLite database (auto-created on first run)
|
||||||
├── build.sh # Build script
|
├── build.sh # Build script
|
||||||
├── go.mod / go.sum
|
├── go.mod / go.sum
|
||||||
└── internal/
|
└── internal/
|
||||||
├── database/
|
├── database/
|
||||||
│ └── main.go # Schema init (InitDB)
|
│ └── main.go # Schema init (InitDB) — all CREATE TABLE statements
|
||||||
├── handlers/
|
├── handlers/
|
||||||
│ └── main.go # HTTP handlers (HealthHandler, AddCompanyHandler)
|
│ ├── main.go # HealthHandler
|
||||||
|
│ ├── currency.go # AddCurrencyHandler, GetCurrenciesHandler
|
||||||
|
│ └── revenue.go # AddRevenueEntryHandler, GetRevenueReportHandler, GetRevenueSumHandler
|
||||||
├── model/
|
├── model/
|
||||||
│ └── company.go # Structs + SQL queries (Company, Currency, CompanyInput)
|
│ ├── 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
|
||||||
├── service/
|
├── service/
|
||||||
│ ├── company.go # Company business logic
|
│ ├── main.go # Service wiring
|
||||||
│ ├── currency.go # Currency business logic
|
│ ├── company.go # Company business logic
|
||||||
│ └── main.go # Service setup
|
│ ├── currency.go # Currency business logic
|
||||||
|
│ └── revenue.go # Revenue aggregation logic
|
||||||
└── shell/
|
└── shell/
|
||||||
├── company.go # Shell commands for companies
|
├── company.go # add-company, list-companies
|
||||||
└── currency.go # Shell commands for currencies
|
├── currency.go # add-currency, list-currency
|
||||||
|
└── revenue.go # add-revenue, list-revenue, sum-revenue
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
@@ -51,7 +60,7 @@ go mod download
|
|||||||
go run main.go
|
go run main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use the build script:
|
Or build first:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x build.sh
|
chmod +x build.sh
|
||||||
@@ -59,23 +68,26 @@ chmod +x build.sh
|
|||||||
./Portifolio
|
./Portifolio
|
||||||
```
|
```
|
||||||
|
|
||||||
On startup you'll see:
|
On startup:
|
||||||
|
|
||||||
```
|
```
|
||||||
Connected to SQLite database
|
Connected to SQLite database
|
||||||
|
Tables ready
|
||||||
Server running on :8080
|
Server running on :8080
|
||||||
|
|
||||||
Shell ready. Commands: add-company, help, exit
|
Shell ready. Type 'help' for commands.
|
||||||
>
|
>
|
||||||
```
|
```
|
||||||
|
|
||||||
The HTTP server and the shell run concurrently — the API is live while you type shell commands.
|
The HTTP server and shell run concurrently — the API is live while you type shell commands.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## REST API
|
## REST API
|
||||||
|
|
||||||
### `GET /health`
|
### `GET /health`
|
||||||
|
|
||||||
Returns the status of the server and database connection.
|
Returns server status, DB connection info, memory usage, and uptime.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:8080/health
|
curl http://localhost:8080/health
|
||||||
@@ -84,44 +96,126 @@ curl http://localhost:8080/health
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"database": "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` if the database is unreachable.
|
Returns `503` with `"status": "degraded"` if the database is unreachable.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `POST /add/company`
|
### `POST /add/company`
|
||||||
|
|
||||||
Add a new company. The `currency_id` must reference an existing currency in the database.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:8080/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`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/companies
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `POST /add/currency`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/add/currency \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"code":"DKK","name":"Danish Krone"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `GET /currencies`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/currencies
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `POST /add/revenue/entry`
|
||||||
|
|
||||||
|
Add a single revenue line to a report. The period is created automatically if it doesn't exist.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/add/revenue/entry \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"name": "Novo Nordisk",
|
"company_id": 1,
|
||||||
"shares_outstanding": 4442064180,
|
"currency_id": 1,
|
||||||
"price": 251.00,
|
"period_type": "Q",
|
||||||
"currency_id": 1
|
"year": 2025,
|
||||||
|
"index": 1,
|
||||||
|
"category": "product",
|
||||||
|
"label": "iPhone",
|
||||||
|
"value": 69143
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
`period_type` is one of:
|
||||||
{ "status": "created" }
|
|
||||||
|
| 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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8080/revenue/sum?company_id=1&period_type=Q&year=2025"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Shell Commands
|
## Shell Commands
|
||||||
|
|
||||||
The shell starts automatically after the server and accepts commands interactively.
|
| Command | Description |
|
||||||
|
|------------------|-----------------------------------------------|
|
||||||
| 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 |
|
| `add-currency` | Add a new currency interactively |
|
||||||
| `list-currency` | List all currencies with their IDs |
|
| `list-currency` | List all currencies with their IDs |
|
||||||
| `add-company` | Add a new company interactively |
|
| `add-revenue` | Add a revenue entry interactively |
|
||||||
| `help` | Show available commands |
|
| `list-revenue` | List revenue entries for a company/period |
|
||||||
| `exit` | Quit the application |
|
| `sum-revenue` | Sum revenue across all periods in a year |
|
||||||
|
| `help` | Show all available commands |
|
||||||
|
| `exit` | Quit |
|
||||||
|
|
||||||
### Example session
|
### Example session
|
||||||
|
|
||||||
@@ -131,19 +225,46 @@ The shell starts automatically after the server and accepts commands interactive
|
|||||||
Name (e.g. Danish Krone): Danish Krone
|
Name (e.g. Danish Krone): Danish Krone
|
||||||
✓ Currency 'Danish Krone' (DKK) added with ID 1
|
✓ Currency 'Danish Krone' (DKK) added with ID 1
|
||||||
|
|
||||||
> list-currency
|
|
||||||
ID CODE NAME
|
|
||||||
------------------------------
|
|
||||||
1 DKK Danish Krone
|
|
||||||
|
|
||||||
> add-company
|
> add-company
|
||||||
Name: Maersk
|
Name: Novo Nordisk
|
||||||
Shares outstanding: 15224309
|
Shares outstanding: 4442064180
|
||||||
Price: 15270.00
|
Price: 251.00
|
||||||
Currency ID: 1
|
Currency ID: 1
|
||||||
✓ Company 'Maersk' added.
|
✓ Company 'Novo Nordisk' added.
|
||||||
|
|
||||||
|
> add-revenue
|
||||||
|
Company ID: 1
|
||||||
|
Currency ID: 1
|
||||||
|
Period type (Q/H/Y): Q
|
||||||
|
Year: 2025
|
||||||
|
Index (Q: 1-4 | H: 1-2 | Y: 1): 1
|
||||||
|
Category (product/location/total): product
|
||||||
|
Label (e.g. iPhone, Americas): Diabetes Care
|
||||||
|
Value: 54200
|
||||||
|
✓ Revenue entry added: product / Diabetes Care = 54200.00 (Q1 2025)
|
||||||
|
|
||||||
|
> sum-revenue
|
||||||
|
Company ID: 1
|
||||||
|
Period type to sum (Q/H/Y): Q
|
||||||
|
Year: 2025
|
||||||
|
|
||||||
|
Revenue Sum — FY2025
|
||||||
|
Total: 54200.00
|
||||||
|
|
||||||
|
By Category:
|
||||||
|
product 54200.00
|
||||||
|
|
||||||
|
By Label:
|
||||||
|
Diabetes Care 54200.00
|
||||||
|
|
||||||
|
> list-companies
|
||||||
|
ID NAME CURRENCY PRICE SHARES
|
||||||
|
------------------------------------------------------------
|
||||||
|
1 Novo Nordisk DKK 251.00 4442064180
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Database Schema
|
## Database Schema
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
@@ -161,13 +282,60 @@ CREATE TABLE companies (
|
|||||||
currency_id INTEGER NOT NULL,
|
currency_id INTEGER NOT NULL,
|
||||||
FOREIGN KEY (currency_id) REFERENCES currencies(id)
|
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 category_defs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
company_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id),
|
||||||
|
UNIQUE(company_id, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE category_labels (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
category_def_id INTEGER NOT NULL,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (category_def_id) REFERENCES category_defs(id),
|
||||||
|
UNIQUE(category_def_id, label)
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- `GET /companies` — list all companies
|
- `GET /company/{id}/revenue` — full revenue history for a company
|
||||||
- `GET /currencies` — list all currencies
|
|
||||||
- Price update endpoint
|
- Price update endpoint
|
||||||
- Market cap calculation (price × shares)
|
- Market cap calculation (price × shares outstanding)
|
||||||
- Multi-currency conversion
|
- Multi-currency conversion
|
||||||
- Frontend dashboard
|
- Frontend dashboard
|
||||||
Reference in New Issue
Block a user