@fuzzle/opencode-accountant 0.16.1 → 0.16.2-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.
Files changed (2) hide show
  1. package/dist/index.js +121 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16987,6 +16987,7 @@ function loadPricesConfig(directory) {
16987
16987
  }
16988
16988
 
16989
16989
  // src/utils/journalUtils.ts
16990
+ init_js_yaml();
16990
16991
  import * as fs4 from "fs";
16991
16992
  import * as path3 from "path";
16992
16993
 
@@ -17178,6 +17179,50 @@ function ensureInvestmentAccountDeclarations(accountJournalPath, accounts, logge
17178
17179
  logger?.info(`Account declarations: added ${missing.length} (${missing.join(", ")})`);
17179
17180
  return { added: missing.sort(), updated: true };
17180
17181
  }
17182
+ function ensureStockPriceEntries(directory, symbols, logger) {
17183
+ const pricesPath = path3.join(directory, "config", "prices.yaml");
17184
+ if (!fs4.existsSync(pricesPath)) {
17185
+ return { added: [], updated: false };
17186
+ }
17187
+ const content = fs4.readFileSync(pricesPath, "utf-8");
17188
+ const parsed = jsYaml.load(content);
17189
+ const existingStocks = new Set;
17190
+ if (parsed?.stocks && typeof parsed.stocks === "object" && !Array.isArray(parsed.stocks)) {
17191
+ for (const key of Object.keys(parsed.stocks)) {
17192
+ existingStocks.add(key);
17193
+ }
17194
+ }
17195
+ const missing = [];
17196
+ for (const symbol2 of symbols) {
17197
+ const ticker = symbol2.toUpperCase();
17198
+ if (!existingStocks.has(ticker)) {
17199
+ missing.push(ticker);
17200
+ }
17201
+ }
17202
+ if (missing.length === 0) {
17203
+ return { added: [], updated: false };
17204
+ }
17205
+ let updatedContent = content;
17206
+ if (!parsed?.stocks) {
17207
+ updatedContent = updatedContent.trimEnd() + `
17208
+
17209
+ stocks:
17210
+ `;
17211
+ }
17212
+ if (!updatedContent.endsWith(`
17213
+ `)) {
17214
+ updatedContent += `
17215
+ `;
17216
+ }
17217
+ const sorted = missing.sort();
17218
+ for (const ticker of sorted) {
17219
+ updatedContent += ` ${ticker}:
17220
+ `;
17221
+ }
17222
+ fs4.writeFileSync(pricesPath, updatedContent);
17223
+ logger?.info(`Price config: added ${sorted.length} stock(s) (${sorted.join(", ")})`);
17224
+ return { added: sorted, updated: true };
17225
+ }
17181
17226
 
17182
17227
  // src/utils/dateUtils.ts
17183
17228
  function formatDateISO(date5) {
@@ -17506,7 +17551,9 @@ function validateProviderConfig(name, config2) {
17506
17551
  return {
17507
17552
  detect,
17508
17553
  currencies,
17509
- importOrder: configObj.importOrder
17554
+ importOrder: configObj.importOrder,
17555
+ lotInventoryPath: typeof configObj.lotInventoryPath === "string" ? configObj.lotInventoryPath : undefined,
17556
+ symbolMapPath: typeof configObj.symbolMapPath === "string" ? configObj.symbolMapPath : undefined
17510
17557
  };
17511
17558
  }
17512
17559
  function loadImportConfig(directory) {
@@ -23515,7 +23562,8 @@ function balancesMatch(balance1, balance2) {
23515
23562
  return false;
23516
23563
  }
23517
23564
  validateCurrencies(parsed1, parsed2);
23518
- return parsed1.amount === parsed2.amount;
23565
+ const round2 = (n) => Math.round(n * 100) / 100;
23566
+ return round2(parsed1.amount) === round2(parsed2.amount);
23519
23567
  }
23520
23568
 
23521
23569
  // src/utils/csvParser.ts
@@ -26427,6 +26475,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26427
26475
  investmentAccounts.add(`equity:conversion:${currency.toLowerCase()}`);
26428
26476
  investmentAccounts.add("equity:rounding");
26429
26477
  ensureInvestmentAccountDeclarations(accountJournalPath, investmentAccounts, logger);
26478
+ ensureStockPriceEntries(projectDir, stockSymbols, logger);
26430
26479
  }
26431
26480
  logger?.logResult({
26432
26481
  totalRows: stats.totalRows,
@@ -26968,6 +27017,34 @@ function generateIbkrAdjustmentEntry(adjustment, logger) {
26968
27017
  `;
26969
27018
  return entry;
26970
27019
  }
27020
+ function generateIbkrForexEntry(entry, logger) {
27021
+ const description = escapeDescription(entry.description);
27022
+ logger?.debug(`Generating IBKR Forex entry: ${entry.amount} ${entry.currency}`);
27023
+ const postings = [
27024
+ {
27025
+ account: `assets:broker:ibkr:${entry.currency.toLowerCase()}`,
27026
+ amount: formatAmount(entry.amount, entry.currency)
27027
+ }
27028
+ ];
27029
+ if (entry.amount < 0) {
27030
+ postings.push({
27031
+ account: "expenses:fees:forex:ibkr",
27032
+ amount: formatAmount(Math.abs(entry.amount), entry.currency)
27033
+ });
27034
+ } else {
27035
+ postings.push({
27036
+ account: "income:fx-gains:ibkr",
27037
+ amount: formatAmount(-entry.amount, entry.currency)
27038
+ });
27039
+ }
27040
+ let result = `${entry.date} ${description}
27041
+ `;
27042
+ result += ` ; ibkr:account:${entry.account}
27043
+ `;
27044
+ result += formatPostings(postings) + `
27045
+ `;
27046
+ return result;
27047
+ }
26971
27048
 
26972
27049
  // src/utils/ibkrCsvPreprocessor.ts
26973
27050
  var TRADE_TYPES2 = new Set(["Buy", "Sell"]);
@@ -26991,7 +27068,7 @@ function parseIbkrCsv(content) {
26991
27068
  const account = values[3]?.trim() || "";
26992
27069
  const description = values[4]?.trim() || "";
26993
27070
  const transactionType = values[5]?.trim() || "";
26994
- const symbol2 = values[6]?.trim() || "";
27071
+ const symbol2 = normalizeSymbol(values[6]?.trim() || "");
26995
27072
  const quantity = parseFloat(values[7] || "0") || 0;
26996
27073
  const price = parseFloat(values[8] || "0") || 0;
26997
27074
  const priceCurrency = values[9]?.trim() || "";
@@ -27064,7 +27141,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27064
27141
  trades: 0,
27065
27142
  dividends: 0,
27066
27143
  adjustments: 0,
27067
- skippedForex: 0
27144
+ forex: 0
27068
27145
  },
27069
27146
  alreadyPreprocessed: true
27070
27147
  };
@@ -27076,7 +27153,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27076
27153
  const trades = [];
27077
27154
  const dividendTxns = [];
27078
27155
  const adjustments = [];
27079
- let skippedForex = 0;
27156
+ const forexTxns = [];
27080
27157
  for (const txn of transactions) {
27081
27158
  if (TRADE_TYPES2.has(txn.transactionType)) {
27082
27159
  trades.push(txn);
@@ -27087,7 +27164,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27087
27164
  } else if (txn.transactionType === DEPOSIT_TYPE) {
27088
27165
  deposits.push(txn);
27089
27166
  } else if (txn.transactionType === FOREX_TYPE) {
27090
- skippedForex++;
27167
+ forexTxns.push(txn);
27091
27168
  } else {
27092
27169
  logger?.warn(`Unknown IBKR transaction type: ${txn.transactionType}`);
27093
27170
  }
@@ -27173,6 +27250,38 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27173
27250
  usedAccounts.add("income:fx-gains:ibkr");
27174
27251
  }
27175
27252
  }
27253
+ if (forexTxns.length > 0) {
27254
+ const forexByDate = new Map;
27255
+ for (const txn of forexTxns) {
27256
+ const existing = forexByDate.get(txn.date);
27257
+ if (existing) {
27258
+ existing.total += txn.netAmount;
27259
+ } else {
27260
+ forexByDate.set(txn.date, { account: txn.account, total: txn.netAmount });
27261
+ }
27262
+ }
27263
+ const sortedDates = [...forexByDate.keys()].sort();
27264
+ for (const date5 of sortedDates) {
27265
+ const { account, total } = forexByDate.get(date5);
27266
+ const amount = Math.round(total * 100) / 100;
27267
+ if (amount === 0)
27268
+ continue;
27269
+ const forexEntry = {
27270
+ date: date5,
27271
+ account,
27272
+ description: "Forex conversion cost",
27273
+ amount,
27274
+ currency: currency.toUpperCase()
27275
+ };
27276
+ journalEntries.push(generateIbkrForexEntry(forexEntry, logger));
27277
+ usedAccounts.add(`assets:broker:ibkr:${currency}`);
27278
+ if (amount < 0) {
27279
+ usedAccounts.add("expenses:fees:forex:ibkr");
27280
+ } else {
27281
+ usedAccounts.add("income:fx-gains:ibkr");
27282
+ }
27283
+ }
27284
+ }
27176
27285
  saveLotInventory(directory, lotInventoryPath, inventory, logger);
27177
27286
  let journalFilePath = null;
27178
27287
  if (journalEntries.length > 0) {
@@ -27194,6 +27303,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27194
27303
  const accountJournalPath = path17.join(directory, "ledger", "investments", "accounts.journal");
27195
27304
  ensureInvestmentAccountDeclarations(accountJournalPath, usedAccounts, logger);
27196
27305
  }
27306
+ ensureStockPriceEntries(directory, tradedSymbols, logger);
27197
27307
  logger?.info(`Generated IBKR journal: ${journalPath} with ${journalEntries.length} entries`);
27198
27308
  }
27199
27309
  let simpleTransactionsCsvPath = null;
@@ -27230,7 +27340,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27230
27340
  trades: trades.length,
27231
27341
  dividends: dividendCount,
27232
27342
  adjustments: adjustments.length,
27233
- skippedForex
27343
+ forex: forexTxns.length
27234
27344
  },
27235
27345
  alreadyPreprocessed: false
27236
27346
  };
@@ -27609,7 +27719,7 @@ async function executeIbkrPreprocessStep(context, contextIds, logger) {
27609
27719
  trades: 0,
27610
27720
  dividends: 0,
27611
27721
  adjustments: 0,
27612
- skippedForex: 0
27722
+ forex: 0
27613
27723
  };
27614
27724
  let lastJournalFile = null;
27615
27725
  ibkrContexts.sort((a, b) => {
@@ -27625,7 +27735,7 @@ async function executeIbkrPreprocessStep(context, contextIds, logger) {
27625
27735
  totalStats.trades += result.stats.trades;
27626
27736
  totalStats.dividends += result.stats.dividends;
27627
27737
  totalStats.adjustments += result.stats.adjustments;
27628
- totalStats.skippedForex += result.stats.skippedForex;
27738
+ totalStats.forex += result.stats.forex;
27629
27739
  if (result.journalFile) {
27630
27740
  lastJournalFile = result.journalFile;
27631
27741
  }
@@ -27635,9 +27745,9 @@ async function executeIbkrPreprocessStep(context, contextIds, logger) {
27635
27745
  });
27636
27746
  logger?.info(`Updated context ${ibkrCtx.contextId} to use filtered CSV: ${path18.basename(result.simpleTransactionsCsv)}`);
27637
27747
  }
27638
- logger?.logStep("IBKR Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.adjustments} adjustments, ${result.stats.skippedForex} forex skipped`);
27748
+ logger?.logStep("IBKR Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.adjustments} adjustments, ${result.stats.forex} forex`);
27639
27749
  }
27640
- const message = `Preprocessed ${totalStats.totalRows} rows: ${totalStats.trades} trades, ${totalStats.dividends} dividends, ${totalStats.adjustments} adjustments, ${totalStats.deposits} deposits, ${totalStats.skippedForex} forex skipped`;
27750
+ const message = `Preprocessed ${totalStats.totalRows} rows: ${totalStats.trades} trades, ${totalStats.dividends} dividends, ${totalStats.adjustments} adjustments, ${totalStats.deposits} deposits, ${totalStats.forex} forex`;
27641
27751
  context.result.steps.ibkrPreprocess = buildStepResult(true, message, {
27642
27752
  ...totalStats,
27643
27753
  journalFile: lastJournalFile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.16.1",
3
+ "version": "0.16.2-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",