@fuzzle/opencode-accountant 0.16.3 → 0.16.4

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 +57 -131
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -26989,62 +26989,6 @@ function generateIbkrDividendEntry(dividend, logger) {
26989
26989
  `;
26990
26990
  return entry;
26991
26991
  }
26992
- function generateIbkrAdjustmentEntry(adjustment, logger) {
26993
- const description = escapeDescription(adjustment.description);
26994
- logger?.debug(`Generating IBKR Adjustment entry: ${adjustment.amount} ${adjustment.currency}`);
26995
- const postings = [
26996
- {
26997
- account: `assets:broker:ibkr:${adjustment.currency.toLowerCase()}`,
26998
- amount: formatAmount(adjustment.amount, adjustment.currency)
26999
- }
27000
- ];
27001
- if (adjustment.amount < 0) {
27002
- postings.push({
27003
- account: "expenses:fx-losses:ibkr",
27004
- amount: formatAmount(Math.abs(adjustment.amount), adjustment.currency)
27005
- });
27006
- } else {
27007
- postings.push({
27008
- account: "income:fx-gains:ibkr",
27009
- amount: formatAmount(-adjustment.amount, adjustment.currency)
27010
- });
27011
- }
27012
- let entry = `${adjustment.date} ${description}
27013
- `;
27014
- entry += ` ; ibkr:account:${adjustment.account}
27015
- `;
27016
- entry += formatPostings(postings) + `
27017
- `;
27018
- return entry;
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
- }
27048
26992
 
27049
26993
  // src/utils/ibkrCsvPreprocessor.ts
27050
26994
  var TRADE_TYPES2 = new Set(["Buy", "Sell"]);
@@ -27126,25 +27070,34 @@ function groupDividends(transactions) {
27126
27070
  groups.sort((a, b) => a.date.localeCompare(b.date));
27127
27071
  return groups;
27128
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
+ }
27129
27093
  async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPath, logger) {
27130
27094
  const csvDir = path17.dirname(csvPath);
27131
27095
  const csvBasename = path17.basename(csvPath, ".csv");
27132
27096
  const journalPath = path17.join(directory, "ledger", "investments", `${year}-ibkr-${currency}.journal`);
27097
+ let existingJournalContent = "";
27133
27098
  if (fs22.existsSync(journalPath)) {
27134
- logger?.info(`IBKR journal already exists: ${journalPath}, skipping preprocessing`);
27135
- return {
27136
- simpleTransactionsCsv: null,
27137
- journalFile: journalPath,
27138
- stats: {
27139
- totalRows: 0,
27140
- deposits: 0,
27141
- trades: 0,
27142
- dividends: 0,
27143
- adjustments: 0,
27144
- forex: 0
27145
- },
27146
- alreadyPreprocessed: true
27147
- };
27099
+ existingJournalContent = fs22.readFileSync(journalPath, "utf-8");
27100
+ logger?.info(`Found existing IBKR journal: ${journalPath}, will check for new entries`);
27148
27101
  }
27149
27102
  const content = fs22.readFileSync(csvPath, "utf-8");
27150
27103
  const transactions = parseIbkrCsv(content);
@@ -27175,7 +27128,6 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27175
27128
  const usedAccounts = new Set;
27176
27129
  const journalEntries = [];
27177
27130
  for (const trade of trades) {
27178
- tradedSymbols.add(trade.symbol);
27179
27131
  const totalCost = Math.abs(trade.grossAmount);
27180
27132
  const unitPrice = trade.quantity !== 0 ? totalCost / Math.abs(trade.quantity) : 0;
27181
27133
  const tradeEntry = {
@@ -27191,6 +27143,11 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27191
27143
  currency: currency.toUpperCase()
27192
27144
  };
27193
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
+ }
27194
27151
  const tradeInfo = {
27195
27152
  date: trade.date,
27196
27153
  orderNum: "",
@@ -27201,13 +27158,19 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27201
27158
  currency: currency.toUpperCase()
27202
27159
  };
27203
27160
  addLot(inventory, tradeInfo, logger);
27204
- journalEntries.push(generateIbkrBuyEntry(tradeEntry, logger));
27161
+ journalEntries.push(entry);
27162
+ tradedSymbols.add(trade.symbol);
27205
27163
  usedAccounts.add(`assets:investments:stocks:${trade.symbol}`);
27206
27164
  usedAccounts.add(`assets:broker:ibkr:${currency}`);
27207
27165
  usedAccounts.add("expenses:fees:trading:ibkr");
27208
27166
  } else {
27167
+ if (existingJournalContent.includes(`${trade.date} Sell ${trade.symbol} - `)) {
27168
+ logger?.debug(`Skipping duplicate Sell: ${trade.date} ${trade.symbol}`);
27169
+ continue;
27170
+ }
27209
27171
  const consumed = consumeLotsFIFO(inventory, trade.symbol, Math.abs(trade.quantity), logger);
27210
27172
  journalEntries.push(generateIbkrSellEntry(tradeEntry, consumed, logger));
27173
+ tradedSymbols.add(trade.symbol);
27211
27174
  usedAccounts.add(`assets:investments:stocks:${trade.symbol}`);
27212
27175
  usedAccounts.add(`assets:broker:ibkr:${currency}`);
27213
27176
  usedAccounts.add("expenses:fees:trading:ibkr");
@@ -27226,74 +27189,36 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27226
27189
  netAmount,
27227
27190
  currency: currency.toUpperCase()
27228
27191
  };
27229
- 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);
27230
27198
  usedAccounts.add(`assets:broker:ibkr:${currency}`);
27231
27199
  usedAccounts.add(`income:dividends:${group.symbol}`);
27232
27200
  if (group.withholdingTax > 0) {
27233
27201
  usedAccounts.add("expenses:taxes:withholding");
27234
27202
  }
27235
27203
  }
27236
- adjustments.sort((a, b) => a.date.localeCompare(b.date));
27237
- for (const adj of adjustments) {
27238
- const adjustmentEntry = {
27239
- date: adj.date,
27240
- account: adj.account,
27241
- description: adj.description || "FX Translations P&L",
27242
- amount: adj.netAmount,
27243
- currency: currency.toUpperCase()
27244
- };
27245
- journalEntries.push(generateIbkrAdjustmentEntry(adjustmentEntry, logger));
27246
- usedAccounts.add(`assets:broker:ibkr:${currency}`);
27247
- if (adj.netAmount < 0) {
27248
- usedAccounts.add("expenses:fx-losses:ibkr");
27249
- } else {
27250
- usedAccounts.add("income:fx-gains:ibkr");
27251
- }
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
- }
27285
27204
  saveLotInventory(directory, lotInventoryPath, inventory, logger);
27286
- let journalFilePath = null;
27205
+ let journalFilePath = existingJournalContent ? journalPath : null;
27287
27206
  if (journalEntries.length > 0) {
27288
- const header = `; IBKR ${currency.toUpperCase()} investment transactions for ${year}
27289
- ; Generated by opencode-accountant
27290
- ; This file is auto-generated - do not edit manually`;
27291
- const journalContent = formatJournalFile(journalEntries, header);
27292
27207
  const investmentsDir = path17.join(directory, "ledger", "investments");
27293
27208
  if (!fs22.existsSync(investmentsDir)) {
27294
27209
  fs22.mkdirSync(investmentsDir, { recursive: true });
27295
27210
  }
27296
- 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
+ }
27297
27222
  journalFilePath = journalPath;
27298
27223
  if (tradedSymbols.size > 0) {
27299
27224
  const commodityJournalPath = path17.join(directory, "ledger", "investments", "commodities.journal");
@@ -27306,11 +27231,12 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27306
27231
  ensureStockPriceEntries(directory, tradedSymbols, logger);
27307
27232
  logger?.info(`Generated IBKR journal: ${journalPath} with ${journalEntries.length} entries`);
27308
27233
  }
27234
+ const simpleTransactions = [...deposits, ...adjustments, ...forexTxns];
27309
27235
  let simpleTransactionsCsvPath = null;
27310
- if (deposits.length > 0) {
27236
+ if (simpleTransactions.length > 0) {
27311
27237
  const filteredCsvPath = path17.join(csvDir, `${csvBasename}-filtered.csv`);
27312
27238
  const csvHeader = "Date,Account,Description,Transaction Type,Symbol,Quantity,Price,Price Currency,Gross Amount,Commission,Net Amount";
27313
- const csvRows = deposits.map((d) => [
27239
+ const csvRows = simpleTransactions.map((d) => [
27314
27240
  d.date,
27315
27241
  d.account,
27316
27242
  `"${d.description.replace(/"/g, '""')}"`,
@@ -27328,7 +27254,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27328
27254
  `) + `
27329
27255
  `);
27330
27256
  simpleTransactionsCsvPath = filteredCsvPath;
27331
- logger?.info(`Generated filtered CSV for deposits: ${filteredCsvPath} (${deposits.length} rows)`);
27257
+ logger?.info(`Generated filtered CSV: ${filteredCsvPath} (${simpleTransactions.length} rows)`);
27332
27258
  }
27333
27259
  const dividendCount = dividendGroups.length;
27334
27260
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.16.3",
3
+ "version": "0.16.4",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",