@fuzzle/opencode-accountant 0.13.16 → 0.13.17-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 +163 -65
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -25306,31 +25306,41 @@ function generateSplitEntry(action, oldQuantity, newQuantity, totalCostBasis, cu
25306
25306
  logger?.debug(`Generating ${splitType} entry: ${oldQuantity} -> ${newQuantity} ${action.symbol}`);
25307
25307
  const commodity = formatCommodity(action.symbol);
25308
25308
  const totalCost = formatAmount2(totalCostBasis, currency);
25309
- const postings = [
25309
+ const conversionAccount = `equity:conversion:${currency.toLowerCase()}`;
25310
+ const outgoingPostings = [
25310
25311
  {
25311
25312
  account: `assets:investments:stocks:${action.symbol}`,
25312
25313
  amount: `-${formatQuantity(oldQuantity)} ${commodity} @@ ${totalCost}`
25313
25314
  },
25314
25315
  {
25315
- account: "equity:conversion",
25316
- amount: `${formatQuantity(oldQuantity)} ${commodity} @@ ${totalCost}`
25317
- },
25316
+ account: conversionAccount,
25317
+ amount: totalCost
25318
+ }
25319
+ ];
25320
+ const incomingPostings = [
25318
25321
  {
25319
- account: "equity:conversion",
25320
- amount: `-${formatQuantity(newQuantity)} ${commodity} @@ ${totalCost}`
25322
+ account: conversionAccount,
25323
+ amount: formatAmount2(-totalCostBasis, currency)
25321
25324
  },
25322
25325
  {
25323
25326
  account: `assets:investments:stocks:${action.symbol}`,
25324
25327
  amount: `${formatQuantity(newQuantity)} ${commodity} @@ ${totalCost}`
25325
25328
  }
25326
25329
  ];
25330
+ const metadata = ` ; swissquote:order:${action.orderNum} isin:${action.isin}
25331
+ ` + ` ; Ratio: ${ratio.toFixed(4)} (${oldQuantity} -> ${newQuantity})
25332
+ `;
25327
25333
  let entry = `${date5} ${description}
25328
25334
  `;
25329
- entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
25335
+ entry += metadata;
25336
+ entry += formatPostings(outgoingPostings) + `
25330
25337
  `;
25331
- entry += ` ; Ratio: ${ratio.toFixed(4)} (${oldQuantity} -> ${newQuantity})
25338
+ entry += `
25332
25339
  `;
25333
- entry += formatPostings(postings) + `
25340
+ entry += `${date5} ${description}
25341
+ `;
25342
+ entry += metadata;
25343
+ entry += formatPostings(incomingPostings) + `
25334
25344
  `;
25335
25345
  return entry;
25336
25346
  }
@@ -25371,43 +25381,101 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossC
25371
25381
  logger?.debug(`Generating multi-way merger entry: ${outSymbols.join(", ")} -> ${inSymbols.join(", ")}`);
25372
25382
  const oldIsins = (crossCurrencyOutgoingIsins ?? group.outgoing.map((a) => a.isin)).filter(Boolean);
25373
25383
  const newIsins = group.incoming.map((a) => a.isin).filter(Boolean);
25374
- const postings = [];
25384
+ let metadata = ` ; swissquote:order:${group.orderNum}
25385
+ `;
25386
+ if (oldIsins.length > 0 || newIsins.length > 0) {
25387
+ metadata += ` ; Old ISIN: ${oldIsins.join(", ") || "n/a"}, New ISINs: ${newIsins.join(", ") || "n/a"}
25388
+ `;
25389
+ }
25390
+ const entries = [];
25375
25391
  for (let i2 = 0;i2 < group.outgoing.length; i2++) {
25376
25392
  const out = group.outgoing[i2];
25377
25393
  const qty = formatQuantity(Math.abs(out.quantity));
25378
25394
  const commodity = formatCommodity(out.symbol);
25379
- const costAnnotation = costInfo && costInfo.outgoingTotalCosts[i2] != null ? ` @@ ${formatAmount2(costInfo.outgoingTotalCosts[i2], costInfo.currency)}` : "";
25380
- postings.push({
25381
- account: `assets:investments:stocks:${out.symbol}`,
25382
- amount: `-${qty} ${commodity}${costAnnotation}`
25383
- });
25384
- postings.push({ account: "equity:conversion", amount: `${qty} ${commodity}${costAnnotation}` });
25395
+ if (costInfo && costInfo.outgoingTotalCosts[i2] != null) {
25396
+ const costAmount = formatAmount2(costInfo.outgoingTotalCosts[i2], costInfo.currency);
25397
+ const postings = [
25398
+ {
25399
+ account: `assets:investments:stocks:${out.symbol}`,
25400
+ amount: `-${qty} ${commodity} @@ ${costAmount}`
25401
+ },
25402
+ {
25403
+ account: `equity:conversion:${costInfo.currency.toLowerCase()}`,
25404
+ amount: costAmount
25405
+ }
25406
+ ];
25407
+ let sub = `${date5} ${description}
25408
+ `;
25409
+ sub += metadata;
25410
+ sub += formatPostings(postings) + `
25411
+ `;
25412
+ entries.push(sub);
25413
+ } else {
25414
+ const postings = [
25415
+ {
25416
+ account: `assets:investments:stocks:${out.symbol}`,
25417
+ amount: `-${qty} ${commodity}`
25418
+ }
25419
+ ];
25420
+ let sub = `${date5} ${description}
25421
+ `;
25422
+ sub += metadata;
25423
+ sub += formatPostings(postings) + `
25424
+ `;
25425
+ entries.push(sub);
25426
+ }
25385
25427
  }
25386
25428
  for (let i2 = 0;i2 < group.incoming.length; i2++) {
25387
25429
  const inc = group.incoming[i2];
25388
25430
  const qty = formatQuantity(Math.abs(inc.quantity));
25389
25431
  const commodity = formatCommodity(inc.symbol);
25390
- const costAnnotation = costInfo && costInfo.incomingTotalCosts[i2] != null ? ` @@ ${formatAmount2(costInfo.incomingTotalCosts[i2], costInfo.currency)}` : "";
25391
- postings.push({
25392
- account: "equity:conversion",
25393
- amount: `-${qty} ${commodity}${costAnnotation}`
25394
- });
25395
- postings.push({
25396
- account: `assets:investments:stocks:${inc.symbol}`,
25397
- amount: `${qty} ${commodity}${costAnnotation}`
25398
- });
25399
- }
25400
- let entry = `${date5} ${description}
25432
+ if (costInfo && costInfo.incomingTotalCosts[i2] != null) {
25433
+ const costAmount = formatAmount2(costInfo.incomingTotalCosts[i2], costInfo.currency);
25434
+ const postings = [
25435
+ {
25436
+ account: `equity:conversion:${costInfo.currency.toLowerCase()}`,
25437
+ amount: formatAmount2(-costInfo.incomingTotalCosts[i2], costInfo.currency)
25438
+ },
25439
+ {
25440
+ account: `assets:investments:stocks:${inc.symbol}`,
25441
+ amount: `${qty} ${commodity} @@ ${costAmount}`
25442
+ }
25443
+ ];
25444
+ let sub = `${date5} ${description}
25401
25445
  `;
25402
- entry += ` ; swissquote:order:${group.orderNum}
25446
+ sub += metadata;
25447
+ sub += formatPostings(postings) + `
25403
25448
  `;
25404
- if (oldIsins.length > 0 || newIsins.length > 0) {
25405
- entry += ` ; Old ISIN: ${oldIsins.join(", ") || "n/a"}, New ISINs: ${newIsins.join(", ") || "n/a"}
25449
+ entries.push(sub);
25450
+ } else {
25451
+ const postings = [
25452
+ {
25453
+ account: `assets:investments:stocks:${inc.symbol}`,
25454
+ amount: `${qty} ${commodity}`
25455
+ }
25456
+ ];
25457
+ let sub = `${date5} ${description}
25458
+ `;
25459
+ sub += metadata;
25460
+ sub += formatPostings(postings) + `
25406
25461
  `;
25462
+ entries.push(sub);
25463
+ }
25407
25464
  }
25408
- entry += formatPostings(postings) + `
25465
+ if (group.cashSettlement) {
25466
+ const cashCurrency = group.cashSettlement.currency.toLowerCase();
25467
+ const cashAmount = formatAmount2(group.cashSettlement.amount, group.cashSettlement.currency);
25468
+ let sub = `${date5} ${description}
25409
25469
  `;
25410
- return entry;
25470
+ sub += metadata;
25471
+ sub += ` assets:broker:swissquote:${cashCurrency} ${cashAmount}
25472
+ `;
25473
+ sub += ` income:capital-gains:realized
25474
+ `;
25475
+ entries.push(sub);
25476
+ }
25477
+ return entries.join(`
25478
+ `);
25411
25479
  }
25412
25480
  function generateRightsDistributionEntry(action, logger) {
25413
25481
  const date5 = formatDate(action.date);
@@ -25430,28 +25498,23 @@ function generateRightsDistributionEntry(action, logger) {
25430
25498
  `;
25431
25499
  return entry;
25432
25500
  }
25433
- function generatePendingMergerWorthlessEntry(date5, orderNum, outgoingSymbols, outgoingIsins, outgoingQuantities, totalCostBasis, currency, logger) {
25501
+ function generateDirectWorthlessEntry(date5, orderNum, outgoingSymbols, outgoingIsins, outgoingQuantities, outgoingTotalCosts, totalCostBasis, currency, logger) {
25434
25502
  const formattedDate = formatDate(date5);
25435
25503
  const symbols = outgoingSymbols.join(" + ");
25436
- const description = escapeDescription(`Worthless: ${symbols} (unresolved merger)`);
25437
- logger?.debug(`Generating pending merger worthless entry: ${symbols}, loss: ${totalCostBasis.toFixed(2)} ${currency}`);
25504
+ const description = escapeDescription(`Worthless liquidation: ${symbols}`);
25505
+ logger?.debug(`Generating direct worthless entry: ${symbols}, loss: ${totalCostBasis.toFixed(2)} ${currency}`);
25438
25506
  const postings = [];
25439
- if (outgoingQuantities.length > 0) {
25440
- const totalQty = outgoingQuantities.reduce((a, b) => a + b, 0);
25441
- for (let i2 = 0;i2 < outgoingSymbols.length; i2++) {
25442
- const symbol2 = outgoingSymbols[i2];
25443
- const quantity = outgoingQuantities[i2] || 0;
25444
- if (quantity <= 0)
25445
- continue;
25446
- const proportion = totalQty > 0 ? quantity / totalQty : 0;
25447
- const costForSymbol = totalCostBasis * proportion;
25448
- const costBasisPerUnit = costForSymbol / quantity;
25449
- const commodity = formatCommodity(symbol2);
25450
- postings.push({
25451
- account: "equity:conversion",
25452
- amount: `-${formatQuantity(quantity)} ${commodity} @ ${formatPrice(costBasisPerUnit)} ${currency}`
25453
- });
25454
- }
25507
+ for (let i2 = 0;i2 < outgoingSymbols.length; i2++) {
25508
+ const symbol2 = outgoingSymbols[i2];
25509
+ const quantity = outgoingQuantities[i2] || 0;
25510
+ const cost = outgoingTotalCosts[i2] || 0;
25511
+ if (quantity <= 0)
25512
+ continue;
25513
+ const commodity = formatCommodity(symbol2);
25514
+ postings.push({
25515
+ account: `assets:investments:stocks:${symbol2}`,
25516
+ amount: `-${formatQuantity(quantity)} ${commodity} @@ ${formatAmount2(cost, currency)}`
25517
+ });
25455
25518
  }
25456
25519
  postings.push({
25457
25520
  account: "expenses:losses:capital",
@@ -25461,7 +25524,7 @@ function generatePendingMergerWorthlessEntry(date5, orderNum, outgoingSymbols, o
25461
25524
  `;
25462
25525
  entry += ` ; swissquote:order:${orderNum}
25463
25526
  `;
25464
- entry += ` ; Pending merger resolved as worthless - total loss: ${totalCostBasis.toFixed(2)} ${currency}
25527
+ entry += ` ; total loss: ${totalCostBasis.toFixed(2)} ${currency}
25465
25528
  `;
25466
25529
  if (outgoingIsins.length > 0) {
25467
25530
  entry += ` ; Original ISINs: ${outgoingIsins.join(", ")}
@@ -25699,6 +25762,17 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25699
25762
  if (group.outgoing.length === 0 && group.incoming.length > 0) {
25700
25763
  const pendingState = loadPendingMerger(projectDir, lotInventoryPath, group.key, logger);
25701
25764
  if (pendingState) {
25765
+ for (let i2 = 0;i2 < pendingState.outgoingSymbols.length; i2++) {
25766
+ group.outgoing.push({
25767
+ date: pendingState.date,
25768
+ orderNum: pendingState.orderNum,
25769
+ type: "Internal exchange of securities",
25770
+ symbol: pendingState.outgoingSymbols[i2],
25771
+ name: "",
25772
+ isin: pendingState.outgoingIsins[i2] || "",
25773
+ quantity: -(pendingState.outgoingQuantities[i2] || 0)
25774
+ });
25775
+ }
25702
25776
  const totalIncomingQty2 = group.incoming.reduce((sum, a) => sum + Math.abs(a.quantity), 0);
25703
25777
  const incomingTotalCosts2 = [];
25704
25778
  for (const inc of group.incoming) {
@@ -25722,11 +25796,11 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25722
25796
  logger?.info(`Cross-currency merger incoming: ${absQty} ${inc.symbol} @ ${costBasisPerUnit.toFixed(2)} ${pendingState.currency}`);
25723
25797
  }
25724
25798
  const costInfo2 = {
25725
- outgoingTotalCosts: [],
25799
+ outgoingTotalCosts: pendingState.outgoingTotalCosts || [],
25726
25800
  incomingTotalCosts: incomingTotalCosts2,
25727
25801
  currency: pendingState.currency
25728
25802
  };
25729
- const entry2 = generateMultiWayMergerEntry(group, pendingState.outgoingSymbols, pendingState.outgoingIsins, costInfo2, logger);
25803
+ const entry2 = generateMultiWayMergerEntry(group, undefined, undefined, costInfo2, logger);
25730
25804
  entries.push(entry2);
25731
25805
  removePendingMerger(projectDir, lotInventoryPath, group.key, logger);
25732
25806
  } else {
@@ -25764,16 +25838,10 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25764
25838
  outgoingSymbols,
25765
25839
  outgoingIsins: group.outgoing.map((a) => a.isin),
25766
25840
  outgoingQuantities: group.outgoing.map((a) => Math.abs(a.quantity)),
25841
+ outgoingTotalCosts,
25767
25842
  totalCostBasis,
25768
25843
  currency: costCurrency
25769
25844
  }, logger);
25770
- const costInfo2 = {
25771
- outgoingTotalCosts,
25772
- incomingTotalCosts: [],
25773
- currency: costCurrency
25774
- };
25775
- const entry2 = generateMultiWayMergerEntry(group, undefined, undefined, costInfo2, logger);
25776
- entries.push(entry2);
25777
25845
  logger?.info(`Cross-currency merger outgoing: ${outgoingSymbols.join(", ")} -> pending (cost basis: ${totalCostBasis.toFixed(2)})`);
25778
25846
  return entries;
25779
25847
  }
@@ -25871,7 +25939,7 @@ function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
25871
25939
  continue;
25872
25940
  }
25873
25941
  const year = parseInt(dateMatch[1], 10);
25874
- const entry = generatePendingMergerWorthlessEntry(state.date, state.orderNum, state.outgoingSymbols, state.outgoingIsins, state.outgoingQuantities || [], state.totalCostBasis, state.currency, logger);
25942
+ const entry = generateDirectWorthlessEntry(state.date, state.orderNum, state.outgoingSymbols, state.outgoingIsins, state.outgoingQuantities || [], state.outgoingTotalCosts || [], state.totalCostBasis, state.currency, logger);
25875
25943
  const journalFile = path13.join(projectDir, "ledger", "investments", `${year}-${state.currency.toLowerCase()}.journal`);
25876
25944
  if (!entriesByJournal.has(journalFile)) {
25877
25945
  entriesByJournal.set(journalFile, []);
@@ -25978,6 +26046,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
25978
26046
  const dividends = [];
25979
26047
  const corporateActions = [];
25980
26048
  const forexTransactions = [];
26049
+ const mergerCashSettlements = [];
25981
26050
  for (const txn of transactions) {
25982
26051
  const category = categorizeTransaction(txn);
25983
26052
  switch (category) {
@@ -25996,8 +26065,12 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
25996
26065
  case "corporate":
25997
26066
  if (!txn.symbol || txn.symbol.trim() === "") {
25998
26067
  if (txn.netAmount && txn.netAmount !== "-") {
25999
- simpleTransactions.push(txn);
26000
- stats.simpleTransactions++;
26068
+ mergerCashSettlements.push({
26069
+ dateOrderKey: `${txn.date}-${txn.orderNum}`,
26070
+ amount: parseNumber(txn.netAmount),
26071
+ currency: txn.currency
26072
+ });
26073
+ stats.corporateActions++;
26001
26074
  } else {
26002
26075
  stats.skipped++;
26003
26076
  }
@@ -26041,6 +26114,31 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26041
26114
  }
26042
26115
  }
26043
26116
  const mergerGroups = groupMergerActions(mergerActions);
26117
+ for (const cash of mergerCashSettlements) {
26118
+ const matchingGroup = mergerGroups.find((g) => g.key === cash.dateOrderKey);
26119
+ if (matchingGroup) {
26120
+ matchingGroup.cashSettlement = { amount: cash.amount, currency: cash.currency };
26121
+ } else {
26122
+ simpleTransactions.push({
26123
+ date: cash.dateOrderKey.split("-").slice(0, 3).join("-"),
26124
+ orderNum: cash.dateOrderKey.split("-").slice(3).join("-"),
26125
+ transaction: "Merger",
26126
+ symbol: "",
26127
+ name: "",
26128
+ isin: "",
26129
+ quantity: "",
26130
+ unitPrice: "",
26131
+ costs: "",
26132
+ accruedInterest: "",
26133
+ netAmount: String(cash.amount),
26134
+ balance: "",
26135
+ currency: cash.currency
26136
+ });
26137
+ stats.simpleTransactions++;
26138
+ stats.corporateActions--;
26139
+ logger?.warn(`Cash settlement ${cash.dateOrderKey} has no matching merger group \u2014 routed to simple CSV`);
26140
+ }
26141
+ }
26044
26142
  for (const group of mergerGroups) {
26045
26143
  timeline.push({ kind: "mergerGroup", sortDate: formatDate(group.date), group });
26046
26144
  }
@@ -26178,7 +26276,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26178
26276
  investmentAccounts.add("income:capital-gains:realized");
26179
26277
  investmentAccounts.add("income:capital-gains:rights-distribution");
26180
26278
  investmentAccounts.add("expenses:losses:capital");
26181
- investmentAccounts.add("equity:conversion");
26279
+ investmentAccounts.add(`equity:conversion:${currency.toLowerCase()}`);
26182
26280
  investmentAccounts.add("equity:rounding");
26183
26281
  ensureInvestmentAccountDeclarations(accountJournalPath, investmentAccounts, logger);
26184
26282
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.13.16",
3
+ "version": "0.13.17-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",