@fuzzle/opencode-accountant 0.14.0-next.1 → 0.14.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 +6 -29
- package/agent/accountant.md +10 -11
- package/dist/index.js +80 -73
- package/docs/tools/{fetch-prices.md → fetch-currency-prices.md} +67 -142
- 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-prices` tool requires a configuration file to specify which currency pairs
|
|
19
|
+
The `fetch-currency-prices` tool requires a configuration file to specify which currency pairs 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,16 +38,9 @@ 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
|
|
48
41
|
```
|
|
49
42
|
|
|
50
|
-
####
|
|
43
|
+
#### Configuration Options
|
|
51
44
|
|
|
52
45
|
Each currency entry supports the following fields:
|
|
53
46
|
|
|
@@ -59,18 +52,6 @@ Each currency entry supports the following fields:
|
|
|
59
52
|
| `fmt_base` | No | Base currency format override for pricehist (e.g., `USD` for yahoo source) |
|
|
60
53
|
| `backfill_date` | No | Start date for backfill mode. Defaults to January 1st of the current year |
|
|
61
54
|
|
|
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
|
-
|
|
74
55
|
#### Directory Structure
|
|
75
56
|
|
|
76
57
|
The plugin expects the following directory structure in your project:
|
|
@@ -80,14 +61,10 @@ your-project/
|
|
|
80
61
|
├── config/
|
|
81
62
|
│ └── prices.yaml # Price fetching configuration
|
|
82
63
|
└── ledger/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
└── stocks/ # Where stock price journals are written
|
|
88
|
-
├── ag.to.journal
|
|
89
|
-
├── drll.journal
|
|
90
|
-
└── bogo.journal
|
|
64
|
+
└── currencies/ # Where price journal files are written
|
|
65
|
+
├── btc-chf.journal
|
|
66
|
+
├── eur-chf.journal
|
|
67
|
+
└── usd-chf.journal
|
|
91
68
|
```
|
|
92
69
|
|
|
93
70
|
#### 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-prices, hledger-mcp (read-only queries)
|
|
11
|
+
# MCP tools available: import-pipeline, fetch-currency-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-prices`
|
|
62
|
+
| `fetch-currency-prices` | Fetching exchange rates | `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-prices` tool instead
|
|
87
|
+
- **Fetching prices** - Use `fetch-currency-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,23 +232,22 @@ The following are MCP tools available to you. Always call these tools directly -
|
|
|
232
232
|
|
|
233
233
|
---
|
|
234
234
|
|
|
235
|
-
### fetch-prices
|
|
235
|
+
### fetch-currency-prices
|
|
236
236
|
|
|
237
|
-
**Purpose:** Fetches currency exchange rates and
|
|
237
|
+
**Purpose:** Fetches currency exchange rates and updates `ledger/currencies/` journals.
|
|
238
238
|
|
|
239
239
|
**Usage:**
|
|
240
240
|
|
|
241
|
-
- Daily mode (default): `fetch-prices()` or `fetch-prices(backfill: false)`
|
|
242
|
-
- Backfill mode: `fetch-prices(backfill: true)`
|
|
241
|
+
- Daily mode (default): `fetch-currency-prices()` or `fetch-currency-prices(backfill: false)`
|
|
242
|
+
- Backfill mode: `fetch-currency-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
|
|
250
|
-
- Stock symbols with dots (e.g., `AG.TO`) are automatically quoted for hledger compatibility
|
|
249
|
+
- Processes all currencies independently (partial failures possible)
|
|
251
250
|
|
|
252
|
-
**Output:** Returns per-
|
|
251
|
+
**Output:** Returns per-currency results with latest price line or error message
|
|
253
252
|
|
|
254
|
-
**Configuration:** `config/prices.yaml` defines currencies
|
|
253
|
+
**Configuration:** `config/prices.yaml` defines currencies, sources, pairs, and backfill dates
|
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-prices.ts
|
|
16873
|
+
// src/tools/fetch-currency-prices.ts
|
|
16874
16874
|
var {$ } = globalThis.Bun;
|
|
16875
16875
|
import * as path4 from "path";
|
|
16876
16876
|
|
|
@@ -16939,26 +16939,6 @@ 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
|
-
}
|
|
16962
16942
|
function loadPricesConfig(directory) {
|
|
16963
16943
|
return loadYamlConfig(directory, CONFIG_FILE, (parsedObj) => {
|
|
16964
16944
|
if (!parsedObj.currencies || typeof parsedObj.currencies !== "object") {
|
|
@@ -16972,15 +16952,17 @@ function loadPricesConfig(directory) {
|
|
|
16972
16952
|
for (const [name, config2] of Object.entries(currenciesObj)) {
|
|
16973
16953
|
currencies[name] = validateCurrencyConfig(name, config2);
|
|
16974
16954
|
}
|
|
16975
|
-
|
|
16976
|
-
if (parsedObj.stocks !== undefined
|
|
16977
|
-
if (
|
|
16978
|
-
throw new Error(`Invalid config: 'stocks' must be a
|
|
16955
|
+
let stocks;
|
|
16956
|
+
if (parsedObj.stocks !== undefined) {
|
|
16957
|
+
if (!Array.isArray(parsedObj.stocks) || parsedObj.stocks.length === 0) {
|
|
16958
|
+
throw new Error(`Invalid config: 'stocks' must be a non-empty array`);
|
|
16979
16959
|
}
|
|
16980
|
-
const
|
|
16981
|
-
|
|
16982
|
-
|
|
16960
|
+
for (const [i2, ticker] of parsedObj.stocks.entries()) {
|
|
16961
|
+
if (typeof ticker !== "string" || ticker === "") {
|
|
16962
|
+
throw new Error(`Invalid config: stock entry at index ${i2} must be a non-empty string`);
|
|
16963
|
+
}
|
|
16983
16964
|
}
|
|
16965
|
+
stocks = parsedObj.stocks;
|
|
16984
16966
|
}
|
|
16985
16967
|
return { currencies, stocks };
|
|
16986
16968
|
}, `Configuration file not found: ${CONFIG_FILE}. Please refer to the plugin's GitHub repository for setup instructions.`);
|
|
@@ -17218,12 +17200,12 @@ function buildToolSuccessResult(data) {
|
|
|
17218
17200
|
return JSON.stringify(result);
|
|
17219
17201
|
}
|
|
17220
17202
|
|
|
17221
|
-
// src/tools/fetch-prices.ts
|
|
17203
|
+
// src/tools/fetch-currency-prices.ts
|
|
17222
17204
|
async function defaultPriceFetcher(cmdArgs) {
|
|
17223
17205
|
const result = await $`pricehist ${cmdArgs}`.quiet();
|
|
17224
17206
|
return result.stdout.toString().trim();
|
|
17225
17207
|
}
|
|
17226
|
-
function buildPricehistArgs(startDate, endDate,
|
|
17208
|
+
function buildPricehistArgs(startDate, endDate, currencyConfig) {
|
|
17227
17209
|
const cmdArgs = [
|
|
17228
17210
|
"fetch",
|
|
17229
17211
|
"-o",
|
|
@@ -17232,11 +17214,11 @@ function buildPricehistArgs(startDate, endDate, config2) {
|
|
|
17232
17214
|
startDate,
|
|
17233
17215
|
"-e",
|
|
17234
17216
|
endDate,
|
|
17235
|
-
|
|
17236
|
-
|
|
17217
|
+
currencyConfig.source,
|
|
17218
|
+
currencyConfig.pair
|
|
17237
17219
|
];
|
|
17238
|
-
if (
|
|
17239
|
-
cmdArgs.push("--fmt-base",
|
|
17220
|
+
if (currencyConfig.fmt_base) {
|
|
17221
|
+
cmdArgs.push("--fmt-base", currencyConfig.fmt_base);
|
|
17240
17222
|
}
|
|
17241
17223
|
return cmdArgs;
|
|
17242
17224
|
}
|
|
@@ -17272,36 +17254,8 @@ function filterAndSortPriceLinesByDateRange(priceLines, startDate, endDate) {
|
|
|
17272
17254
|
return parsed.date >= startDate && parsed.date <= endDate;
|
|
17273
17255
|
}).sort((a, b) => a.date.localeCompare(b.date)).map((parsed) => parsed.formattedLine);
|
|
17274
17256
|
}
|
|
17275
|
-
async function
|
|
17276
|
-
const
|
|
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");
|
|
17257
|
+
async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = defaultPriceFetcher, configLoader = loadPricesConfig) {
|
|
17258
|
+
const restrictionError = checkAccountantAgent(agent, "fetch currency prices");
|
|
17305
17259
|
if (restrictionError) {
|
|
17306
17260
|
return restrictionError;
|
|
17307
17261
|
}
|
|
@@ -17317,8 +17271,34 @@ async function fetchPrices(directory, agent, backfill, priceFetcher = defaultPri
|
|
|
17317
17271
|
const results = [];
|
|
17318
17272
|
for (const [ticker, currencyConfig] of Object.entries(config2.currencies)) {
|
|
17319
17273
|
try {
|
|
17320
|
-
const
|
|
17321
|
-
|
|
17274
|
+
const startDate = backfill ? currencyConfig.backfill_date || defaultBackfillDate : endDate;
|
|
17275
|
+
const cmdArgs = buildPricehistArgs(startDate, endDate, currencyConfig);
|
|
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
|
+
});
|
|
17322
17302
|
} catch (err) {
|
|
17323
17303
|
results.push({
|
|
17324
17304
|
ticker,
|
|
@@ -17326,10 +17306,37 @@ async function fetchPrices(directory, agent, backfill, priceFetcher = defaultPri
|
|
|
17326
17306
|
});
|
|
17327
17307
|
}
|
|
17328
17308
|
}
|
|
17329
|
-
for (const
|
|
17309
|
+
for (const ticker of config2.stocks ?? []) {
|
|
17330
17310
|
try {
|
|
17331
|
-
const
|
|
17332
|
-
|
|
17311
|
+
const startDate = backfill ? defaultBackfillDate : endDate;
|
|
17312
|
+
const cmdArgs = ["fetch", "-o", "ledger", "-s", startDate, "-e", endDate, "yahoo", ticker];
|
|
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
|
+
});
|
|
17333
17340
|
} catch (err) {
|
|
17334
17341
|
results.push({
|
|
17335
17342
|
ticker,
|
|
@@ -17339,15 +17346,15 @@ async function fetchPrices(directory, agent, backfill, priceFetcher = defaultPri
|
|
|
17339
17346
|
}
|
|
17340
17347
|
return buildSuccessResult(results, endDate, backfill);
|
|
17341
17348
|
}
|
|
17342
|
-
var
|
|
17349
|
+
var fetch_currency_prices_default = tool({
|
|
17343
17350
|
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/.",
|
|
17344
17351
|
args: {
|
|
17345
|
-
backfill: tool.schema.boolean().optional().describe("If true, fetch history from each currency
|
|
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)")
|
|
17346
17353
|
},
|
|
17347
17354
|
async execute(params, context) {
|
|
17348
17355
|
const { directory, agent } = context;
|
|
17349
17356
|
const { backfill } = params;
|
|
17350
|
-
return
|
|
17357
|
+
return fetchCurrencyPrices(directory, agent, backfill || false);
|
|
17351
17358
|
}
|
|
17352
17359
|
});
|
|
17353
17360
|
// src/tools/classify-statements.ts
|
|
@@ -27810,7 +27817,7 @@ var AccountantPlugin = async () => {
|
|
|
27810
27817
|
const agent = loadAgent(AGENT_FILE);
|
|
27811
27818
|
return {
|
|
27812
27819
|
tool: {
|
|
27813
|
-
"fetch-prices":
|
|
27820
|
+
"fetch-currency-prices": fetch_currency_prices_default,
|
|
27814
27821
|
"classify-statements": classify_statements_default,
|
|
27815
27822
|
"import-statements": import_statements_default,
|
|
27816
27823
|
"reconcile-statements": reconcile_statement_default,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# fetch-prices Tool
|
|
1
|
+
# fetch-currency-prices Tool
|
|
2
2
|
|
|
3
|
-
The `fetch-prices` tool fetches end-of-day currency exchange rates and
|
|
3
|
+
The `fetch-currency-prices` tool fetches end-of-day currency exchange rates and updates the price journals in `ledger/currencies/`. 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": "BTC",
|
|
37
|
+
"priceLine": "P 2026-02-21 BTC 52341.50 CHF",
|
|
38
|
+
"file": "btc.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 currency is fetched.
|
|
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": "USD",
|
|
63
|
+
"priceLine": "P 2026-02-21 USD 0.881 CHF",
|
|
64
|
+
"file": "usd.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 currencies succeed and others fail:
|
|
75
75
|
|
|
76
76
|
```json
|
|
77
77
|
{
|
|
@@ -96,7 +96,7 @@ When some tickers succeed and others fail:
|
|
|
96
96
|
}
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
The tool processes all currencies
|
|
99
|
+
The tool processes all currencies 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
|
|
130
|
+
- Fast execution (single date per currency)
|
|
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-prices()
|
|
138
|
+
fetch-currency-prices()
|
|
139
139
|
# or
|
|
140
|
-
fetch-prices(backfill: false)
|
|
140
|
+
fetch-currency-prices(backfill: false)
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
**Use when:**
|
|
@@ -151,22 +151,22 @@ fetch-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
|
|
154
|
+
- Slower execution (multiple dates per currency)
|
|
155
|
+
- Typical use: Initial setup, adding new currency, filling gaps
|
|
156
156
|
|
|
157
|
-
**Date range:** Per-
|
|
157
|
+
**Date range:** Per-currency `backfill_date` (or default) to yesterday
|
|
158
158
|
|
|
159
159
|
**Example:**
|
|
160
160
|
|
|
161
161
|
```
|
|
162
|
-
fetch-prices(backfill: true)
|
|
162
|
+
fetch-currency-prices(backfill: true)
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
**Use when:**
|
|
166
166
|
|
|
167
|
-
- Adding a new currency
|
|
167
|
+
- Adding a new currency (populate full history)
|
|
168
168
|
- Fixing missing dates (fill gaps)
|
|
169
|
-
- Initial repository setup (populate all
|
|
169
|
+
- Initial repository setup (populate all currencies)
|
|
170
170
|
- Recovering from extended outage
|
|
171
171
|
|
|
172
172
|
### Backfill Date Configuration
|
|
@@ -182,22 +182,12 @@ 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
|
-
|
|
195
185
|
**Default backfill_date:**
|
|
196
186
|
|
|
197
|
-
- If no `backfill_date`
|
|
187
|
+
- If currency has no `backfill_date` configured: January 1st of current year
|
|
198
188
|
- Example: In 2026, default is `2026-01-01`
|
|
199
189
|
|
|
200
|
-
**Why per-
|
|
190
|
+
**Why per-currency?**
|
|
201
191
|
|
|
202
192
|
- Different assets have different availability histories
|
|
203
193
|
- Cryptocurrencies may have shorter histories
|
|
@@ -228,7 +218,7 @@ The tool **always fetches up to yesterday**, never today.
|
|
|
228
218
|
|
|
229
219
|
**Backfill mode:**
|
|
230
220
|
|
|
231
|
-
- Start date =
|
|
221
|
+
- Start date = currency's `backfill_date` (or default: Jan 1 of current year)
|
|
232
222
|
- Fetches range from start to yesterday
|
|
233
223
|
|
|
234
224
|
### Date Filtering
|
|
@@ -309,12 +299,6 @@ P 2026-01-16 EUR 0.943 CHF
|
|
|
309
299
|
P 2026-02-21 EUR 0.944 CHF
|
|
310
300
|
```
|
|
311
301
|
|
|
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
|
-
|
|
318
302
|
### Price Line Format
|
|
319
303
|
|
|
320
304
|
**Format:** `P date commodity price base-currency`
|
|
@@ -323,40 +307,33 @@ P 2026-01-16 00:00:00 "AG.TO" 10.07 CAD
|
|
|
323
307
|
|
|
324
308
|
- `P` = Price directive (hledger syntax)
|
|
325
309
|
- `date` = YYYY-MM-DD (may include timestamp HH:MM:SS)
|
|
326
|
-
- `commodity` = Currency
|
|
327
|
-
- `price` = Exchange rate
|
|
328
|
-
- `base-currency` = Base currency for conversion (e.g., CHF
|
|
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)
|
|
329
313
|
|
|
330
|
-
**
|
|
314
|
+
**Example:**
|
|
331
315
|
|
|
332
316
|
```
|
|
333
317
|
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
|
|
336
318
|
```
|
|
337
319
|
|
|
338
|
-
|
|
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.
|
|
320
|
+
Means: 1 EUR = 0.944 CHF on 2026-02-21
|
|
341
321
|
|
|
342
322
|
### File Locations
|
|
343
323
|
|
|
344
|
-
-
|
|
345
|
-
-
|
|
324
|
+
- All price journals stored in `ledger/currencies/`
|
|
325
|
+
- One file per currency (configured in `config/prices.yaml`)
|
|
346
326
|
- Files updated in place (existing prices preserved, new ones added/merged)
|
|
347
327
|
|
|
348
328
|
**Example structure:**
|
|
349
329
|
|
|
350
330
|
```
|
|
351
331
|
ledger/
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
└──
|
|
357
|
-
├── ag.to.journal
|
|
358
|
-
├── drll.journal
|
|
359
|
-
└── bogo.journal
|
|
332
|
+
└── currencies/
|
|
333
|
+
├── eur.journal
|
|
334
|
+
├── usd.journal
|
|
335
|
+
├── btc.journal
|
|
336
|
+
└── eth.journal
|
|
360
337
|
```
|
|
361
338
|
|
|
362
339
|
## Typical Workflow
|
|
@@ -365,12 +342,11 @@ ledger/
|
|
|
365
342
|
|
|
366
343
|
**Goal:** Keep prices current with daily/weekly updates
|
|
367
344
|
|
|
368
|
-
1. Run `fetch-prices()` (or `fetch-prices(backfill: false)`)
|
|
345
|
+
1. Run `fetch-currency-prices()` (or `fetch-currency-prices(backfill: false)`)
|
|
369
346
|
2. Check output for any errors
|
|
370
347
|
3. Verify prices were added:
|
|
371
348
|
```bash
|
|
372
|
-
tail -3 ledger/currencies/eur
|
|
373
|
-
tail -3 ledger/stocks/ag.to.journal
|
|
349
|
+
tail -3 ledger/currencies/eur.journal
|
|
374
350
|
```
|
|
375
351
|
4. Success: Latest prices now available for hledger
|
|
376
352
|
|
|
@@ -390,55 +366,39 @@ ledger/
|
|
|
390
366
|
file: gbp.journal
|
|
391
367
|
backfill_date: '2024-01-01'
|
|
392
368
|
```
|
|
393
|
-
2. Run `fetch-prices(backfill: true)` to fetch historical data
|
|
369
|
+
2. Run `fetch-currency-prices(backfill: true)` to fetch historical data
|
|
394
370
|
3. Check output and verify `ledger/currencies/gbp.journal` created
|
|
395
371
|
4. Inspect file to confirm date range:
|
|
396
372
|
```bash
|
|
397
373
|
head -3 ledger/currencies/gbp.journal
|
|
398
374
|
tail -3 ledger/currencies/gbp.journal
|
|
399
375
|
```
|
|
400
|
-
5. Subsequent updates: use daily mode (`fetch-prices()`)
|
|
376
|
+
5. Subsequent updates: use daily mode (`fetch-currency-prices()`)
|
|
401
377
|
|
|
402
|
-
### Scenario 3:
|
|
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
|
|
378
|
+
### Scenario 3: Fixing Missing Dates
|
|
419
379
|
|
|
420
380
|
**Goal:** Fill gaps in existing price history
|
|
421
381
|
|
|
422
382
|
1. Identify date gaps in price journals:
|
|
423
383
|
```bash
|
|
424
|
-
cat ledger/currencies/eur
|
|
384
|
+
cat ledger/currencies/eur.journal
|
|
425
385
|
# Notice: prices for Feb 15-20 are missing
|
|
426
386
|
```
|
|
427
|
-
2. Run `fetch-prices(backfill: true)` to fill gaps
|
|
387
|
+
2. Run `fetch-currency-prices(backfill: true)` to fill gaps
|
|
428
388
|
3. Deduplication ensures:
|
|
429
389
|
- Existing prices preserved
|
|
430
390
|
- Missing dates added
|
|
431
391
|
- No duplicate entries
|
|
432
392
|
4. Verify gaps are filled:
|
|
433
393
|
```bash
|
|
434
|
-
cat ledger/currencies/eur
|
|
394
|
+
cat ledger/currencies/eur.journal | grep "2026-02"
|
|
435
395
|
```
|
|
436
396
|
|
|
437
|
-
### Scenario
|
|
397
|
+
### Scenario 4: Handling Errors
|
|
438
398
|
|
|
439
399
|
**Goal:** Recover from partial failures
|
|
440
400
|
|
|
441
|
-
1. Run `fetch-prices()`
|
|
401
|
+
1. Run `fetch-currency-prices()`
|
|
442
402
|
2. Output shows `success: false` with partial results:
|
|
443
403
|
```json
|
|
444
404
|
{
|
|
@@ -449,7 +409,7 @@ ledger/
|
|
|
449
409
|
]
|
|
450
410
|
}
|
|
451
411
|
```
|
|
452
|
-
3. Check error messages for each failed
|
|
412
|
+
3. Check error messages for each failed currency
|
|
453
413
|
4. Common fixes:
|
|
454
414
|
- **Network issues**: Retry later
|
|
455
415
|
- **API rate limits**: Wait (usually resets hourly/daily) and retry
|
|
@@ -457,7 +417,7 @@ ledger/
|
|
|
457
417
|
- **Missing pricehist**: Install external dependency
|
|
458
418
|
5. Re-run tool (idempotent - safe to retry)
|
|
459
419
|
|
|
460
|
-
**Note:** Successful
|
|
420
|
+
**Note:** Successful currencies are already updated. Only failed currencies need retry.
|
|
461
421
|
|
|
462
422
|
## Configuration
|
|
463
423
|
|
|
@@ -468,23 +428,14 @@ ledger/
|
|
|
468
428
|
```yaml
|
|
469
429
|
currencies:
|
|
470
430
|
<TICKER>:
|
|
471
|
-
source: <source-name>
|
|
472
|
-
pair: <trading-pair>
|
|
473
|
-
file: <journal-filename>
|
|
474
|
-
fmt_base: <base-currency>
|
|
475
|
-
backfill_date: <YYYY-MM-DD>
|
|
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
|
|
431
|
+
source: <source-name> # e.g., yahoo, coinbase, coinmarketcap, ecb
|
|
432
|
+
pair: <trading-pair> # Source-specific format
|
|
433
|
+
file: <journal-filename> # e.g., eur.journal
|
|
434
|
+
fmt_base: <base-currency> # Optional, e.g., CHF, USD
|
|
435
|
+
backfill_date: <YYYY-MM-DD> # Optional, per-currency backfill start
|
|
485
436
|
```
|
|
486
437
|
|
|
487
|
-
###
|
|
438
|
+
### Field Descriptions
|
|
488
439
|
|
|
489
440
|
| Field | Required | Description |
|
|
490
441
|
| --------------- | -------- | -------------------------------------------------------------- |
|
|
@@ -494,16 +445,6 @@ stocks:
|
|
|
494
445
|
| `fmt_base` | No | Base currency for price notation (default: inferred from pair) |
|
|
495
446
|
| `backfill_date` | No | Override default backfill start date for this currency |
|
|
496
447
|
|
|
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
|
-
|
|
507
448
|
### Configuration Examples
|
|
508
449
|
|
|
509
450
|
**Fiat currencies (Yahoo Finance):**
|
|
@@ -553,17 +494,6 @@ currencies:
|
|
|
553
494
|
file: eur.journal
|
|
554
495
|
```
|
|
555
496
|
|
|
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
|
-
|
|
567
497
|
### External Dependency: pricehist
|
|
568
498
|
|
|
569
499
|
The tool uses the `pricehist` command-line tool to fetch price data.
|
|
@@ -590,19 +520,19 @@ The tool uses the `pricehist` command-line tool to fetch price data.
|
|
|
590
520
|
| Configuration error | Missing or invalid `config/prices.yaml` | Ensure config file exists with proper YAML syntax and required fields |
|
|
591
521
|
| Invalid date range | Start date after end date | Check `backfill_date` configuration; must be before yesterday |
|
|
592
522
|
| Agent restriction | Called by wrong agent | Use `Task(subagent_type='accountant', prompt='update prices')` |
|
|
593
|
-
| Permission error | Cannot write to journal files | Check file permissions on `ledger/currencies/`
|
|
523
|
+
| Permission error | Cannot write to journal files | Check file permissions on `ledger/currencies/` directory |
|
|
594
524
|
| Invalid source/pair | Source or pair format incorrect | Check `pricehist` docs for correct format for your source |
|
|
595
525
|
|
|
596
526
|
### Partial Failures
|
|
597
527
|
|
|
598
|
-
The tool processes all currencies
|
|
528
|
+
The tool processes all currencies **independently**.
|
|
599
529
|
|
|
600
530
|
**Behavior:**
|
|
601
531
|
|
|
602
|
-
- Each
|
|
603
|
-
- Failure in one
|
|
604
|
-
- Overall `success: false` if ANY
|
|
605
|
-
- Check `results` array for per-
|
|
532
|
+
- Each currency is fetched and updated separately
|
|
533
|
+
- Failure in one currency doesn't affect others
|
|
534
|
+
- Overall `success: false` if ANY currency fails
|
|
535
|
+
- Check `results` array for per-currency status
|
|
606
536
|
|
|
607
537
|
**Example:**
|
|
608
538
|
|
|
@@ -610,20 +540,19 @@ The tool processes all currencies and stocks **independently**.
|
|
|
610
540
|
{
|
|
611
541
|
"success": false,
|
|
612
542
|
"results": [
|
|
613
|
-
{ "ticker": "EUR", "priceLine": "P 2026-02-21 EUR 0.944 CHF", "file": "eur
|
|
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" }
|
|
543
|
+
{ "ticker": "EUR", "priceLine": "P 2026-02-21 EUR 0.944 CHF", "file": "eur.journal" },
|
|
544
|
+
{ "ticker": "BTC", "error": "API rate limit exceeded" }
|
|
616
545
|
]
|
|
617
546
|
}
|
|
618
547
|
```
|
|
619
548
|
|
|
620
|
-
EUR
|
|
549
|
+
EUR succeeded (updated), BTC failed (not updated).
|
|
621
550
|
|
|
622
551
|
**Recovery:**
|
|
623
552
|
|
|
624
553
|
- Can re-run tool safely (idempotent)
|
|
625
|
-
- Successful
|
|
626
|
-
- Only failed
|
|
554
|
+
- Successful currencies won't re-fetch (deduplication handles this)
|
|
555
|
+
- Only failed currencies will retry
|
|
627
556
|
|
|
628
557
|
### Debugging Tips
|
|
629
558
|
|
|
@@ -631,15 +560,12 @@ EUR and AG.TO succeeded (updated), BTC failed (not updated).
|
|
|
631
560
|
|
|
632
561
|
```bash
|
|
633
562
|
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
|
|
636
563
|
```
|
|
637
564
|
|
|
638
|
-
**Check journal
|
|
565
|
+
**Check journal file:**
|
|
639
566
|
|
|
640
567
|
```bash
|
|
641
|
-
cat ledger/currencies/eur
|
|
642
|
-
cat ledger/stocks/ag.to.journal
|
|
568
|
+
cat ledger/currencies/eur.journal
|
|
643
569
|
```
|
|
644
570
|
|
|
645
571
|
**Check config syntax:**
|
|
@@ -652,5 +578,4 @@ cat config/prices.yaml
|
|
|
652
578
|
|
|
653
579
|
```bash
|
|
654
580
|
ls -la ledger/currencies/
|
|
655
|
-
ls -la ledger/stocks/
|
|
656
581
|
```
|