@fuzzle/opencode-accountant 0.13.21 → 0.14.0-next.1
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 +91 -40
- 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,7 +16972,17 @@ function loadPricesConfig(directory) {
|
|
|
16952
16972
|
for (const [name, config2] of Object.entries(currenciesObj)) {
|
|
16953
16973
|
currencies[name] = validateCurrencyConfig(name, config2);
|
|
16954
16974
|
}
|
|
16955
|
-
|
|
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`);
|
|
16979
|
+
}
|
|
16980
|
+
const stocksObj = parsedObj.stocks;
|
|
16981
|
+
for (const [ticker, raw] of Object.entries(stocksObj)) {
|
|
16982
|
+
stocks[ticker] = normalizeStockConfig(ticker, raw);
|
|
16983
|
+
}
|
|
16984
|
+
}
|
|
16985
|
+
return { currencies, stocks };
|
|
16956
16986
|
}, `Configuration file not found: ${CONFIG_FILE}. Please refer to the plugin's GitHub repository for setup instructions.`);
|
|
16957
16987
|
}
|
|
16958
16988
|
|
|
@@ -17188,12 +17218,12 @@ function buildToolSuccessResult(data) {
|
|
|
17188
17218
|
return JSON.stringify(result);
|
|
17189
17219
|
}
|
|
17190
17220
|
|
|
17191
|
-
// src/tools/fetch-
|
|
17221
|
+
// src/tools/fetch-prices.ts
|
|
17192
17222
|
async function defaultPriceFetcher(cmdArgs) {
|
|
17193
17223
|
const result = await $`pricehist ${cmdArgs}`.quiet();
|
|
17194
17224
|
return result.stdout.toString().trim();
|
|
17195
17225
|
}
|
|
17196
|
-
function buildPricehistArgs(startDate, endDate,
|
|
17226
|
+
function buildPricehistArgs(startDate, endDate, config2) {
|
|
17197
17227
|
const cmdArgs = [
|
|
17198
17228
|
"fetch",
|
|
17199
17229
|
"-o",
|
|
@@ -17202,11 +17232,11 @@ function buildPricehistArgs(startDate, endDate, currencyConfig) {
|
|
|
17202
17232
|
startDate,
|
|
17203
17233
|
"-e",
|
|
17204
17234
|
endDate,
|
|
17205
|
-
|
|
17206
|
-
|
|
17235
|
+
config2.source,
|
|
17236
|
+
config2.pair
|
|
17207
17237
|
];
|
|
17208
|
-
if (
|
|
17209
|
-
cmdArgs.push("--fmt-base",
|
|
17238
|
+
if (config2.fmt_base) {
|
|
17239
|
+
cmdArgs.push("--fmt-base", config2.fmt_base);
|
|
17210
17240
|
}
|
|
17211
17241
|
return cmdArgs;
|
|
17212
17242
|
}
|
|
@@ -17227,6 +17257,14 @@ function parsePriceLine(line) {
|
|
|
17227
17257
|
formattedLine: line
|
|
17228
17258
|
};
|
|
17229
17259
|
}
|
|
17260
|
+
function ensureQuotedSymbols(line) {
|
|
17261
|
+
return line.replace(/^(P \d{4}-\d{2}-\d{2}(?: \d{2}:\d{2}:\d{2})?) (\S+) /, (_match, prefix, symbol2) => {
|
|
17262
|
+
if (symbol2.includes(".") && !symbol2.startsWith('"')) {
|
|
17263
|
+
return `${prefix} "${symbol2}" `;
|
|
17264
|
+
}
|
|
17265
|
+
return `${prefix} ${symbol2} `;
|
|
17266
|
+
});
|
|
17267
|
+
}
|
|
17230
17268
|
function filterAndSortPriceLinesByDateRange(priceLines, startDate, endDate) {
|
|
17231
17269
|
return priceLines.map(parsePriceLine).filter((parsed) => {
|
|
17232
17270
|
if (!parsed)
|
|
@@ -17234,8 +17272,36 @@ function filterAndSortPriceLinesByDateRange(priceLines, startDate, endDate) {
|
|
|
17234
17272
|
return parsed.date >= startDate && parsed.date <= endDate;
|
|
17235
17273
|
}).sort((a, b) => a.date.localeCompare(b.date)).map((parsed) => parsed.formattedLine);
|
|
17236
17274
|
}
|
|
17237
|
-
async function
|
|
17238
|
-
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");
|
|
17239
17305
|
if (restrictionError) {
|
|
17240
17306
|
return restrictionError;
|
|
17241
17307
|
}
|
|
@@ -17251,34 +17317,19 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
|
|
|
17251
17317
|
const results = [];
|
|
17252
17318
|
for (const [ticker, currencyConfig] of Object.entries(config2.currencies)) {
|
|
17253
17319
|
try {
|
|
17254
|
-
const
|
|
17255
|
-
|
|
17256
|
-
|
|
17257
|
-
const rawPriceLines = output.split(`
|
|
17258
|
-
`).filter((line) => line.startsWith("P "));
|
|
17259
|
-
if (rawPriceLines.length === 0) {
|
|
17260
|
-
results.push({
|
|
17261
|
-
ticker,
|
|
17262
|
-
error: `No price lines in pricehist output: ${output}`
|
|
17263
|
-
});
|
|
17264
|
-
continue;
|
|
17265
|
-
}
|
|
17266
|
-
const priceLines = filterAndSortPriceLinesByDateRange(rawPriceLines, startDate, endDate);
|
|
17267
|
-
if (priceLines.length === 0) {
|
|
17268
|
-
results.push({
|
|
17269
|
-
ticker,
|
|
17270
|
-
error: `No price data found within date range ${startDate} to ${endDate}`
|
|
17271
|
-
});
|
|
17272
|
-
continue;
|
|
17273
|
-
}
|
|
17274
|
-
const journalPath = path4.join(directory, "ledger", "currencies", currencyConfig.file);
|
|
17275
|
-
updatePriceJournal(journalPath, priceLines);
|
|
17276
|
-
const latestPriceLine = priceLines[priceLines.length - 1];
|
|
17320
|
+
const result = await fetchAndWritePrices(ticker, currencyConfig, "ledger/currencies", directory, endDate, defaultBackfillDate, backfill, priceFetcher);
|
|
17321
|
+
results.push(result);
|
|
17322
|
+
} catch (err) {
|
|
17277
17323
|
results.push({
|
|
17278
17324
|
ticker,
|
|
17279
|
-
|
|
17280
|
-
file: currencyConfig.file
|
|
17325
|
+
error: String(err)
|
|
17281
17326
|
});
|
|
17327
|
+
}
|
|
17328
|
+
}
|
|
17329
|
+
for (const [ticker, stockConfig] of Object.entries(config2.stocks)) {
|
|
17330
|
+
try {
|
|
17331
|
+
const result = await fetchAndWritePrices(ticker, stockConfig, "ledger/stocks", directory, endDate, defaultBackfillDate, backfill, priceFetcher);
|
|
17332
|
+
results.push(result);
|
|
17282
17333
|
} catch (err) {
|
|
17283
17334
|
results.push({
|
|
17284
17335
|
ticker,
|
|
@@ -17288,15 +17339,15 @@ async function fetchCurrencyPrices(directory, agent, backfill, priceFetcher = de
|
|
|
17288
17339
|
}
|
|
17289
17340
|
return buildSuccessResult(results, endDate, backfill);
|
|
17290
17341
|
}
|
|
17291
|
-
var
|
|
17292
|
-
description: "ACCOUNTANT AGENT ONLY: Fetches end-of-day prices for all configured currencies (from config/prices.yaml) and appends them to the corresponding price journals in ledger/currencies/.",
|
|
17342
|
+
var fetch_prices_default = tool({
|
|
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/.",
|
|
17293
17344
|
args: {
|
|
17294
|
-
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)")
|
|
17295
17346
|
},
|
|
17296
17347
|
async execute(params, context) {
|
|
17297
17348
|
const { directory, agent } = context;
|
|
17298
17349
|
const { backfill } = params;
|
|
17299
|
-
return
|
|
17350
|
+
return fetchPrices(directory, agent, backfill || false);
|
|
17300
17351
|
}
|
|
17301
17352
|
});
|
|
17302
17353
|
// src/tools/classify-statements.ts
|
|
@@ -27759,7 +27810,7 @@ var AccountantPlugin = async () => {
|
|
|
27759
27810
|
const agent = loadAgent(AGENT_FILE);
|
|
27760
27811
|
return {
|
|
27761
27812
|
tool: {
|
|
27762
|
-
"fetch-
|
|
27813
|
+
"fetch-prices": fetch_prices_default,
|
|
27763
27814
|
"classify-statements": classify_statements_default,
|
|
27764
27815
|
"import-statements": import_statements_default,
|
|
27765
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
|
```
|