@fuzzle/opencode-accountant 0.13.21-next.1 → 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 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-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:
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
- └── currencies/ # Where price journal files are written
65
- ├── btc-chf.journal
66
- ├── eur-chf.journal
67
- └── usd-chf.journal
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
@@ -8,7 +8,7 @@ tools:
8
8
  bash: true
9
9
  edit: true
10
10
  write: true
11
- # MCP tools available: import-pipeline, fetch-currency-prices, hledger-mcp (read-only queries)
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-currency-prices` | Fetching exchange rates | `curl` to price APIs, manual price entries |
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-currency-prices` tool instead
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-currency-prices
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-currency-prices()` or `fetch-currency-prices(backfill: false)`
242
- - Backfill mode: `fetch-currency-prices(backfill: true)`
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-currency results with latest price line or error message
252
+ **Output:** Returns per-ticker results with latest price line or error message
252
253
 
253
- **Configuration:** `config/prices.yaml` defines currencies, sources, pairs, and backfill dates
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-currency-prices.ts
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
- 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`);
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
- 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
- }
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-currency-prices.ts
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, currencyConfig) {
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
- currencyConfig.source,
17218
- currencyConfig.pair
17235
+ config2.source,
17236
+ config2.pair
17219
17237
  ];
17220
- if (currencyConfig.fmt_base) {
17221
- cmdArgs.push("--fmt-base", currencyConfig.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 fetchCurrencyPrices(directory, agent, backfill, priceFetcher = defaultPriceFetcher, configLoader = loadPricesConfig) {
17258
- const restrictionError = checkAccountantAgent(agent, "fetch currency prices");
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 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
- });
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 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
- });
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 fetch_currency_prices_default = tool({
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 fetchCurrencyPrices(directory, agent, backfill || false);
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-currency-prices": fetch_currency_prices_default,
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-currency-prices Tool
1
+ # fetch-prices Tool
2
2
 
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.).
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": "BTC",
37
- "priceLine": "P 2026-02-21 BTC 52341.50 CHF",
38
- "file": "btc.journal"
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 currency is fetched.
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": "USD",
63
- "priceLine": "P 2026-02-21 USD 0.881 CHF",
64
- "file": "usd.journal"
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 currencies succeed and others fail:
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 currency)
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-currency-prices()
138
+ fetch-prices()
139
139
  # or
140
- fetch-currency-prices(backfill: false)
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 currency)
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-currency `backfill_date` (or default) to yesterday
157
+ **Date range:** Per-ticker `backfill_date` (or default) to yesterday
158
158
 
159
159
  **Example:**
160
160
 
161
161
  ```
162
- fetch-currency-prices(backfill: true)
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 currencies)
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 currency has no `backfill_date` configured: January 1st of current year
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-currency?**
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 = currency's `backfill_date` (or default: Jan 1 of current year)
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
- **Example:**
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
- Means: 1 EUR = 0.944 CHF on 2026-02-21
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
- - All price journals stored in `ledger/currencies/`
325
- - One file per currency (configured in `config/prices.yaml`)
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
- └── currencies/
333
- ├── eur.journal
334
- ├── usd.journal
335
- ├── btc.journal
336
- └── eth.journal
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-currency-prices()` (or `fetch-currency-prices(backfill: false)`)
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-currency-prices(backfill: true)` to fetch historical data
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-currency-prices()`)
400
+ 5. Subsequent updates: use daily mode (`fetch-prices()`)
377
401
 
378
- ### Scenario 3: Fixing Missing Dates
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-currency-prices(backfill: true)` to fill gaps
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 4: Handling Errors
437
+ ### Scenario 5: Handling Errors
398
438
 
399
439
  **Goal:** Recover from partial failures
400
440
 
401
- 1. Run `fetch-currency-prices()`
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 currency
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 currencies are already updated. Only failed currencies need retry.
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> # 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
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/` directory |
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 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
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 currencies won't re-fetch (deduplication handles this)
555
- - Only failed currencies will retry
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 file:**
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
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.13.21-next.1",
3
+ "version": "0.14.0-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",