@fuzzle/opencode-accountant 0.16.2 → 0.16.3-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 +108 -133
  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) {
@@ -26428,6 +26475,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26428
26475
  investmentAccounts.add(`equity:conversion:${currency.toLowerCase()}`);
26429
26476
  investmentAccounts.add("equity:rounding");
26430
26477
  ensureInvestmentAccountDeclarations(accountJournalPath, investmentAccounts, logger);
26478
+ ensureStockPriceEntries(projectDir, stockSymbols, logger);
26431
26479
  }
26432
26480
  logger?.logResult({
26433
26481
  totalRows: stats.totalRows,
@@ -26941,62 +26989,6 @@ function generateIbkrDividendEntry(dividend, logger) {
26941
26989
  `;
26942
26990
  return entry;
26943
26991
  }
26944
- function generateIbkrAdjustmentEntry(adjustment, logger) {
26945
- const description = escapeDescription(adjustment.description);
26946
- logger?.debug(`Generating IBKR Adjustment entry: ${adjustment.amount} ${adjustment.currency}`);
26947
- const postings = [
26948
- {
26949
- account: `assets:broker:ibkr:${adjustment.currency.toLowerCase()}`,
26950
- amount: formatAmount(adjustment.amount, adjustment.currency)
26951
- }
26952
- ];
26953
- if (adjustment.amount < 0) {
26954
- postings.push({
26955
- account: "expenses:fx-losses:ibkr",
26956
- amount: formatAmount(Math.abs(adjustment.amount), adjustment.currency)
26957
- });
26958
- } else {
26959
- postings.push({
26960
- account: "income:fx-gains:ibkr",
26961
- amount: formatAmount(-adjustment.amount, adjustment.currency)
26962
- });
26963
- }
26964
- let entry = `${adjustment.date} ${description}
26965
- `;
26966
- entry += ` ; ibkr:account:${adjustment.account}
26967
- `;
26968
- entry += formatPostings(postings) + `
26969
- `;
26970
- return entry;
26971
- }
26972
- function generateIbkrForexEntry(entry, logger) {
26973
- const description = escapeDescription(entry.description);
26974
- logger?.debug(`Generating IBKR Forex entry: ${entry.amount} ${entry.currency}`);
26975
- const postings = [
26976
- {
26977
- account: `assets:broker:ibkr:${entry.currency.toLowerCase()}`,
26978
- amount: formatAmount(entry.amount, entry.currency)
26979
- }
26980
- ];
26981
- if (entry.amount < 0) {
26982
- postings.push({
26983
- account: "expenses:fees:forex:ibkr",
26984
- amount: formatAmount(Math.abs(entry.amount), entry.currency)
26985
- });
26986
- } else {
26987
- postings.push({
26988
- account: "income:fx-gains:ibkr",
26989
- amount: formatAmount(-entry.amount, entry.currency)
26990
- });
26991
- }
26992
- let result = `${entry.date} ${description}
26993
- `;
26994
- result += ` ; ibkr:account:${entry.account}
26995
- `;
26996
- result += formatPostings(postings) + `
26997
- `;
26998
- return result;
26999
- }
27000
26992
 
27001
26993
  // src/utils/ibkrCsvPreprocessor.ts
27002
26994
  var TRADE_TYPES2 = new Set(["Buy", "Sell"]);
@@ -27020,7 +27012,7 @@ function parseIbkrCsv(content) {
27020
27012
  const account = values[3]?.trim() || "";
27021
27013
  const description = values[4]?.trim() || "";
27022
27014
  const transactionType = values[5]?.trim() || "";
27023
- const symbol2 = values[6]?.trim() || "";
27015
+ const symbol2 = normalizeSymbol(values[6]?.trim() || "");
27024
27016
  const quantity = parseFloat(values[7] || "0") || 0;
27025
27017
  const price = parseFloat(values[8] || "0") || 0;
27026
27018
  const priceCurrency = values[9]?.trim() || "";
@@ -27078,25 +27070,34 @@ function groupDividends(transactions) {
27078
27070
  groups.sort((a, b) => a.date.localeCompare(b.date));
27079
27071
  return groups;
27080
27072
  }
27073
+ function isIbkrEntryDuplicate(entry, existingJournalContent) {
27074
+ if (!existingJournalContent)
27075
+ return false;
27076
+ const firstLine = entry.split(`
27077
+ `)[0].trim();
27078
+ if (!firstLine)
27079
+ return false;
27080
+ if (!existingJournalContent.includes(firstLine))
27081
+ return false;
27082
+ const brokerLine = entry.split(`
27083
+ `).find((l) => l.includes("assets:broker:ibkr:"));
27084
+ if (!brokerLine)
27085
+ return true;
27086
+ const amountMatch = brokerLine.match(/(-?\d+\.\d+\s+[A-Z]+)/);
27087
+ if (!amountMatch)
27088
+ return true;
27089
+ const idx = existingJournalContent.indexOf(firstLine);
27090
+ const chunk = existingJournalContent.slice(idx, idx + 500);
27091
+ return chunk.includes(amountMatch[1]);
27092
+ }
27081
27093
  async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPath, logger) {
27082
27094
  const csvDir = path17.dirname(csvPath);
27083
27095
  const csvBasename = path17.basename(csvPath, ".csv");
27084
27096
  const journalPath = path17.join(directory, "ledger", "investments", `${year}-ibkr-${currency}.journal`);
27097
+ let existingJournalContent = "";
27085
27098
  if (fs22.existsSync(journalPath)) {
27086
- logger?.info(`IBKR journal already exists: ${journalPath}, skipping preprocessing`);
27087
- return {
27088
- simpleTransactionsCsv: null,
27089
- journalFile: journalPath,
27090
- stats: {
27091
- totalRows: 0,
27092
- deposits: 0,
27093
- trades: 0,
27094
- dividends: 0,
27095
- adjustments: 0,
27096
- forex: 0
27097
- },
27098
- alreadyPreprocessed: true
27099
- };
27099
+ existingJournalContent = fs22.readFileSync(journalPath, "utf-8");
27100
+ logger?.info(`Found existing IBKR journal: ${journalPath}, will check for new entries`);
27100
27101
  }
27101
27102
  const content = fs22.readFileSync(csvPath, "utf-8");
27102
27103
  const transactions = parseIbkrCsv(content);
@@ -27127,7 +27128,6 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27127
27128
  const usedAccounts = new Set;
27128
27129
  const journalEntries = [];
27129
27130
  for (const trade of trades) {
27130
- tradedSymbols.add(trade.symbol);
27131
27131
  const totalCost = Math.abs(trade.grossAmount);
27132
27132
  const unitPrice = trade.quantity !== 0 ? totalCost / Math.abs(trade.quantity) : 0;
27133
27133
  const tradeEntry = {
@@ -27143,6 +27143,11 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27143
27143
  currency: currency.toUpperCase()
27144
27144
  };
27145
27145
  if (trade.transactionType === "Buy") {
27146
+ const entry = generateIbkrBuyEntry(tradeEntry, logger);
27147
+ if (isIbkrEntryDuplicate(entry, existingJournalContent)) {
27148
+ logger?.debug(`Skipping duplicate Buy: ${trade.date} ${trade.symbol}`);
27149
+ continue;
27150
+ }
27146
27151
  const tradeInfo = {
27147
27152
  date: trade.date,
27148
27153
  orderNum: "",
@@ -27153,13 +27158,19 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27153
27158
  currency: currency.toUpperCase()
27154
27159
  };
27155
27160
  addLot(inventory, tradeInfo, logger);
27156
- journalEntries.push(generateIbkrBuyEntry(tradeEntry, logger));
27161
+ journalEntries.push(entry);
27162
+ tradedSymbols.add(trade.symbol);
27157
27163
  usedAccounts.add(`assets:investments:stocks:${trade.symbol}`);
27158
27164
  usedAccounts.add(`assets:broker:ibkr:${currency}`);
27159
27165
  usedAccounts.add("expenses:fees:trading:ibkr");
27160
27166
  } else {
27167
+ if (existingJournalContent.includes(`${trade.date} Sell ${trade.symbol} - `)) {
27168
+ logger?.debug(`Skipping duplicate Sell: ${trade.date} ${trade.symbol}`);
27169
+ continue;
27170
+ }
27161
27171
  const consumed = consumeLotsFIFO(inventory, trade.symbol, Math.abs(trade.quantity), logger);
27162
27172
  journalEntries.push(generateIbkrSellEntry(tradeEntry, consumed, logger));
27173
+ tradedSymbols.add(trade.symbol);
27163
27174
  usedAccounts.add(`assets:investments:stocks:${trade.symbol}`);
27164
27175
  usedAccounts.add(`assets:broker:ibkr:${currency}`);
27165
27176
  usedAccounts.add("expenses:fees:trading:ibkr");
@@ -27178,74 +27189,36 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27178
27189
  netAmount,
27179
27190
  currency: currency.toUpperCase()
27180
27191
  };
27181
- journalEntries.push(generateIbkrDividendEntry(dividendEntry, logger));
27192
+ const entry = generateIbkrDividendEntry(dividendEntry, logger);
27193
+ if (isIbkrEntryDuplicate(entry, existingJournalContent)) {
27194
+ logger?.debug(`Skipping duplicate Dividend: ${group.date} ${group.symbol}`);
27195
+ continue;
27196
+ }
27197
+ journalEntries.push(entry);
27182
27198
  usedAccounts.add(`assets:broker:ibkr:${currency}`);
27183
27199
  usedAccounts.add(`income:dividends:${group.symbol}`);
27184
27200
  if (group.withholdingTax > 0) {
27185
27201
  usedAccounts.add("expenses:taxes:withholding");
27186
27202
  }
27187
27203
  }
27188
- adjustments.sort((a, b) => a.date.localeCompare(b.date));
27189
- for (const adj of adjustments) {
27190
- const adjustmentEntry = {
27191
- date: adj.date,
27192
- account: adj.account,
27193
- description: adj.description || "FX Translations P&L",
27194
- amount: adj.netAmount,
27195
- currency: currency.toUpperCase()
27196
- };
27197
- journalEntries.push(generateIbkrAdjustmentEntry(adjustmentEntry, logger));
27198
- usedAccounts.add(`assets:broker:ibkr:${currency}`);
27199
- if (adj.netAmount < 0) {
27200
- usedAccounts.add("expenses:fx-losses:ibkr");
27201
- } else {
27202
- usedAccounts.add("income:fx-gains:ibkr");
27203
- }
27204
- }
27205
- if (forexTxns.length > 0) {
27206
- const forexByDate = new Map;
27207
- for (const txn of forexTxns) {
27208
- const existing = forexByDate.get(txn.date);
27209
- if (existing) {
27210
- existing.total += txn.netAmount;
27211
- } else {
27212
- forexByDate.set(txn.date, { account: txn.account, total: txn.netAmount });
27213
- }
27214
- }
27215
- const sortedDates = [...forexByDate.keys()].sort();
27216
- for (const date5 of sortedDates) {
27217
- const { account, total } = forexByDate.get(date5);
27218
- const amount = Math.round(total * 100) / 100;
27219
- if (amount === 0)
27220
- continue;
27221
- const forexEntry = {
27222
- date: date5,
27223
- account,
27224
- description: "Forex conversion cost",
27225
- amount,
27226
- currency: currency.toUpperCase()
27227
- };
27228
- journalEntries.push(generateIbkrForexEntry(forexEntry, logger));
27229
- usedAccounts.add(`assets:broker:ibkr:${currency}`);
27230
- if (amount < 0) {
27231
- usedAccounts.add("expenses:fees:forex:ibkr");
27232
- } else {
27233
- usedAccounts.add("income:fx-gains:ibkr");
27234
- }
27235
- }
27236
- }
27237
27204
  saveLotInventory(directory, lotInventoryPath, inventory, logger);
27238
- let journalFilePath = null;
27205
+ let journalFilePath = existingJournalContent ? journalPath : null;
27239
27206
  if (journalEntries.length > 0) {
27240
- const header = `; IBKR ${currency.toUpperCase()} investment transactions for ${year}
27241
- ; Generated by opencode-accountant
27242
- ; This file is auto-generated - do not edit manually`;
27243
- const journalContent = formatJournalFile(journalEntries, header);
27244
27207
  const investmentsDir = path17.join(directory, "ledger", "investments");
27245
27208
  if (!fs22.existsSync(investmentsDir)) {
27246
27209
  fs22.mkdirSync(investmentsDir, { recursive: true });
27247
27210
  }
27248
- fs22.writeFileSync(journalPath, journalContent);
27211
+ if (existingJournalContent) {
27212
+ fs22.appendFileSync(journalPath, `
27213
+ ` + journalEntries.join(`
27214
+ `));
27215
+ } else {
27216
+ const header = `; IBKR ${currency.toUpperCase()} investment transactions for ${year}
27217
+ ; Generated by opencode-accountant
27218
+ ; This file is auto-generated - do not edit manually`;
27219
+ const journalContent = formatJournalFile(journalEntries, header);
27220
+ fs22.writeFileSync(journalPath, journalContent);
27221
+ }
27249
27222
  journalFilePath = journalPath;
27250
27223
  if (tradedSymbols.size > 0) {
27251
27224
  const commodityJournalPath = path17.join(directory, "ledger", "investments", "commodities.journal");
@@ -27255,13 +27228,15 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27255
27228
  const accountJournalPath = path17.join(directory, "ledger", "investments", "accounts.journal");
27256
27229
  ensureInvestmentAccountDeclarations(accountJournalPath, usedAccounts, logger);
27257
27230
  }
27231
+ ensureStockPriceEntries(directory, tradedSymbols, logger);
27258
27232
  logger?.info(`Generated IBKR journal: ${journalPath} with ${journalEntries.length} entries`);
27259
27233
  }
27234
+ const simpleTransactions = [...deposits, ...adjustments, ...forexTxns];
27260
27235
  let simpleTransactionsCsvPath = null;
27261
- if (deposits.length > 0) {
27236
+ if (simpleTransactions.length > 0) {
27262
27237
  const filteredCsvPath = path17.join(csvDir, `${csvBasename}-filtered.csv`);
27263
27238
  const csvHeader = "Date,Account,Description,Transaction Type,Symbol,Quantity,Price,Price Currency,Gross Amount,Commission,Net Amount";
27264
- const csvRows = deposits.map((d) => [
27239
+ const csvRows = simpleTransactions.map((d) => [
27265
27240
  d.date,
27266
27241
  d.account,
27267
27242
  `"${d.description.replace(/"/g, '""')}"`,
@@ -27279,7 +27254,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27279
27254
  `) + `
27280
27255
  `);
27281
27256
  simpleTransactionsCsvPath = filteredCsvPath;
27282
- logger?.info(`Generated filtered CSV for deposits: ${filteredCsvPath} (${deposits.length} rows)`);
27257
+ logger?.info(`Generated filtered CSV: ${filteredCsvPath} (${simpleTransactions.length} rows)`);
27283
27258
  }
27284
27259
  const dividendCount = dividendGroups.length;
27285
27260
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.16.2",
3
+ "version": "0.16.3-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",