@fuzzle/opencode-accountant 0.14.0 → 0.15.0
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 +29 -6
- package/agent/accountant.md +11 -10
- package/dist/index.js +73 -80
- package/docs/tools/{fetch-currency-prices.md → fetch-prices.md} +142 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ An OpenCode accounting agent, specialized in double-entry-bookkepping with hledg
|
|
|
16
16
|
|
|
17
17
|
### Price Fetching Configuration
|
|
18
18
|
|
|
19
|
-
The `fetch-
|
|
19
|
+
The `fetch-prices` tool requires a configuration file to specify which currency pairs and stock tickers to fetch. Create a `config/prices.yaml` file in your project directory with the following structure:
|
|
20
20
|
|
|
21
21
|
```yaml
|
|
22
22
|
currencies:
|
|
@@ -38,9 +38,16 @@ currencies:
|
|
|
38
38
|
file: usd-chf.journal
|
|
39
39
|
fmt_base: USD
|
|
40
40
|
backfill_date: '2025-06-01'
|
|
41
|
+
|
|
42
|
+
stocks:
|
|
43
|
+
AG.TO:
|
|
44
|
+
DRLL:
|
|
45
|
+
BOGO:
|
|
46
|
+
pair: BOGO.V
|
|
47
|
+
fmt_base: BOGO
|
|
41
48
|
```
|
|
42
49
|
|
|
43
|
-
#### Configuration Options
|
|
50
|
+
#### Currency Configuration Options
|
|
44
51
|
|
|
45
52
|
Each currency entry supports the following fields:
|
|
46
53
|
|
|
@@ -52,6 +59,18 @@ Each currency entry supports the following fields:
|
|
|
52
59
|
| `fmt_base` | No | Base currency format override for pricehist (e.g., `USD` for yahoo source) |
|
|
53
60
|
| `backfill_date` | No | Start date for backfill mode. Defaults to January 1st of the current year |
|
|
54
61
|
|
|
62
|
+
#### Stock Configuration Options
|
|
63
|
+
|
|
64
|
+
The `stocks` section is an optional mapping of ticker symbols. Bare keys (null values) use defaults; entries with properties override them:
|
|
65
|
+
|
|
66
|
+
| Field | Default | Description |
|
|
67
|
+
| --------------- | -------------------------------- | ---------------------------------------------------------------- |
|
|
68
|
+
| `source` | `yahoo` | The pricehist data source |
|
|
69
|
+
| `pair` | the ticker key itself | The ticker/pair to fetch (useful when exchange ticker differs) |
|
|
70
|
+
| `file` | `{lowercase ticker}.journal` | Output filename in `ledger/stocks/` |
|
|
71
|
+
| `fmt_base` | — | Base symbol override (e.g., `BOGO` when pair is `BOGO.V`) |
|
|
72
|
+
| `backfill_date` | — | Start date for backfill mode (defaults to Jan 1 of current year) |
|
|
73
|
+
|
|
55
74
|
#### Directory Structure
|
|
56
75
|
|
|
57
76
|
The plugin expects the following directory structure in your project:
|
|
@@ -61,10 +80,14 @@ your-project/
|
|
|
61
80
|
├── config/
|
|
62
81
|
│ └── prices.yaml # Price fetching configuration
|
|
63
82
|
└── ledger/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
├── currencies/ # Where currency price journals are written
|
|
84
|
+
│ ├── btc-chf.journal
|
|
85
|
+
│ ├── eur-chf.journal
|
|
86
|
+
│ └── usd-chf.journal
|
|
87
|
+
└── stocks/ # Where stock price journals are written
|
|
88
|
+
├── ag.to.journal
|
|
89
|
+
├── drll.journal
|
|
90
|
+
└── bogo.journal
|
|
68
91
|
```
|
|
69
92
|
|
|
70
93
|
#### Base Currency
|
package/agent/accountant.md
CHANGED
|
@@ -8,7 +8,7 @@ tools:
|
|
|
8
8
|
bash: true
|
|
9
9
|
edit: true
|
|
10
10
|
write: true
|
|
11
|
-
# MCP tools available: import-pipeline, fetch-
|
|
11
|
+
# MCP tools available: import-pipeline, fetch-prices, hledger-mcp (read-only queries)
|
|
12
12
|
permissions:
|
|
13
13
|
bash: allow
|
|
14
14
|
edit: allow
|
|
@@ -59,7 +59,7 @@ You have access to specialized MCP tools that MUST be used for their designated
|
|
|
59
59
|
| Tool | Use For | NEVER Do Instead |
|
|
60
60
|
| ----------------------- | ---------------------------------------------------- | --------------------------------------------------------- |
|
|
61
61
|
| `import-pipeline` | Full import workflow (classify → import → reconcile) | Manual file moves, `hledger import`, manual journal edits |
|
|
62
|
-
| `fetch-
|
|
62
|
+
| `fetch-prices` | Fetching exchange rates and stock prices | `curl` to price APIs, manual price entries |
|
|
63
63
|
| hledger MCP tools | Read-only queries (balance, register, accounts, etc.) | `hledger bal`, `hledger reg`, `hledger print` via bash |
|
|
64
64
|
|
|
65
65
|
These tools handle validation, deduplication, error checking, and file organization automatically. Bypassing them risks data corruption, duplicate transactions, and inconsistent state.
|
|
@@ -84,7 +84,7 @@ Bash is **FORBIDDEN** for:
|
|
|
84
84
|
- **`hledger import`** - Use `import-pipeline` tool instead
|
|
85
85
|
- **Moving/copying CSV files** - Use `import-pipeline` tool instead
|
|
86
86
|
- **Writing to files** - Use `edit`/`write` tools with user approval only
|
|
87
|
-
- **Fetching prices** - Use `fetch-
|
|
87
|
+
- **Fetching prices** - Use `fetch-prices` tool instead
|
|
88
88
|
- **`hledger -f <file>`** - Never specify journal files explicitly; `.hledger.journal` includes all journals automatically
|
|
89
89
|
|
|
90
90
|
## Manual Editing Policy
|
|
@@ -232,22 +232,23 @@ The following are MCP tools available to you. Always call these tools directly -
|
|
|
232
232
|
|
|
233
233
|
---
|
|
234
234
|
|
|
235
|
-
### fetch-
|
|
235
|
+
### fetch-prices
|
|
236
236
|
|
|
237
|
-
**Purpose:** Fetches currency exchange rates and updates `ledger/currencies/` journals.
|
|
237
|
+
**Purpose:** Fetches currency exchange rates and stock prices, updates `ledger/currencies/` and `ledger/stocks/` journals.
|
|
238
238
|
|
|
239
239
|
**Usage:**
|
|
240
240
|
|
|
241
|
-
- Daily mode (default): `fetch-
|
|
242
|
-
- Backfill mode: `fetch-
|
|
241
|
+
- Daily mode (default): `fetch-prices()` or `fetch-prices(backfill: false)`
|
|
242
|
+
- Backfill mode: `fetch-prices(backfill: true)`
|
|
243
243
|
|
|
244
244
|
**Behavior:**
|
|
245
245
|
|
|
246
246
|
- Daily mode: Fetches yesterday's prices only
|
|
247
247
|
- Backfill mode: Fetches from `backfill_date` (or Jan 1 of current year) to yesterday
|
|
248
248
|
- Updates journal files in-place with deduplication (newer prices overwrite older for same date)
|
|
249
|
-
- Processes all currencies independently (partial failures possible)
|
|
249
|
+
- Processes all currencies and stocks independently (partial failures possible)
|
|
250
|
+
- Stock symbols with dots (e.g., `AG.TO`) are automatically quoted for hledger compatibility
|
|
250
251
|
|
|
251
|
-
**Output:** Returns per-
|
|
252
|
+
**Output:** Returns per-ticker results with latest price line or error message
|
|
252
253
|
|
|
253
|
-
**Configuration:** `config/prices.yaml` defines currencies
|
|
254
|
+
**Configuration:** `config/prices.yaml` defines currencies (with full config) and stocks (map with optional overrides)
|
package/dist/index.js
CHANGED
|
@@ -16870,7 +16870,7 @@ function tool(input) {
|
|
|
16870
16870
|
return input;
|
|
16871
16871
|
}
|
|
16872
16872
|
tool.schema = exports_external;
|
|
16873
|
-
// src/tools/fetch-
|
|
16873
|
+
// src/tools/fetch-prices.ts
|
|
16874
16874
|
var {$ } = globalThis.Bun;
|
|
16875
16875
|
import * as path4 from "path";
|
|
16876
16876
|
|
|
@@ -16939,6 +16939,26 @@ function validateCurrencyConfig(name, config2) {
|
|
|
16939
16939
|
backfill_date: typeof configObj.backfill_date === "string" ? configObj.backfill_date : undefined
|
|
16940
16940
|
};
|
|
16941
16941
|
}
|
|
16942
|
+
function normalizeStockConfig(ticker, raw) {
|
|
16943
|
+
if (raw === null || raw === undefined) {
|
|
16944
|
+
return {
|
|
16945
|
+
source: "yahoo",
|
|
16946
|
+
pair: ticker,
|
|
16947
|
+
file: `${ticker.toLowerCase()}.journal`
|
|
16948
|
+
};
|
|
16949
|
+
}
|
|
16950
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
16951
|
+
throw new Error(`Invalid config for stock '${ticker}': expected an object or null`);
|
|
16952
|
+
}
|
|
16953
|
+
const obj = raw;
|
|
16954
|
+
return {
|
|
16955
|
+
source: typeof obj.source === "string" && obj.source !== "" ? obj.source : "yahoo",
|
|
16956
|
+
pair: typeof obj.pair === "string" && obj.pair !== "" ? obj.pair : ticker,
|
|
16957
|
+
file: typeof obj.file === "string" && obj.file !== "" ? obj.file : `${ticker.toLowerCase()}.journal`,
|
|
16958
|
+
fmt_base: typeof obj.fmt_base === "string" ? obj.fmt_base : undefined,
|
|
16959
|
+
backfill_date: typeof obj.backfill_date === "string" ? obj.backfill_date : undefined
|
|
16960
|
+
};
|
|
16961
|
+
}
|
|
16942
16962
|
function loadPricesConfig(directory) {
|
|
16943
16963
|
return loadYamlConfig(directory, CONFIG_FILE, (parsedObj) => {
|
|
16944
16964
|
if (!parsedObj.currencies || typeof parsedObj.currencies !== "object") {
|
|
@@ -16952,17 +16972,15 @@ function loadPricesConfig(directory) {
|
|
|
16952
16972
|
for (const [name, config2] of Object.entries(currenciesObj)) {
|
|
16953
16973
|
currencies[name] = validateCurrencyConfig(name, config2);
|
|
16954
16974
|
}
|
|
16955
|
-
|
|
16956
|
-
if (parsedObj.stocks !== undefined) {
|
|
16957
|
-
if (
|
|
16958
|
-
throw new Error(`Invalid config: 'stocks' must be a
|
|
16975
|
+
const stocks = {};
|
|
16976
|
+
if (parsedObj.stocks !== undefined && parsedObj.stocks !== null) {
|
|
16977
|
+
if (typeof parsedObj.stocks !== "object" || Array.isArray(parsedObj.stocks)) {
|
|
16978
|
+
throw new Error(`Invalid config: 'stocks' must be a mapping`);
|
|
16959
16979
|
}
|
|
16960
|
-
|
|
16961
|
-
|
|
16962
|
-
|
|
16963
|
-
}
|
|
16980
|
+
const stocksObj = parsedObj.stocks;
|
|
16981
|
+
for (const [ticker, raw] of Object.entries(stocksObj)) {
|
|
16982
|
+
stocks[ticker] = normalizeStockConfig(ticker, raw);
|
|
16964
16983
|
}
|
|
16965
|
-
stocks = parsedObj.stocks;
|
|
16966
16984
|
}
|
|
16967
16985
|
return { currencies, stocks };
|
|
16968
16986
|
}, `Configuration file not found: ${CONFIG_FILE}. Please refer to the plugin's GitHub repository for setup instructions.`);
|
|
@@ -17200,12 +17218,12 @@ function buildToolSuccessResult(data) {
|
|
|
17200
17218
|
return JSON.stringify(result);
|
|
17201
17219
|
}
|
|
17202
17220
|
|
|
17203
|
-
// src/tools/fetch-
|
|
17221
|
+
// src/tools/fetch-prices.ts
|
|
17204
17222
|
async function defaultPriceFetcher(cmdArgs) {
|
|
17205
17223
|
const result = await $`pricehist ${cmdArgs}`.quiet();
|
|
17206
17224
|
return result.stdout.toString().trim();
|
|
17207
17225
|
}
|
|
17208
|
-
function buildPricehistArgs(startDate, endDate,
|
|
17226
|
+
function buildPricehistArgs(startDate, endDate, config2) {
|
|
17209
17227
|
const cmdArgs = [
|
|
17210
17228
|
"fetch",
|
|
17211
17229
|
"-o",
|
|
@@ -17214,11 +17232,11 @@ function buildPricehistArgs(startDate, endDate, currencyConfig) {
|
|
|
17214
17232
|
startDate,
|
|
17215
17233
|
"-e",
|
|
17216
17234
|
endDate,
|
|
17217
|
-
|
|
17218
|
-
|
|
17235
|
+
config2.source,
|
|
17236
|
+
config2.pair
|
|
17219
17237
|
];
|
|
17220
|
-
if (
|
|
17221
|
-
cmdArgs.push("--fmt-base",
|
|
17238
|
+
if (config2.fmt_base) {
|
|
17239
|
+
cmdArgs.push("--fmt-base", config2.fmt_base);
|
|
17222
17240
|
}
|
|
17223
17241
|
return cmdArgs;
|
|
17224
17242
|
}
|
|
@@ -17254,8 +17272,36 @@ function filterAndSortPriceLinesByDateRange(priceLines, startDate, endDate) {
|
|
|
17254
17272
|
return parsed.date >= startDate && parsed.date <= endDate;
|
|
17255
17273
|
}).sort((a, b) => a.date.localeCompare(b.date)).map((parsed) => parsed.formattedLine);
|
|
17256
17274
|
}
|
|
17257
|
-
async function
|
|
17258
|
-
const
|
|
17275
|
+
async function fetchAndWritePrices(ticker, config2, outputSubdir, directory, endDate, defaultBackfillDate, backfill, priceFetcher) {
|
|
17276
|
+
const startDate = backfill ? config2.backfill_date || defaultBackfillDate : endDate;
|
|
17277
|
+
const cmdArgs = buildPricehistArgs(startDate, endDate, config2);
|
|
17278
|
+
const output = await priceFetcher(cmdArgs);
|
|
17279
|
+
const rawPriceLines = output.split(`
|
|
17280
|
+
`).filter((line) => line.startsWith("P "));
|
|
17281
|
+
if (rawPriceLines.length === 0) {
|
|
17282
|
+
return {
|
|
17283
|
+
ticker,
|
|
17284
|
+
error: `No price lines in pricehist output: ${output}`
|
|
17285
|
+
};
|
|
17286
|
+
}
|
|
17287
|
+
const priceLines = filterAndSortPriceLinesByDateRange(rawPriceLines, startDate, endDate).map(ensureQuotedSymbols);
|
|
17288
|
+
if (priceLines.length === 0) {
|
|
17289
|
+
return {
|
|
17290
|
+
ticker,
|
|
17291
|
+
error: `No price data found within date range ${startDate} to ${endDate}`
|
|
17292
|
+
};
|
|
17293
|
+
}
|
|
17294
|
+
const journalPath = path4.join(directory, outputSubdir, config2.file);
|
|
17295
|
+
updatePriceJournal(journalPath, priceLines);
|
|
17296
|
+
const latestPriceLine = priceLines[priceLines.length - 1];
|
|
17297
|
+
return {
|
|
17298
|
+
ticker,
|
|
17299
|
+
priceLine: latestPriceLine,
|
|
17300
|
+
file: config2.file
|
|
17301
|
+
};
|
|
17302
|
+
}
|
|
17303
|
+
async function fetchPrices(directory, agent, backfill, priceFetcher = defaultPriceFetcher, configLoader = loadPricesConfig) {
|
|
17304
|
+
const restrictionError = checkAccountantAgent(agent, "fetch prices");
|
|
17259
17305
|
if (restrictionError) {
|
|
17260
17306
|
return restrictionError;
|
|
17261
17307
|
}
|
|
@@ -17271,34 +17317,8 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
|
|
|
17271
17317
|
const results = [];
|
|
17272
17318
|
for (const [ticker, currencyConfig] of Object.entries(config2.currencies)) {
|
|
17273
17319
|
try {
|
|
17274
|
-
const
|
|
17275
|
-
|
|
17276
|
-
const output = await priceFetcher(cmdArgs);
|
|
17277
|
-
const rawPriceLines = output.split(`
|
|
17278
|
-
`).filter((line) => line.startsWith("P "));
|
|
17279
|
-
if (rawPriceLines.length === 0) {
|
|
17280
|
-
results.push({
|
|
17281
|
-
ticker,
|
|
17282
|
-
error: `No price lines in pricehist output: ${output}`
|
|
17283
|
-
});
|
|
17284
|
-
continue;
|
|
17285
|
-
}
|
|
17286
|
-
const priceLines = filterAndSortPriceLinesByDateRange(rawPriceLines, startDate, endDate);
|
|
17287
|
-
if (priceLines.length === 0) {
|
|
17288
|
-
results.push({
|
|
17289
|
-
ticker,
|
|
17290
|
-
error: `No price data found within date range ${startDate} to ${endDate}`
|
|
17291
|
-
});
|
|
17292
|
-
continue;
|
|
17293
|
-
}
|
|
17294
|
-
const journalPath = path4.join(directory, "ledger", "currencies", currencyConfig.file);
|
|
17295
|
-
updatePriceJournal(journalPath, priceLines);
|
|
17296
|
-
const latestPriceLine = priceLines[priceLines.length - 1];
|
|
17297
|
-
results.push({
|
|
17298
|
-
ticker,
|
|
17299
|
-
priceLine: latestPriceLine,
|
|
17300
|
-
file: currencyConfig.file
|
|
17301
|
-
});
|
|
17320
|
+
const result = await fetchAndWritePrices(ticker, currencyConfig, "ledger/currencies", directory, endDate, defaultBackfillDate, backfill, priceFetcher);
|
|
17321
|
+
results.push(result);
|
|
17302
17322
|
} catch (err) {
|
|
17303
17323
|
results.push({
|
|
17304
17324
|
ticker,
|
|
@@ -17306,37 +17326,10 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
|
|
|
17306
17326
|
});
|
|
17307
17327
|
}
|
|
17308
17328
|
}
|
|
17309
|
-
for (const ticker of config2.stocks
|
|
17329
|
+
for (const [ticker, stockConfig] of Object.entries(config2.stocks)) {
|
|
17310
17330
|
try {
|
|
17311
|
-
const
|
|
17312
|
-
|
|
17313
|
-
const output = await priceFetcher(cmdArgs);
|
|
17314
|
-
const rawPriceLines = output.split(`
|
|
17315
|
-
`).filter((line) => line.startsWith("P "));
|
|
17316
|
-
if (rawPriceLines.length === 0) {
|
|
17317
|
-
results.push({
|
|
17318
|
-
ticker,
|
|
17319
|
-
error: `No price lines in pricehist output: ${output}`
|
|
17320
|
-
});
|
|
17321
|
-
continue;
|
|
17322
|
-
}
|
|
17323
|
-
const priceLines = filterAndSortPriceLinesByDateRange(rawPriceLines, startDate, endDate).map(ensureQuotedSymbols);
|
|
17324
|
-
if (priceLines.length === 0) {
|
|
17325
|
-
results.push({
|
|
17326
|
-
ticker,
|
|
17327
|
-
error: `No price data found within date range ${startDate} to ${endDate}`
|
|
17328
|
-
});
|
|
17329
|
-
continue;
|
|
17330
|
-
}
|
|
17331
|
-
const file2 = `${ticker.toLowerCase()}.journal`;
|
|
17332
|
-
const journalPath = path4.join(directory, "ledger", "stocks", file2);
|
|
17333
|
-
updatePriceJournal(journalPath, priceLines);
|
|
17334
|
-
const latestPriceLine = priceLines[priceLines.length - 1];
|
|
17335
|
-
results.push({
|
|
17336
|
-
ticker,
|
|
17337
|
-
priceLine: latestPriceLine,
|
|
17338
|
-
file: file2
|
|
17339
|
-
});
|
|
17331
|
+
const result = await fetchAndWritePrices(ticker, stockConfig, "ledger/stocks", directory, endDate, defaultBackfillDate, backfill, priceFetcher);
|
|
17332
|
+
results.push(result);
|
|
17340
17333
|
} catch (err) {
|
|
17341
17334
|
results.push({
|
|
17342
17335
|
ticker,
|
|
@@ -17346,15 +17339,15 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
|
|
|
17346
17339
|
}
|
|
17347
17340
|
return buildSuccessResult(results, endDate, backfill);
|
|
17348
17341
|
}
|
|
17349
|
-
var
|
|
17342
|
+
var fetch_prices_default = tool({
|
|
17350
17343
|
description: "ACCOUNTANT AGENT ONLY: Fetches end-of-day prices for all configured currencies and stock tickers (from config/prices.yaml) and appends them to the corresponding price journals in ledger/currencies/ and ledger/stocks/.",
|
|
17351
17344
|
args: {
|
|
17352
|
-
backfill: tool.schema.boolean().optional().describe("If true, fetch history from each currency's configured backfill_date (or Jan 1 of current year if not specified)")
|
|
17345
|
+
backfill: tool.schema.boolean().optional().describe("If true, fetch history from each currency/stock's configured backfill_date (or Jan 1 of current year if not specified)")
|
|
17353
17346
|
},
|
|
17354
17347
|
async execute(params, context) {
|
|
17355
17348
|
const { directory, agent } = context;
|
|
17356
17349
|
const { backfill } = params;
|
|
17357
|
-
return
|
|
17350
|
+
return fetchPrices(directory, agent, backfill || false);
|
|
17358
17351
|
}
|
|
17359
17352
|
});
|
|
17360
17353
|
// src/tools/classify-statements.ts
|
|
@@ -27817,7 +27810,7 @@ var AccountantPlugin = async () => {
|
|
|
27817
27810
|
const agent = loadAgent(AGENT_FILE);
|
|
27818
27811
|
return {
|
|
27819
27812
|
tool: {
|
|
27820
|
-
"fetch-
|
|
27813
|
+
"fetch-prices": fetch_prices_default,
|
|
27821
27814
|
"classify-statements": classify_statements_default,
|
|
27822
27815
|
"import-statements": import_statements_default,
|
|
27823
27816
|
"reconcile-statements": reconcile_statement_default,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# fetch-
|
|
1
|
+
# fetch-prices Tool
|
|
2
2
|
|
|
3
|
-
The `fetch-
|
|
3
|
+
The `fetch-prices` tool fetches end-of-day currency exchange rates and stock prices, updating the price journals in `ledger/currencies/` and `ledger/stocks/`. It uses the external `pricehist` tool to fetch data from various sources (Yahoo Finance, CoinMarketCap, ECB, etc.).
|
|
4
4
|
|
|
5
5
|
This tool is **restricted to the accountant agent only**.
|
|
6
6
|
|
|
@@ -33,15 +33,15 @@ When fetching only yesterday's prices:
|
|
|
33
33
|
"file": "usd.journal"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
|
-
"ticker": "
|
|
37
|
-
"priceLine": "P 2026-02-21
|
|
38
|
-
"file": "
|
|
36
|
+
"ticker": "AG.TO",
|
|
37
|
+
"priceLine": "P 2026-02-21 \"AG.TO\" 10.07 CAD",
|
|
38
|
+
"file": "ag.to.journal"
|
|
39
39
|
}
|
|
40
40
|
]
|
|
41
41
|
}
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
**Note:** The `priceLine` shows the latest (most recent) price added. In daily mode, only one price per
|
|
44
|
+
**Note:** The `priceLine` shows the latest (most recent) price added. In daily mode, only one price per ticker is fetched. Stock symbols with dots are automatically quoted for hledger compatibility.
|
|
45
45
|
|
|
46
46
|
### Backfill Success (backfill: true)
|
|
47
47
|
|
|
@@ -59,9 +59,9 @@ When fetching historical prices:
|
|
|
59
59
|
"file": "eur.journal"
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
|
-
"ticker": "
|
|
63
|
-
"priceLine": "P 2026-02-21
|
|
64
|
-
"file": "
|
|
62
|
+
"ticker": "DRLL",
|
|
63
|
+
"priceLine": "P 2026-02-21 DRLL 28.77 USD",
|
|
64
|
+
"file": "drll.journal"
|
|
65
65
|
}
|
|
66
66
|
]
|
|
67
67
|
}
|
|
@@ -71,7 +71,7 @@ When fetching historical prices:
|
|
|
71
71
|
|
|
72
72
|
### Partial Failure
|
|
73
73
|
|
|
74
|
-
When some
|
|
74
|
+
When some tickers succeed and others fail:
|
|
75
75
|
|
|
76
76
|
```json
|
|
77
77
|
{
|
|
@@ -96,7 +96,7 @@ When some currencies succeed and others fail:
|
|
|
96
96
|
}
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
The tool processes all currencies independently. Partial success is possible.
|
|
99
|
+
The tool processes all currencies and stocks independently. Partial success is possible.
|
|
100
100
|
|
|
101
101
|
### Configuration Error
|
|
102
102
|
|
|
@@ -126,8 +126,8 @@ When called by the wrong agent:
|
|
|
126
126
|
|
|
127
127
|
**Behavior:**
|
|
128
128
|
|
|
129
|
-
- Fetches only yesterday's price for each currency
|
|
130
|
-
- Fast execution (single date per
|
|
129
|
+
- Fetches only yesterday's price for each currency/stock
|
|
130
|
+
- Fast execution (single date per ticker)
|
|
131
131
|
- Typical use: Daily or weekly routine updates
|
|
132
132
|
|
|
133
133
|
**Date range:** Yesterday to yesterday
|
|
@@ -135,9 +135,9 @@ When called by the wrong agent:
|
|
|
135
135
|
**Example:**
|
|
136
136
|
|
|
137
137
|
```
|
|
138
|
-
fetch-
|
|
138
|
+
fetch-prices()
|
|
139
139
|
# or
|
|
140
|
-
fetch-
|
|
140
|
+
fetch-prices(backfill: false)
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
**Use when:**
|
|
@@ -151,22 +151,22 @@ fetch-currency-prices(backfill: false)
|
|
|
151
151
|
**Behavior:**
|
|
152
152
|
|
|
153
153
|
- Fetches historical prices from `backfill_date` to yesterday
|
|
154
|
-
- Slower execution (multiple dates per
|
|
155
|
-
- Typical use: Initial setup, adding new currency, filling gaps
|
|
154
|
+
- Slower execution (multiple dates per ticker)
|
|
155
|
+
- Typical use: Initial setup, adding new currency/stock, filling gaps
|
|
156
156
|
|
|
157
|
-
**Date range:** Per-
|
|
157
|
+
**Date range:** Per-ticker `backfill_date` (or default) to yesterday
|
|
158
158
|
|
|
159
159
|
**Example:**
|
|
160
160
|
|
|
161
161
|
```
|
|
162
|
-
fetch-
|
|
162
|
+
fetch-prices(backfill: true)
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
**Use when:**
|
|
166
166
|
|
|
167
|
-
- Adding a new currency (populate full history)
|
|
167
|
+
- Adding a new currency or stock (populate full history)
|
|
168
168
|
- Fixing missing dates (fill gaps)
|
|
169
|
-
- Initial repository setup (populate all
|
|
169
|
+
- Initial repository setup (populate all prices)
|
|
170
170
|
- Recovering from extended outage
|
|
171
171
|
|
|
172
172
|
### Backfill Date Configuration
|
|
@@ -182,12 +182,22 @@ currencies:
|
|
|
182
182
|
backfill_date: '2024-01-01' # Start from this date
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
+
**Per-stock backfill_date:**
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
stocks:
|
|
189
|
+
BOGO:
|
|
190
|
+
pair: BOGO.V
|
|
191
|
+
fmt_base: BOGO
|
|
192
|
+
backfill_date: '2025-06-01'
|
|
193
|
+
```
|
|
194
|
+
|
|
185
195
|
**Default backfill_date:**
|
|
186
196
|
|
|
187
|
-
- If
|
|
197
|
+
- If no `backfill_date` is configured: January 1st of current year
|
|
188
198
|
- Example: In 2026, default is `2026-01-01`
|
|
189
199
|
|
|
190
|
-
**Why per-
|
|
200
|
+
**Why per-ticker?**
|
|
191
201
|
|
|
192
202
|
- Different assets have different availability histories
|
|
193
203
|
- Cryptocurrencies may have shorter histories
|
|
@@ -218,7 +228,7 @@ The tool **always fetches up to yesterday**, never today.
|
|
|
218
228
|
|
|
219
229
|
**Backfill mode:**
|
|
220
230
|
|
|
221
|
-
- Start date =
|
|
231
|
+
- Start date = ticker's `backfill_date` (or default: Jan 1 of current year)
|
|
222
232
|
- Fetches range from start to yesterday
|
|
223
233
|
|
|
224
234
|
### Date Filtering
|
|
@@ -299,6 +309,12 @@ P 2026-01-16 EUR 0.943 CHF
|
|
|
299
309
|
P 2026-02-21 EUR 0.944 CHF
|
|
300
310
|
```
|
|
301
311
|
|
|
312
|
+
```
|
|
313
|
+
# ledger/stocks/ag.to.journal
|
|
314
|
+
P 2026-01-15 00:00:00 "AG.TO" 10.05 CAD
|
|
315
|
+
P 2026-01-16 00:00:00 "AG.TO" 10.07 CAD
|
|
316
|
+
```
|
|
317
|
+
|
|
302
318
|
### Price Line Format
|
|
303
319
|
|
|
304
320
|
**Format:** `P date commodity price base-currency`
|
|
@@ -307,33 +323,40 @@ P 2026-02-21 EUR 0.944 CHF
|
|
|
307
323
|
|
|
308
324
|
- `P` = Price directive (hledger syntax)
|
|
309
325
|
- `date` = YYYY-MM-DD (may include timestamp HH:MM:SS)
|
|
310
|
-
- `commodity` = Currency being priced (e.g., EUR, USD, BTC)
|
|
311
|
-
- `price` = Exchange rate value
|
|
312
|
-
- `base-currency` = Base currency for conversion (e.g., CHF)
|
|
326
|
+
- `commodity` = Currency/stock being priced (e.g., EUR, USD, BTC, "AG.TO")
|
|
327
|
+
- `price` = Exchange rate or price value
|
|
328
|
+
- `base-currency` = Base currency for conversion (e.g., CHF, CAD, USD)
|
|
313
329
|
|
|
314
|
-
**
|
|
330
|
+
**Examples:**
|
|
315
331
|
|
|
316
332
|
```
|
|
317
333
|
P 2026-02-21 EUR 0.944 CHF
|
|
334
|
+
P 2026-02-21 00:00:00 "AG.TO" 10.07 CAD
|
|
335
|
+
P 2026-02-21 00:00:00 DRLL 28.77 USD
|
|
318
336
|
```
|
|
319
337
|
|
|
320
|
-
|
|
338
|
+
### Symbol Quoting
|
|
339
|
+
|
|
340
|
+
Stock symbols containing dots (e.g., `AG.TO`, `BRC.V`) are automatically quoted with double quotes for hledger compatibility. Symbols without dots (e.g., `DRLL`, `GBTC`) remain unquoted.
|
|
321
341
|
|
|
322
342
|
### File Locations
|
|
323
343
|
|
|
324
|
-
-
|
|
325
|
-
-
|
|
344
|
+
- Currency price journals: `ledger/currencies/` (one file per currency)
|
|
345
|
+
- Stock price journals: `ledger/stocks/` (one file per stock)
|
|
326
346
|
- Files updated in place (existing prices preserved, new ones added/merged)
|
|
327
347
|
|
|
328
348
|
**Example structure:**
|
|
329
349
|
|
|
330
350
|
```
|
|
331
351
|
ledger/
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
352
|
+
├── currencies/
|
|
353
|
+
│ ├── eur-chf.journal
|
|
354
|
+
│ ├── usd-chf.journal
|
|
355
|
+
│ └── btc-chf.journal
|
|
356
|
+
└── stocks/
|
|
357
|
+
├── ag.to.journal
|
|
358
|
+
├── drll.journal
|
|
359
|
+
└── bogo.journal
|
|
337
360
|
```
|
|
338
361
|
|
|
339
362
|
## Typical Workflow
|
|
@@ -342,11 +365,12 @@ ledger/
|
|
|
342
365
|
|
|
343
366
|
**Goal:** Keep prices current with daily/weekly updates
|
|
344
367
|
|
|
345
|
-
1. Run `fetch-
|
|
368
|
+
1. Run `fetch-prices()` (or `fetch-prices(backfill: false)`)
|
|
346
369
|
2. Check output for any errors
|
|
347
370
|
3. Verify prices were added:
|
|
348
371
|
```bash
|
|
349
|
-
tail -3 ledger/currencies/eur.journal
|
|
372
|
+
tail -3 ledger/currencies/eur-chf.journal
|
|
373
|
+
tail -3 ledger/stocks/ag.to.journal
|
|
350
374
|
```
|
|
351
375
|
4. Success: Latest prices now available for hledger
|
|
352
376
|
|
|
@@ -366,39 +390,55 @@ ledger/
|
|
|
366
390
|
file: gbp.journal
|
|
367
391
|
backfill_date: '2024-01-01'
|
|
368
392
|
```
|
|
369
|
-
2. Run `fetch-
|
|
393
|
+
2. Run `fetch-prices(backfill: true)` to fetch historical data
|
|
370
394
|
3. Check output and verify `ledger/currencies/gbp.journal` created
|
|
371
395
|
4. Inspect file to confirm date range:
|
|
372
396
|
```bash
|
|
373
397
|
head -3 ledger/currencies/gbp.journal
|
|
374
398
|
tail -3 ledger/currencies/gbp.journal
|
|
375
399
|
```
|
|
376
|
-
5. Subsequent updates: use daily mode (`fetch-
|
|
400
|
+
5. Subsequent updates: use daily mode (`fetch-prices()`)
|
|
377
401
|
|
|
378
|
-
### Scenario 3:
|
|
402
|
+
### Scenario 3: Adding a New Stock
|
|
403
|
+
|
|
404
|
+
**Goal:** Add a new stock ticker
|
|
405
|
+
|
|
406
|
+
1. Add stock to `config/prices.yaml`:
|
|
407
|
+
```yaml
|
|
408
|
+
stocks:
|
|
409
|
+
AAPL: # Defaults: source=yahoo, pair=AAPL, file=aapl.journal
|
|
410
|
+
BOGO: # Custom pair and fmt_base for TSXV listing
|
|
411
|
+
pair: BOGO.V
|
|
412
|
+
fmt_base: BOGO
|
|
413
|
+
```
|
|
414
|
+
2. Run `fetch-prices(backfill: true)` to fetch historical data
|
|
415
|
+
3. Check output and verify `ledger/stocks/aapl.journal` created
|
|
416
|
+
4. Subsequent updates: use daily mode (`fetch-prices()`)
|
|
417
|
+
|
|
418
|
+
### Scenario 4: Fixing Missing Dates
|
|
379
419
|
|
|
380
420
|
**Goal:** Fill gaps in existing price history
|
|
381
421
|
|
|
382
422
|
1. Identify date gaps in price journals:
|
|
383
423
|
```bash
|
|
384
|
-
cat ledger/currencies/eur.journal
|
|
424
|
+
cat ledger/currencies/eur-chf.journal
|
|
385
425
|
# Notice: prices for Feb 15-20 are missing
|
|
386
426
|
```
|
|
387
|
-
2. Run `fetch-
|
|
427
|
+
2. Run `fetch-prices(backfill: true)` to fill gaps
|
|
388
428
|
3. Deduplication ensures:
|
|
389
429
|
- Existing prices preserved
|
|
390
430
|
- Missing dates added
|
|
391
431
|
- No duplicate entries
|
|
392
432
|
4. Verify gaps are filled:
|
|
393
433
|
```bash
|
|
394
|
-
cat ledger/currencies/eur.journal | grep "2026-02"
|
|
434
|
+
cat ledger/currencies/eur-chf.journal | grep "2026-02"
|
|
395
435
|
```
|
|
396
436
|
|
|
397
|
-
### Scenario
|
|
437
|
+
### Scenario 5: Handling Errors
|
|
398
438
|
|
|
399
439
|
**Goal:** Recover from partial failures
|
|
400
440
|
|
|
401
|
-
1. Run `fetch-
|
|
441
|
+
1. Run `fetch-prices()`
|
|
402
442
|
2. Output shows `success: false` with partial results:
|
|
403
443
|
```json
|
|
404
444
|
{
|
|
@@ -409,7 +449,7 @@ ledger/
|
|
|
409
449
|
]
|
|
410
450
|
}
|
|
411
451
|
```
|
|
412
|
-
3. Check error messages for each failed
|
|
452
|
+
3. Check error messages for each failed ticker
|
|
413
453
|
4. Common fixes:
|
|
414
454
|
- **Network issues**: Retry later
|
|
415
455
|
- **API rate limits**: Wait (usually resets hourly/daily) and retry
|
|
@@ -417,7 +457,7 @@ ledger/
|
|
|
417
457
|
- **Missing pricehist**: Install external dependency
|
|
418
458
|
5. Re-run tool (idempotent - safe to retry)
|
|
419
459
|
|
|
420
|
-
**Note:** Successful
|
|
460
|
+
**Note:** Successful tickers are already updated. Only failed tickers need retry.
|
|
421
461
|
|
|
422
462
|
## Configuration
|
|
423
463
|
|
|
@@ -428,14 +468,23 @@ ledger/
|
|
|
428
468
|
```yaml
|
|
429
469
|
currencies:
|
|
430
470
|
<TICKER>:
|
|
431
|
-
source: <source-name>
|
|
432
|
-
pair: <trading-pair>
|
|
433
|
-
file: <journal-filename>
|
|
434
|
-
fmt_base: <base-currency>
|
|
435
|
-
backfill_date: <YYYY-MM-DD>
|
|
471
|
+
source: <source-name> # Required: e.g., yahoo, coinmarketcap, ecb
|
|
472
|
+
pair: <trading-pair> # Required: source-specific format
|
|
473
|
+
file: <journal-filename> # Required: e.g., eur-chf.journal
|
|
474
|
+
fmt_base: <base-currency> # Optional: e.g., CHF, USD
|
|
475
|
+
backfill_date: <YYYY-MM-DD> # Optional: per-currency backfill start
|
|
476
|
+
|
|
477
|
+
stocks:
|
|
478
|
+
<TICKER>: # Null value → all defaults (source=yahoo, pair=key, file=key.lower.journal)
|
|
479
|
+
<TICKER>:
|
|
480
|
+
pair: <exchange-ticker> # Optional: override pair (e.g., BOGO.V for TSXV listing)
|
|
481
|
+
fmt_base: <symbol> # Optional: rename symbol in output (e.g., BOGO)
|
|
482
|
+
source: <source-name> # Optional: default yahoo
|
|
483
|
+
file: <journal-filename> # Optional: default {key.lowercase}.journal
|
|
484
|
+
backfill_date: <YYYY-MM-DD> # Optional: per-stock backfill start
|
|
436
485
|
```
|
|
437
486
|
|
|
438
|
-
### Field Descriptions
|
|
487
|
+
### Currency Field Descriptions
|
|
439
488
|
|
|
440
489
|
| Field | Required | Description |
|
|
441
490
|
| --------------- | -------- | -------------------------------------------------------------- |
|
|
@@ -445,6 +494,16 @@ currencies:
|
|
|
445
494
|
| `fmt_base` | No | Base currency for price notation (default: inferred from pair) |
|
|
446
495
|
| `backfill_date` | No | Override default backfill start date for this currency |
|
|
447
496
|
|
|
497
|
+
### Stock Field Descriptions
|
|
498
|
+
|
|
499
|
+
| Field | Default | Description |
|
|
500
|
+
| --------------- | ---------------------------- | ---------------------------------------------------------------- |
|
|
501
|
+
| `source` | `yahoo` | Price data source |
|
|
502
|
+
| `pair` | the ticker key itself | Ticker/pair to fetch (useful when exchange ticker differs) |
|
|
503
|
+
| `file` | `{lowercase key}.journal` | Journal filename in `ledger/stocks/` |
|
|
504
|
+
| `fmt_base` | — | Symbol override in output (e.g., `BOGO` when pair is `BOGO.V`) |
|
|
505
|
+
| `backfill_date` | — | Override default backfill start date for this stock |
|
|
506
|
+
|
|
448
507
|
### Configuration Examples
|
|
449
508
|
|
|
450
509
|
**Fiat currencies (Yahoo Finance):**
|
|
@@ -494,6 +553,17 @@ currencies:
|
|
|
494
553
|
file: eur.journal
|
|
495
554
|
```
|
|
496
555
|
|
|
556
|
+
**Stocks (Yahoo Finance):**
|
|
557
|
+
|
|
558
|
+
```yaml
|
|
559
|
+
stocks:
|
|
560
|
+
AG.TO: # Defaults: yahoo, pair=AG.TO, file=ag.to.journal
|
|
561
|
+
DRLL: # Defaults: yahoo, pair=DRLL, file=drll.journal
|
|
562
|
+
BOGO: # Custom: fetches BOGO.V, outputs as BOGO
|
|
563
|
+
pair: BOGO.V
|
|
564
|
+
fmt_base: BOGO
|
|
565
|
+
```
|
|
566
|
+
|
|
497
567
|
### External Dependency: pricehist
|
|
498
568
|
|
|
499
569
|
The tool uses the `pricehist` command-line tool to fetch price data.
|
|
@@ -520,19 +590,19 @@ The tool uses the `pricehist` command-line tool to fetch price data.
|
|
|
520
590
|
| Configuration error | Missing or invalid `config/prices.yaml` | Ensure config file exists with proper YAML syntax and required fields |
|
|
521
591
|
| Invalid date range | Start date after end date | Check `backfill_date` configuration; must be before yesterday |
|
|
522
592
|
| Agent restriction | Called by wrong agent | Use `Task(subagent_type='accountant', prompt='update prices')` |
|
|
523
|
-
| Permission error | Cannot write to journal files | Check file permissions on `ledger/currencies/`
|
|
593
|
+
| Permission error | Cannot write to journal files | Check file permissions on `ledger/currencies/` and `ledger/stocks/` dirs |
|
|
524
594
|
| Invalid source/pair | Source or pair format incorrect | Check `pricehist` docs for correct format for your source |
|
|
525
595
|
|
|
526
596
|
### Partial Failures
|
|
527
597
|
|
|
528
|
-
The tool processes all currencies **independently**.
|
|
598
|
+
The tool processes all currencies and stocks **independently**.
|
|
529
599
|
|
|
530
600
|
**Behavior:**
|
|
531
601
|
|
|
532
|
-
- Each
|
|
533
|
-
- Failure in one
|
|
534
|
-
- Overall `success: false` if ANY
|
|
535
|
-
- Check `results` array for per-
|
|
602
|
+
- Each ticker is fetched and updated separately
|
|
603
|
+
- Failure in one ticker doesn't affect others
|
|
604
|
+
- Overall `success: false` if ANY ticker fails
|
|
605
|
+
- Check `results` array for per-ticker status
|
|
536
606
|
|
|
537
607
|
**Example:**
|
|
538
608
|
|
|
@@ -540,19 +610,20 @@ The tool processes all currencies **independently**.
|
|
|
540
610
|
{
|
|
541
611
|
"success": false,
|
|
542
612
|
"results": [
|
|
543
|
-
{ "ticker": "EUR", "priceLine": "P 2026-02-21 EUR 0.944 CHF", "file": "eur.journal" },
|
|
544
|
-
{ "ticker": "BTC", "error": "API rate limit exceeded" }
|
|
613
|
+
{ "ticker": "EUR", "priceLine": "P 2026-02-21 EUR 0.944 CHF", "file": "eur-chf.journal" },
|
|
614
|
+
{ "ticker": "BTC", "error": "API rate limit exceeded" },
|
|
615
|
+
{ "ticker": "AG.TO", "priceLine": "P 2026-02-21 \"AG.TO\" 10.07 CAD", "file": "ag.to.journal" }
|
|
545
616
|
]
|
|
546
617
|
}
|
|
547
618
|
```
|
|
548
619
|
|
|
549
|
-
EUR succeeded (updated), BTC failed (not updated).
|
|
620
|
+
EUR and AG.TO succeeded (updated), BTC failed (not updated).
|
|
550
621
|
|
|
551
622
|
**Recovery:**
|
|
552
623
|
|
|
553
624
|
- Can re-run tool safely (idempotent)
|
|
554
|
-
- Successful
|
|
555
|
-
- Only failed
|
|
625
|
+
- Successful tickers won't re-fetch (deduplication handles this)
|
|
626
|
+
- Only failed tickers will retry
|
|
556
627
|
|
|
557
628
|
### Debugging Tips
|
|
558
629
|
|
|
@@ -560,12 +631,15 @@ EUR succeeded (updated), BTC failed (not updated).
|
|
|
560
631
|
|
|
561
632
|
```bash
|
|
562
633
|
pricehist fetch -o ledger -s 2026-02-21 -e 2026-02-21 yahoo EURCHF=X
|
|
634
|
+
pricehist fetch -o ledger -s 2026-02-21 -e 2026-02-21 yahoo AG.TO
|
|
635
|
+
pricehist fetch -o ledger -s 2026-02-21 -e 2026-02-21 yahoo BOGO.V --fmt-base BOGO
|
|
563
636
|
```
|
|
564
637
|
|
|
565
|
-
**Check journal
|
|
638
|
+
**Check journal files:**
|
|
566
639
|
|
|
567
640
|
```bash
|
|
568
|
-
cat ledger/currencies/eur.journal
|
|
641
|
+
cat ledger/currencies/eur-chf.journal
|
|
642
|
+
cat ledger/stocks/ag.to.journal
|
|
569
643
|
```
|
|
570
644
|
|
|
571
645
|
**Check config syntax:**
|
|
@@ -578,4 +652,5 @@ cat config/prices.yaml
|
|
|
578
652
|
|
|
579
653
|
```bash
|
|
580
654
|
ls -la ledger/currencies/
|
|
655
|
+
ls -la ledger/stocks/
|
|
581
656
|
```
|