@fuzzle/opencode-accountant 0.10.5-next.1 → 0.10.6-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
@@ -270,6 +270,15 @@ The pipeline automatically manages account declarations:
270
270
 
271
271
  **No manual account setup required.** Account declarations are created proactively before import attempts.
272
272
 
273
+ #### Commodity Declarations
274
+
275
+ For Swissquote investment imports, the pipeline automatically manages commodity declarations:
276
+
277
+ - Collects all stock ticker symbols from trades, dividends, and corporate actions
278
+ - Writes sorted `commodity "SYMBOL"` declarations to `ledger/investments/commodities.journal`
279
+ - Merges with existing declarations (idempotent)
280
+ - Ensures `hledger check --strict` validation passes for quoted commodity symbols
281
+
273
282
  #### Unknown Postings
274
283
 
275
284
  When a transaction doesn't match any `if` pattern in the rules file, hledger assigns it to `income:unknown` or `expenses:unknown` depending on the transaction direction. The pipeline will fail at the validation step, reporting the unknown postings so you can add appropriate rules before retrying.
@@ -164,6 +164,15 @@ The import pipeline automatically:
164
164
  - Prevents `hledger check --strict` failures due to missing account declarations
165
165
  - No manual account setup required
166
166
 
167
+ ### Automatic Commodity Declarations
168
+
169
+ For Swissquote investment imports, the pipeline automatically:
170
+
171
+ - Collects all stock ticker symbols from trades, dividends, and corporate actions
172
+ - Writes sorted `commodity "SYMBOL"` declarations to `ledger/investments/commodities.journal`
173
+ - Merges with existing declarations (idempotent)
174
+ - Prevents `hledger check --strict` failures due to missing commodity declarations
175
+
167
176
  ### Automatic Balance Detection
168
177
 
169
178
  The reconciliation step attempts to extract closing balance from:
package/dist/index.js CHANGED
@@ -4533,7 +4533,7 @@ var init_accountSuggester = __esm(() => {
4533
4533
 
4534
4534
  // src/index.ts
4535
4535
  init_agentLoader();
4536
- import { dirname as dirname6, join as join15 } from "path";
4536
+ import { dirname as dirname7, join as join15 } from "path";
4537
4537
  import { fileURLToPath as fileURLToPath3 } from "url";
4538
4538
 
4539
4539
  // node_modules/zod/v4/classic/external.js
@@ -17051,6 +17051,38 @@ function ensureYearJournalExists(directory, year) {
17051
17051
  }
17052
17052
  return yearJournalPath;
17053
17053
  }
17054
+ function ensureCommodityDeclarations(commodityJournalPath, symbols, logger) {
17055
+ const existing = new Set;
17056
+ if (fs4.existsSync(commodityJournalPath)) {
17057
+ const content2 = fs4.readFileSync(commodityJournalPath, "utf-8");
17058
+ for (const line of content2.split(`
17059
+ `)) {
17060
+ const match = line.trim().match(/^commodity\s+"(.+)"/);
17061
+ if (match) {
17062
+ existing.add(match[1]);
17063
+ }
17064
+ }
17065
+ }
17066
+ const missing = [];
17067
+ for (const symbol2 of symbols) {
17068
+ const upper = symbol2.toUpperCase();
17069
+ if (!existing.has(upper)) {
17070
+ missing.push(upper);
17071
+ existing.add(upper);
17072
+ }
17073
+ }
17074
+ if (missing.length === 0) {
17075
+ return { added: [], updated: false };
17076
+ }
17077
+ const sorted = Array.from(existing).sort((a, b) => a.localeCompare(b));
17078
+ const content = sorted.map((s) => `commodity "${s}"`).join(`
17079
+ `) + `
17080
+ `;
17081
+ ensureDirectory(path3.dirname(commodityJournalPath));
17082
+ fs4.writeFileSync(commodityJournalPath, content);
17083
+ logger?.info(`Commodity declarations: added ${missing.length} (${missing.join(", ")})`);
17084
+ return { added: missing.sort(), updated: true };
17085
+ }
17054
17086
 
17055
17087
  // src/utils/dateUtils.ts
17056
17088
  function formatDateISO(date5) {
@@ -17408,16 +17440,22 @@ function parseCSVPreview(content, skipRows = 0, delimiter = ",") {
17408
17440
  csvContent = lines.slice(skipRows).join(`
17409
17441
  `);
17410
17442
  }
17411
- const result = import_papaparse.default.parse(csvContent, {
17412
- header: true,
17413
- preview: 1,
17414
- skipEmptyLines: true,
17415
- delimiter
17416
- });
17417
- return {
17418
- fields: result.meta.fields,
17419
- firstRow: result.data[0]
17420
- };
17443
+ const origWarn = console.warn;
17444
+ console.warn = () => {};
17445
+ try {
17446
+ const result = import_papaparse.default.parse(csvContent, {
17447
+ header: true,
17448
+ preview: 1,
17449
+ skipEmptyLines: true,
17450
+ delimiter
17451
+ });
17452
+ return {
17453
+ fields: result.meta.fields,
17454
+ firstRow: result.data[0]
17455
+ };
17456
+ } finally {
17457
+ console.warn = origWarn;
17458
+ }
17421
17459
  }
17422
17460
  function extractLastRowField(content, skipRows, delimiter, fieldName) {
17423
17461
  let csvContent = content;
@@ -17427,11 +17465,18 @@ function extractLastRowField(content, skipRows, delimiter, fieldName) {
17427
17465
  csvContent = lines.slice(skipRows).join(`
17428
17466
  `);
17429
17467
  }
17430
- const result = import_papaparse.default.parse(csvContent, {
17431
- header: true,
17432
- skipEmptyLines: true,
17433
- delimiter
17434
- });
17468
+ const origWarn = console.warn;
17469
+ console.warn = () => {};
17470
+ let result;
17471
+ try {
17472
+ result = import_papaparse.default.parse(csvContent, {
17473
+ header: true,
17474
+ skipEmptyLines: true,
17475
+ delimiter
17476
+ });
17477
+ } finally {
17478
+ console.warn = origWarn;
17479
+ }
17435
17480
  if (result.data.length === 0)
17436
17481
  return;
17437
17482
  const lastRow = result.data[result.data.length - 1];
@@ -25003,6 +25048,9 @@ function formatDate(dateStr) {
25003
25048
  function escapeDescription(desc) {
25004
25049
  return desc.replace(/[;|]/g, "-").trim();
25005
25050
  }
25051
+ function formatCommodity(symbol2) {
25052
+ return `"${symbol2.toUpperCase()}"`;
25053
+ }
25006
25054
  function formatQuantity(qty) {
25007
25055
  return qty.toFixed(6).replace(/\.?0+$/, "");
25008
25056
  }
@@ -25020,11 +25068,12 @@ function generateBuyEntry(trade, logger) {
25020
25068
  const fees = trade.costs;
25021
25069
  const cashOut = totalCost + fees;
25022
25070
  logger?.debug(`Generating Buy entry: ${qty} ${trade.symbol} @ ${price} ${trade.currency}`);
25071
+ const commodity = formatCommodity(trade.symbol);
25023
25072
  let entry = `${date5} ${description}
25024
25073
  `;
25025
25074
  entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
25026
25075
  `;
25027
- entry += ` assets:investments:stocks:${trade.symbol} ${qty} @ ${price} ${trade.currency}
25076
+ entry += ` assets:investments:stocks:${trade.symbol} ${qty} ${commodity} @ ${price} ${trade.currency}
25028
25077
  `;
25029
25078
  if (fees > 0) {
25030
25079
  entry += ` expenses:fees:trading:swissquote ${formatAmount2(fees, trade.currency)}
@@ -25044,6 +25093,7 @@ function generateSellEntry(trade, consumed, logger) {
25044
25093
  const cashIn = saleProceeds - fees;
25045
25094
  const gain = calculateCapitalGain(consumed, salePrice, trade.quantity);
25046
25095
  logger?.debug(`Generating Sell entry: ${qty} ${trade.symbol} @ ${salePrice} ${trade.currency}, gain: ${gain.toFixed(2)}`);
25096
+ const commodity = formatCommodity(trade.symbol);
25047
25097
  let entry = `${date5} ${description}
25048
25098
  `;
25049
25099
  entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
@@ -25054,7 +25104,7 @@ function generateSellEntry(trade, consumed, logger) {
25054
25104
  for (const c of consumed) {
25055
25105
  const lotQty = formatQuantity(c.quantity);
25056
25106
  const lotPrice = formatPrice(c.lot.costBasis);
25057
- entry += ` assets:investments:stocks:${trade.symbol} -${lotQty} @ ${lotPrice} ${trade.currency}
25107
+ entry += ` assets:investments:stocks:${trade.symbol} -${lotQty} ${commodity} @ ${lotPrice} ${trade.currency}
25058
25108
  `;
25059
25109
  }
25060
25110
  entry += ` assets:broker:swissquote:${trade.currency.toLowerCase()} ${formatAmount2(cashIn, trade.currency)}
@@ -25102,13 +25152,14 @@ function generateSplitEntry(action, oldQuantity, newQuantity, logger) {
25102
25152
  `;
25103
25153
  entry += ` ; Ratio: ${ratio.toFixed(4)} (${oldQuantity} -> ${newQuantity})
25104
25154
  `;
25105
- entry += ` assets:investments:stocks:${action.symbol} -${formatQuantity(oldQuantity)}
25155
+ const commodity = formatCommodity(action.symbol);
25156
+ entry += ` assets:investments:stocks:${action.symbol} -${formatQuantity(oldQuantity)} ${commodity}
25106
25157
  `;
25107
- entry += ` equity:conversion ${formatQuantity(oldQuantity)}
25158
+ entry += ` equity:conversion ${formatQuantity(oldQuantity)} ${commodity}
25108
25159
  `;
25109
- entry += ` equity:conversion -${formatQuantity(newQuantity)}
25160
+ entry += ` equity:conversion -${formatQuantity(newQuantity)} ${commodity}
25110
25161
  `;
25111
- entry += ` assets:investments:stocks:${action.symbol} ${formatQuantity(newQuantity)}
25162
+ entry += ` assets:investments:stocks:${action.symbol} ${formatQuantity(newQuantity)} ${commodity}
25112
25163
  `;
25113
25164
  return entry;
25114
25165
  }
@@ -25125,10 +25176,11 @@ function generateWorthlessEntry(action, removedLots, logger) {
25125
25176
  `;
25126
25177
  entry += ` ; Total loss: ${totalCost.toFixed(2)} ${currency}
25127
25178
  `;
25179
+ const commodity = formatCommodity(action.symbol);
25128
25180
  for (const lot of removedLots) {
25129
25181
  const qty = formatQuantity(lot.quantity);
25130
25182
  const price = formatPrice(lot.costBasis);
25131
- entry += ` assets:investments:stocks:${action.symbol} -${qty} @ ${price} ${currency}
25183
+ entry += ` assets:investments:stocks:${action.symbol} -${qty} ${commodity} @ ${price} ${currency}
25132
25184
  `;
25133
25185
  }
25134
25186
  entry += ` expenses:losses:capital ${formatAmount2(totalCost, currency)}
@@ -25153,16 +25205,18 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, logger
25153
25205
  }
25154
25206
  for (const out of group.outgoing) {
25155
25207
  const qty = formatQuantity(Math.abs(out.quantity));
25156
- entry += ` assets:investments:stocks:${out.symbol} -${qty}
25208
+ const commodity = formatCommodity(out.symbol);
25209
+ entry += ` assets:investments:stocks:${out.symbol} -${qty} ${commodity}
25157
25210
  `;
25158
- entry += ` equity:conversion ${qty}
25211
+ entry += ` equity:conversion ${qty} ${commodity}
25159
25212
  `;
25160
25213
  }
25161
25214
  for (const inc of group.incoming) {
25162
25215
  const qty = formatQuantity(Math.abs(inc.quantity));
25163
- entry += ` equity:conversion -${qty}
25216
+ const commodity = formatCommodity(inc.symbol);
25217
+ entry += ` equity:conversion -${qty} ${commodity}
25164
25218
  `;
25165
- entry += ` assets:investments:stocks:${inc.symbol} ${qty}
25219
+ entry += ` assets:investments:stocks:${inc.symbol} ${qty} ${commodity}
25166
25220
  `;
25167
25221
  }
25168
25222
  return entry;
@@ -25172,11 +25226,12 @@ function generateRightsDistributionEntry(action, logger) {
25172
25226
  const qty = formatQuantity(Math.abs(action.quantity));
25173
25227
  const description = escapeDescription(`Rights Distribution: ${action.symbol} - ${action.name}`);
25174
25228
  logger?.debug(`Generating Rights Distribution entry: ${qty} ${action.symbol}`);
25229
+ const commodity = formatCommodity(action.symbol);
25175
25230
  let entry = `${date5} ${description}
25176
25231
  `;
25177
25232
  entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
25178
25233
  `;
25179
- entry += ` assets:investments:stocks:${action.symbol} ${qty} @ 0.00 CAD
25234
+ entry += ` assets:investments:stocks:${action.symbol} ${qty} ${commodity} @ 0.00 CAD
25180
25235
  `;
25181
25236
  entry += ` income:capital-gains:rights-distribution 0.00 CAD
25182
25237
  `;
@@ -25744,6 +25799,23 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
25744
25799
  logger?.logStep("write-journal", "success", `Created ${path13.basename(journalFile)} with ${journalEntries.length} entries`);
25745
25800
  }
25746
25801
  }
25802
+ const stockSymbols = new Set;
25803
+ for (const trade of trades) {
25804
+ stockSymbols.add(trade.symbol);
25805
+ }
25806
+ for (const dividend of dividends) {
25807
+ stockSymbols.add(dividend.symbol);
25808
+ }
25809
+ for (const action of corporateActions) {
25810
+ stockSymbols.add(action.symbol);
25811
+ if (action.newSymbol) {
25812
+ stockSymbols.add(action.newSymbol);
25813
+ }
25814
+ }
25815
+ if (stockSymbols.size > 0) {
25816
+ const commodityJournalPath = path13.join(projectDir, "ledger", "investments", "commodities.journal");
25817
+ ensureCommodityDeclarations(commodityJournalPath, stockSymbols, logger);
25818
+ }
25747
25819
  logger?.logResult({
25748
25820
  totalRows: stats.totalRows,
25749
25821
  simpleTransactions: stats.simpleTransactions,
@@ -26672,7 +26744,7 @@ to produce equity conversion entries for Bitcoin purchases.
26672
26744
  }
26673
26745
  });
26674
26746
  // src/index.ts
26675
- var __dirname2 = dirname6(fileURLToPath3(import.meta.url));
26747
+ var __dirname2 = dirname7(fileURLToPath3(import.meta.url));
26676
26748
  var AGENT_FILE = join15(__dirname2, "..", "agent", "accountant.md");
26677
26749
  var AccountantPlugin = async () => {
26678
26750
  const agent = loadAgent(AGENT_FILE);
@@ -220,7 +220,8 @@ See [classify-statements](classify-statements.md) for details.
220
220
  - **Rights Distributions**: adds shares at zero cost basis
221
221
  - **Cross-currency mergers**: saves outgoing side to pending state, loads when incoming side is processed
222
222
  8. Generates per-year investment journal entries (`ledger/investments/{year}-{currency}.journal`) based on the CSV filename's date range
223
- 9. Outputs filtered CSV (simple transactions only) for hledger rules import
223
+ 9. Collects all stock ticker symbols and writes commodity declarations to `ledger/investments/commodities.journal` (merged with existing, sorted, idempotent)
224
+ 10. Outputs filtered CSV (simple transactions only) for hledger rules import
224
225
 
225
226
  **Symbol Map**: Maps Swissquote's internal symbol names (e.g., `GOLD ROYALTY RG`, `VIZSLA WT 12.25`) to canonical ticker symbols (e.g., `GROY`, `VROY-WT`). Applied before any processing, so journal entries, lot inventory filenames, and account names all use the mapped symbols. See [Symbol Map Configuration](../configuration/symbol-map.md).
226
227
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.10.5-next.1",
3
+ "version": "0.10.6-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",