@pi-stef/finance-api 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +305 -45
- package/docker/Dockerfile +7 -0
- package/docker/README.md +122 -2
- package/docker/docker-compose.yml +7 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# @pi-stef/finance-api
|
|
2
2
|
|
|
3
|
-
Always-on local service for financial data ingestion, storage, and deterministic quant analysis.
|
|
3
|
+
Always-on local service for financial data ingestion, storage, and deterministic quant analysis. Backed by SQLite; serves a bearer-token-authenticated HTTP API to the `@pi-stef/finance` extension and any other client.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
### Docker (recommended)
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
12
|
cd packages/finance-api/docker
|
|
11
|
-
docker compose up
|
|
13
|
+
docker compose up -d
|
|
12
14
|
```
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
Pulls `ghcr.io/sfiorini/pi-stef/finance-api:latest` and starts the service at `http://127.0.0.1:7780`. See the [Docker guide](docker/README.md) for image tags, volumes, and retrieving the token.
|
|
15
17
|
|
|
16
18
|
### Native
|
|
17
19
|
|
|
@@ -22,20 +24,53 @@ pnpm serve
|
|
|
22
24
|
|
|
23
25
|
See [docs/native-run.md](docs/native-run.md) for launchd/systemd setup.
|
|
24
26
|
|
|
27
|
+
### Verify
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
curl http://127.0.0.1:7780/v1/health
|
|
31
|
+
# {"ok":true,"data":{"status":"ok","uptimeS":0}}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Authentication
|
|
37
|
+
|
|
38
|
+
All endpoints except `/v1/health` require a bearer token via the `Authorization` header:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Authorization: Bearer <token>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Token lifecycle:**
|
|
45
|
+
|
|
46
|
+
- On first start, the service generates a random UUID token and writes it to `~/.pi/sf/finance/token` (`chmod 600`), created atomically and race-safe via `O_EXCL`.
|
|
47
|
+
- The token is stable across restarts as long as the token file persists.
|
|
48
|
+
- In Docker, the token is stored inside the container at `/root/.pi/sf/finance/token` and persists via the `finance-config` volume. Retrieve it with:
|
|
49
|
+
```bash
|
|
50
|
+
docker compose exec finance-api cat /root/.pi/sf/finance/token
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Override:** Set `SF_FINANCE_TOKEN` to pin a specific token (useful for CI or sharing across hosts).
|
|
54
|
+
|
|
55
|
+
The `@pi-stef/finance` extension reads this token automatically when co-located on the same host.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
25
59
|
## Configuration
|
|
26
60
|
|
|
27
|
-
|
|
61
|
+
All configuration is via environment variables (prefix `SF_FINANCE_`):
|
|
28
62
|
|
|
29
63
|
| Variable | Default | Description |
|
|
30
64
|
|----------|---------|-------------|
|
|
31
|
-
| `SF_FINANCE_HOST` | `127.0.0.1`
|
|
65
|
+
| `SF_FINANCE_HOST` | `127.0.0.1` (`0.0.0.0` in Docker) | Server bind host |
|
|
32
66
|
| `SF_FINANCE_PORT` | `7780` | Server port |
|
|
33
|
-
| `SF_FINANCE_DB` | `~/.pi/sf/finance/finance.db` | SQLite database path |
|
|
34
|
-
| `
|
|
67
|
+
| `SF_FINANCE_DB` | `~/.pi/sf/finance/finance.db` (`/data/finance.db` in Docker) | SQLite database path |
|
|
68
|
+
| `SF_FINANCE_TOKEN` | (auto-generated) | Bearer token (overrides the token file) |
|
|
69
|
+
| `SF_FINANCE_DATA_FEED` | `stooq` | Price data feed (`stooq`) |
|
|
35
70
|
|
|
36
|
-
|
|
71
|
+
### Secrets (`secrets.json`)
|
|
37
72
|
|
|
38
|
-
Create `~/.pi/sf/finance/secrets.json` with provider credentials
|
|
73
|
+
Create `~/.pi/sf/finance/secrets.json` with provider credentials. The file is `chmod 600` on creation.
|
|
39
74
|
|
|
40
75
|
```json
|
|
41
76
|
{
|
|
@@ -52,55 +87,280 @@ Create `~/.pi/sf/finance/secrets.json` with provider credentials:
|
|
|
52
87
|
}
|
|
53
88
|
```
|
|
54
89
|
|
|
55
|
-
|
|
90
|
+
Each provider's required credentials are documented under [Providers](#providers).
|
|
91
|
+
|
|
92
|
+
---
|
|
56
93
|
|
|
57
94
|
## Providers
|
|
58
95
|
|
|
59
|
-
| Provider | Kind | Auth | Status |
|
|
60
|
-
|
|
96
|
+
| Provider | Kind | Auth (in `secrets.json`) | Status |
|
|
97
|
+
|----------|------|--------------------------|--------|
|
|
61
98
|
| File Import (CSV/OFX) | brokerage/banking | `filePath` | ✅ Working |
|
|
62
99
|
| Coinbase | crypto | `keyName` + `privateKey` | ⚠️ Stub (HMAC not implemented) |
|
|
63
100
|
| SnapTrade | brokerage | `clientId` + `consumerKey` | ⚠️ Stub |
|
|
64
101
|
| SimpleFIN | banking | `accessKey` | ⚠️ Stub |
|
|
65
102
|
| Teller | banking | `token` | ⚠️ Stub |
|
|
66
103
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
**Provider setup:**
|
|
105
|
+
|
|
106
|
+
- **File Import (CSV)** — Export positions from your brokerage (e.g. Fidelity's Positions download) and point `filePath` at the CSV. Supported columns: symbol, quantity, last price. Call `POST /v1/import` with the path to ingest.
|
|
107
|
+
- **File Import (OFX)** — Export transactions from your bank in OFX format and point `filePath` at it.
|
|
108
|
+
- **Coinbase / SnapTrade / SimpleFIN / Teller** — Stubs in the current release. Credentials are accepted and validated against the contract, but live API calls are not yet implemented. Tracked for a future release.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## HTTP API reference
|
|
113
|
+
|
|
114
|
+
Base URL: `http://127.0.0.1:7780`. All endpoints return `{ "ok": true, "data": {...} }` on success or `{ "ok": false, "error": { "code": "...", "message": "..." } }` on failure.
|
|
115
|
+
|
|
116
|
+
### `GET /v1/health` *(public)*
|
|
117
|
+
|
|
118
|
+
Health check; no auth required.
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{ "ok": true, "data": { "status": "ok", "uptimeS": 123 } }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `GET /v1/market-status`
|
|
125
|
+
|
|
126
|
+
Returns the current US market session classification.
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{ "ok": true, "data": { "session": "regular", "timestamp": 1782000000000 } }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`session` is one of `pre`, `regular`, `post`, `closed`. Holiday list currently covers 2026.
|
|
133
|
+
|
|
134
|
+
### `GET /v1/holdings`
|
|
135
|
+
|
|
136
|
+
Accounts and their holdings.
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{ "ok": true, "data": { "accounts": [
|
|
140
|
+
{ "id": "fidelity", "provider_id": "import", "kind": "brokerage", "name": "Fidelity",
|
|
141
|
+
"holdings": [ { "account_id": "fidelity", "symbol": "AAPL", "quantity": 10, "asset_class": "equity", "as_of": 1782000000000 } ] }
|
|
142
|
+
] } }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `GET /v1/net-worth`
|
|
146
|
+
|
|
147
|
+
Total portfolio value using latest prices (falls back to average cost).
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{ "ok": true, "data": { "netWorth": 123456.78, "accountCount": 3 } }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `GET /v1/allocation`
|
|
154
|
+
|
|
155
|
+
Current asset allocation as flat weights by asset class.
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{ "ok": true, "data": { "allocation": { "equity": 0.72, "bonds": 0.18, "cash": 0.10 }, "totalValue": 123456.78 } }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `GET /v1/drift`
|
|
162
|
+
|
|
163
|
+
Allocation drift vs the configured goal's target allocation.
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{ "ok": true, "data": { "drift": [
|
|
167
|
+
{ "class": "equity", "currentPct": 0.72, "targetPct": 0.80, "deltaPct": -0.08, "value": 88888.0 }
|
|
168
|
+
] } }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `GET /v1/goals`
|
|
172
|
+
|
|
173
|
+
List investment goals (target allocation is parsed from stored JSON).
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{ "ok": true, "data": { "goals": [
|
|
177
|
+
{ "id": "g1", "name": "Growth", "targetAllocation": { "equity": 0.8, "bonds": 0.2 }, "riskLimits": {}, "horizon_years": 10 }
|
|
178
|
+
] } }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
> Note: `target_allocation` and `risk_limits` are camelCased in the response (`targetAllocation`/`riskLimits`, parsed from JSON); `horizon_years` keeps its snake_case DB form.
|
|
182
|
+
|
|
183
|
+
### `POST /v1/goals`
|
|
184
|
+
|
|
185
|
+
Create or update (UPSERT) an investment goal. Validates that the target allocation sums to ~1.0.
|
|
186
|
+
|
|
187
|
+
**Request body:**
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"id": "g1",
|
|
192
|
+
"name": "Growth",
|
|
193
|
+
"targetAllocation": { "equity": 0.8, "bonds": 0.2 },
|
|
194
|
+
"riskLimits": { "maxConcentration": 0.25 },
|
|
195
|
+
"horizonYears": 10
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
| Field | Type | Required | Description |
|
|
200
|
+
|-------|------|----------|-------------|
|
|
201
|
+
| `id` | string | yes | Goal identifier |
|
|
202
|
+
| `name` | string | yes | Display name |
|
|
203
|
+
| `targetAllocation` | object | yes | Asset-class weights (must sum to ~1.0) |
|
|
204
|
+
| `riskLimits` | object | no | Risk limits (e.g. `maxConcentration`) |
|
|
205
|
+
| `horizonYears` | number | no | Investment horizon |
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{ "ok": true, "data": { "id": "g1" } }
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### `GET /v1/suggestions`
|
|
212
|
+
|
|
213
|
+
Pending rebalance/risk/drift suggestions computed by the quant engine. Each suggestion's `payload` is parsed from stored JSON.
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{ "ok": true, "data": { "suggestions": [
|
|
217
|
+
{ "id": "s-...-0", "kind": "rebalance", "status": "pending", "payload": { "symbol": "AAPL", "action": "buy", "amount": 500 } }
|
|
218
|
+
] } }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### `POST /v1/suggestions/dismiss`
|
|
222
|
+
|
|
223
|
+
Dismiss a suggestion by id.
|
|
224
|
+
|
|
225
|
+
**Request body:** `{ "id": "s-...-0" }`
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{ "ok": true, "data": { "dismissed": "s-...-0" } }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### `POST /v1/sync`
|
|
232
|
+
|
|
233
|
+
Trigger a full scheduler tick: ingest from all configured providers, refresh prices, recompute suggestions.
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{ "ok": true, "data": {
|
|
237
|
+
"message": "Sync complete",
|
|
238
|
+
"session": "regular",
|
|
239
|
+
"accountsIngested": 3,
|
|
240
|
+
"holdingsIngested": 12,
|
|
241
|
+
"pricesUpdated": 12,
|
|
242
|
+
"suggestionsCreated": 2,
|
|
243
|
+
"errors": []
|
|
244
|
+
} }
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### `POST /v1/import`
|
|
248
|
+
|
|
249
|
+
Import holdings from a local file (CSV or OFX). Absolute paths are allowed (single-user local service); relative paths containing `..` are rejected.
|
|
250
|
+
|
|
251
|
+
**Request body:** `{ "filePath": "/Users/me/Downloads/fidelity-positions.csv" }`
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{ "ok": true, "data": { "message": "Import complete", "filePath": "...", "accounts": 1 } }
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### `GET /v1/history?symbol=AAPL[&accountId=...]`
|
|
258
|
+
|
|
259
|
+
Price history for a symbol, newest first. `accountId` optionally filters to prices relevant to a holding in that account.
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{ "ok": true, "data": { "history": [
|
|
263
|
+
{ "symbol": "AAPL", "date": 1782000000000, "close": 210.5, "source": "stooq" }
|
|
264
|
+
] } }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `POST /v1/export`
|
|
268
|
+
|
|
269
|
+
Export data. `format: json` returns all tables inline; `format: sqlite` writes a backup copy of the database (restricted to the finance backup directory).
|
|
270
|
+
|
|
271
|
+
**Request body:** `{ "format": "json" }` or `{ "format": "sqlite", "path": "backup.db" }`
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
// json
|
|
275
|
+
{ "ok": true, "data": { "holdings": [...], "prices": [...], ... } }
|
|
276
|
+
// sqlite
|
|
277
|
+
{ "ok": true, "data": { "backupPath": "/home/.pi/sf/finance/backups/backup.db" } }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Data model
|
|
283
|
+
|
|
284
|
+
SQLite, stored at `SF_FINANCE_DB`. Versioned migrations (see `src/store/schema.ts`); future changes add migration entries rather than mutating existing tables.
|
|
285
|
+
|
|
286
|
+
| Table | Purpose |
|
|
287
|
+
|-------|---------|
|
|
288
|
+
| `accounts` | Linked accounts (provider, kind, name, mask, currency, staleness) |
|
|
289
|
+
| `holdings` | Current holdings per account/symbol (quantity, avg cost, asset class, as-of) |
|
|
290
|
+
| `transactions` | Transactions (date, symbol, qty, price, type, fees) |
|
|
291
|
+
| `prices` | Price history per symbol/date (close, source) |
|
|
292
|
+
| `lots` | Tax lots per holding (open date, qty, cost basis) |
|
|
293
|
+
| `goals` | Investment goals (target allocation, risk limits, horizon) |
|
|
294
|
+
| `suggestion_records` | Persisted suggestions (kind, payload, status) |
|
|
295
|
+
| `market_sessions` | Cached market-session snapshots per date |
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Scheduler & quant engine
|
|
300
|
+
|
|
301
|
+
The built-in scheduler (`src/scheduler/`) runs a periodic tick whose cadence depends on the market session: more frequent intraday, hourly after hours, and every few hours when closed. Each tick:
|
|
302
|
+
|
|
303
|
+
1. **Ingests** fresh data from configured providers via the provider registry.
|
|
304
|
+
2. **Refreshes prices** from the configured data feed (default `stooq`).
|
|
305
|
+
3. **Recomputes suggestions** deterministically through the quant engine:
|
|
306
|
+
- **Drift** — current vs target allocation deltas
|
|
307
|
+
- **Rebalance** — buy/sell amounts to return to target
|
|
308
|
+
- **Risk** — concentration and cash-drag checks against `riskLimits`
|
|
309
|
+
- **DCA** — dollar-cost-averaging recommendations (where configured)
|
|
310
|
+
|
|
311
|
+
**Determinism:** all numbers are computed by pure functions in `src/quant/`. The LLM client applies judgment but never recomputes the figures. This keeps suggestions reproducible and auditable.
|
|
312
|
+
|
|
313
|
+
`POST /v1/sync` triggers a tick on demand.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Backup & restore
|
|
318
|
+
|
|
319
|
+
- **JSON export:** `POST /v1/export {"format":"json"}` returns all data inline.
|
|
320
|
+
- **SQLite backup:** `POST /v1/export {"format":"sqlite"}` writes a timestamped `.db` copy to `~/.pi/sf/finance/backups/` (path is sandboxed to that directory).
|
|
321
|
+
- **Restore:** stop the service, replace `SF_FINANCE_DB` with the backup file, restart.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Observability
|
|
326
|
+
|
|
327
|
+
Structured logs are emitted to stdout (JSON) with `level`, `msg`, and contextual fields. Key events: server start, ingest results, staleness warnings, tick summaries. Increase verbosity via your process supervisor's log level (the service logs at `info` by default).
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Security model
|
|
332
|
+
|
|
333
|
+
- **Local-first:** bind to `127.0.0.1` by default. Docker maps `127.0.0.1:7780:7780` (localhost only) so the service is not exposed to the LAN.
|
|
334
|
+
- **Bearer auth:** every non-health endpoint requires a token; compared with `timingSafeEqual`.
|
|
335
|
+
- **Secrets:** `secrets.json` is `chmod 600`; provider credentials never leave the host.
|
|
336
|
+
- **File imports:** absolute paths are allowed (local file access by design); relative `..` traversal is rejected.
|
|
337
|
+
- **Backups:** the export route sandboxes SQLite backups to the finance backup directory.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Troubleshooting
|
|
342
|
+
|
|
343
|
+
| Symptom | Fix |
|
|
344
|
+
|---------|-----|
|
|
345
|
+
| `401 Unauthorized` | Retrieve/regenerate the token (see [Authentication](#authentication)); check `SF_FINANCE_TOKEN` |
|
|
346
|
+
| Port already in use | Change `SF_FINANCE_PORT` and the compose port mapping |
|
|
347
|
+
| Stale holdings | Run `POST /v1/sync`; check provider credentials in `secrets.json` |
|
|
348
|
+
| `better-sqlite3` build fails (native) | Use the Docker image, or ensure `python3 make g++` are installed |
|
|
349
|
+
| No suggestions after sync | Set a goal via `POST /v1/goals` — drift/rebalance need a target |
|
|
350
|
+
|
|
351
|
+
---
|
|
94
352
|
|
|
95
353
|
## Cost
|
|
96
354
|
|
|
97
|
-
- **Free tier
|
|
98
|
-
- **Optional
|
|
99
|
-
- **Optional
|
|
355
|
+
- **Free tier:** File imports (CSV/OFX) and `stooq` prices — no API costs.
|
|
356
|
+
- **Optional:** Coinbase API (free, view-only scope) — currently a stub.
|
|
357
|
+
- **Optional:** SnapTrade / SimpleFIN / Teller aggregators (may have fees) — currently stubs.
|
|
358
|
+
|
|
359
|
+
---
|
|
100
360
|
|
|
101
361
|
## Disclaimer
|
|
102
362
|
|
|
103
|
-
**This is not financial advice.** The service provides deterministic calculations based on your data and configured goals. Suggestions are informational only — no trades are executed automatically.
|
|
363
|
+
**This is not financial advice.** The service provides deterministic calculations based on your data and configured goals. Suggestions are informational only — no trades are executed automatically. Always consult a qualified financial advisor before making investment decisions.
|
|
104
364
|
|
|
105
365
|
## License
|
|
106
366
|
|
package/docker/Dockerfile
CHANGED
|
@@ -23,8 +23,15 @@ COPY packages/paths/src ./packages/paths/src
|
|
|
23
23
|
|
|
24
24
|
# ---- runtime stage: slim, no build tooling ----
|
|
25
25
|
FROM node:20-slim AS runtime
|
|
26
|
+
ARG OCI_VERSION=dev
|
|
26
27
|
WORKDIR /app
|
|
27
28
|
|
|
29
|
+
# OCI labels for traceability (populated by buildx from metadata-action in CI)
|
|
30
|
+
LABEL org.opencontainers.image.title="@pi-stef/finance-api" \
|
|
31
|
+
org.opencontainers.image.source="https://github.com/sfiorini/pi-stef" \
|
|
32
|
+
org.opencontainers.image.licenses="MIT" \
|
|
33
|
+
org.opencontainers.image.version="${OCI_VERSION}"
|
|
34
|
+
|
|
28
35
|
# Install curl for healthcheck
|
|
29
36
|
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
|
|
30
37
|
|
package/docker/README.md
CHANGED
|
@@ -1,3 +1,123 @@
|
|
|
1
|
-
#
|
|
1
|
+
# finance-api Docker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `@pi-stef/finance-api` service is published as a multi-arch Docker image to the GitHub Container Registry.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd packages/finance-api/docker
|
|
9
|
+
docker compose up -d
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
This pulls `ghcr.io/sfiorini/pi-stef/finance-api:latest` and starts the service at `http://127.0.0.1:7780`.
|
|
13
|
+
|
|
14
|
+
Check it's running:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
curl http://127.0.0.1:7780/v1/health
|
|
18
|
+
# {"ok":true,"data":{"status":"ok","uptimeS":0}}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Image
|
|
22
|
+
|
|
23
|
+
| Registry | Image |
|
|
24
|
+
|----------|-------|
|
|
25
|
+
| GHCR | `ghcr.io/sfiorini/pi-stef/finance-api` |
|
|
26
|
+
|
|
27
|
+
**Tags:**
|
|
28
|
+
|
|
29
|
+
- `latest` — most recent release
|
|
30
|
+
- `X.Y.Z` — pinned release (e.g. `0.1.0`)
|
|
31
|
+
|
|
32
|
+
**Platforms:** `linux/amd64`, `linux/arm64` (Intel Macs / Linux servers + Apple Silicon).
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Pull a specific version
|
|
36
|
+
docker pull ghcr.io/sfiorini/pi-stef/finance-api:0.1.0
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The image is built from the repo source on every `@pi-stef/finance-api@X.Y.Z` tag push (see `.github/workflows/docker.yml`), so it always matches the released npm package.
|
|
40
|
+
|
|
41
|
+
## Build from source (local dev)
|
|
42
|
+
|
|
43
|
+
To build the image locally instead of pulling from the registry:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cd packages/finance-api/docker
|
|
47
|
+
# Uncomment the `build:` block in docker-compose.yml, then:
|
|
48
|
+
docker compose up --build
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or build directly with `docker build`:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
docker build -f packages/finance-api/docker/Dockerfile -t finance-api:dev .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The Dockerfile is a multi-stage source build. The build stage installs `python3`/`make`/`g++` to compile `better-sqlite3` native bindings; the runtime stage is slim and ships only the compiled app plus `curl` for healthchecks.
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
All configuration is via environment variables (prefix `SF_FINANCE_`), set automatically by `docker-compose.yml`:
|
|
62
|
+
|
|
63
|
+
| Variable | Default | Description |
|
|
64
|
+
|----------|---------|-------------|
|
|
65
|
+
| `SF_FINANCE_HOST` | `0.0.0.0` (container) | Server bind host |
|
|
66
|
+
| `SF_FINANCE_PORT` | `7780` | Server port |
|
|
67
|
+
| `SF_FINANCE_DB` | `/data/finance.db` | SQLite database path |
|
|
68
|
+
| `SF_FINANCE_DATA_FEED` | `stooq` | Price data feed |
|
|
69
|
+
|
|
70
|
+
See the [service README](../README.md) for the full configuration and secrets reference.
|
|
71
|
+
|
|
72
|
+
## Volumes
|
|
73
|
+
|
|
74
|
+
Two named volumes persist data across container restarts:
|
|
75
|
+
|
|
76
|
+
| Volume | Mount | Contents |
|
|
77
|
+
|--------|-------|----------|
|
|
78
|
+
| `finance-data` | `/data` | SQLite database (`finance.db`) |
|
|
79
|
+
| `finance-config` | `/root/.pi/sf/finance` | Auto-generated bearer token + config |
|
|
80
|
+
|
|
81
|
+
> **Important:** both volumes are required. The token volume (`finance-config`) ensures your bearer token survives restarts — without it, a new token is generated on every start and clients lose access.
|
|
82
|
+
|
|
83
|
+
## Retrieving the bearer token
|
|
84
|
+
|
|
85
|
+
The service auto-generates a bearer token on first start and writes it to `/root/.pi/sf/finance/token` inside the container (persisted via the `finance-config` volume). Retrieve it with:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
docker compose exec finance-api cat /root/.pi/sf/finance/token
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Use this token for all authenticated API requests:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
curl -H "Authorization: Bearer <token>" http://127.0.0.1:7780/v1/holdings
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The `@pi-stef/finance` extension client reads this token automatically when both run on the same host. In Docker, copy the token into the extension's config (`~/.pi/sf/finance/config.json`) or the `SF_FINANCE_TOKEN` env var.
|
|
98
|
+
|
|
99
|
+
## Healthcheck
|
|
100
|
+
|
|
101
|
+
The container includes a built-in healthcheck hitting `/v1/health` every 30s:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
docker compose ps # STATUS column shows "healthy"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## GHCR visibility
|
|
108
|
+
|
|
109
|
+
The first push creates the package under the `sfiorini` namespace on GHCR. By default the image inherits the repository's visibility (private for a private repo). To allow unauthenticated pulls, set the package to **public** in GitHub → Packages → `pi-stef/finance-api` → Package settings.
|
|
110
|
+
|
|
111
|
+
## Troubleshooting
|
|
112
|
+
|
|
113
|
+
| Symptom | Fix |
|
|
114
|
+
|---------|-----|
|
|
115
|
+
| `401 Unauthorized` | Token mismatch — retrieve it from the container (above) and update client config |
|
|
116
|
+
| Port already in use | Change `SF_FINANCE_PORT` and the compose port mapping |
|
|
117
|
+
| `better-sqlite3` build fails | Use the prebuilt registry image; building from source requires the build stage's toolchain |
|
|
118
|
+
| Can't reach service from another container | Use the service name `finance-api` as the hostname, or set `SF_FINANCE_HOST=0.0.0.0` and join the same Docker network |
|
|
119
|
+
| Healthcheck never goes healthy | Check `docker compose logs finance-api`; ensure the DB volume is writable |
|
|
120
|
+
|
|
121
|
+
## Native (non-Docker) alternative
|
|
122
|
+
|
|
123
|
+
See [docs/native-run.md](../docs/native-run.md) for launchd/systemd setup without Docker.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
version: "3.8"
|
|
2
|
-
|
|
3
1
|
services:
|
|
4
2
|
finance-api:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
# Pull the published image from GHCR (default):
|
|
4
|
+
image: ghcr.io/sfiorini/pi-stef/finance-api:latest
|
|
5
|
+
# To build from local source instead, comment out `image:` above and run:
|
|
6
|
+
# docker compose up --build
|
|
7
|
+
# build:
|
|
8
|
+
# context: ../..
|
|
9
|
+
# dockerfile: packages/finance-api/docker/Dockerfile
|
|
8
10
|
ports:
|
|
9
11
|
- "127.0.0.1:7780:7780" # Localhost only for security
|
|
10
12
|
volumes:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-stef/finance-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Always-on local service for @pi-stef/finance: ingests financial-account data, stores locally, runs a deterministic quant engine and market-aware scheduler.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|