@fuzzle/opencode-accountant 0.13.15 → 0.13.16-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 +158 -61
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -25298,32 +25298,49 @@ function generateDividendEntry(dividend, logger) {
25298
25298
  `;
25299
25299
  return entry;
25300
25300
  }
25301
- function generateSplitEntry(action, oldQuantity, newQuantity, logger) {
25301
+ function generateSplitEntry(action, oldQuantity, newQuantity, totalCostBasis, currency, logger) {
25302
25302
  const date5 = formatDate(action.date);
25303
25303
  const ratio = action.ratio || newQuantity / oldQuantity;
25304
25304
  const splitType = ratio > 1 ? "Split" : "Reverse Split";
25305
25305
  const description = escapeDescription(`${splitType}: ${action.symbol} (${action.name})`);
25306
25306
  logger?.debug(`Generating ${splitType} entry: ${oldQuantity} -> ${newQuantity} ${action.symbol}`);
25307
25307
  const commodity = formatCommodity(action.symbol);
25308
- const postings = [
25308
+ const totalCost = formatAmount2(totalCostBasis, currency);
25309
+ const conversionAccount = `equity:conversion:${currency.toLowerCase()}`;
25310
+ const outgoingPostings = [
25309
25311
  {
25310
25312
  account: `assets:investments:stocks:${action.symbol}`,
25311
- amount: `-${formatQuantity(oldQuantity)} ${commodity}`
25313
+ amount: `-${formatQuantity(oldQuantity)} ${commodity} @@ ${totalCost}`
25314
+ },
25315
+ {
25316
+ account: conversionAccount,
25317
+ amount: totalCost
25318
+ }
25319
+ ];
25320
+ const incomingPostings = [
25321
+ {
25322
+ account: conversionAccount,
25323
+ amount: formatAmount2(-totalCostBasis, currency)
25312
25324
  },
25313
- { account: "equity:conversion", amount: `${formatQuantity(oldQuantity)} ${commodity}` },
25314
- { account: "equity:conversion", amount: `-${formatQuantity(newQuantity)} ${commodity}` },
25315
25325
  {
25316
25326
  account: `assets:investments:stocks:${action.symbol}`,
25317
- amount: `${formatQuantity(newQuantity)} ${commodity}`
25327
+ amount: `${formatQuantity(newQuantity)} ${commodity} @@ ${totalCost}`
25318
25328
  }
25319
25329
  ];
25330
+ const metadata = ` ; swissquote:order:${action.orderNum} isin:${action.isin}
25331
+ ` + ` ; Ratio: ${ratio.toFixed(4)} (${oldQuantity} -> ${newQuantity})
25332
+ `;
25320
25333
  let entry = `${date5} ${description}
25321
25334
  `;
25322
- entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
25335
+ entry += metadata;
25336
+ entry += formatPostings(outgoingPostings) + `
25323
25337
  `;
25324
- entry += ` ; Ratio: ${ratio.toFixed(4)} (${oldQuantity} -> ${newQuantity})
25338
+ entry += `
25325
25339
  `;
25326
- entry += formatPostings(postings) + `
25340
+ entry += `${date5} ${description}
25341
+ `;
25342
+ entry += metadata;
25343
+ entry += formatPostings(incomingPostings) + `
25327
25344
  `;
25328
25345
  return entry;
25329
25346
  }
@@ -25355,7 +25372,7 @@ function generateWorthlessEntry(action, removedLots, logger) {
25355
25372
  `;
25356
25373
  return entry;
25357
25374
  }
25358
- function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossCurrencyOutgoingIsins, logger) {
25375
+ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossCurrencyOutgoingIsins, costInfo, logger) {
25359
25376
  const date5 = formatDate(group.date);
25360
25377
  const outSymbols = crossCurrencyOutgoingSymbols ?? group.outgoing.map((a) => a.symbol);
25361
25378
  const inSymbols = group.incoming.map((a) => a.symbol);
@@ -25364,36 +25381,89 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossC
25364
25381
  logger?.debug(`Generating multi-way merger entry: ${outSymbols.join(", ")} -> ${inSymbols.join(", ")}`);
25365
25382
  const oldIsins = (crossCurrencyOutgoingIsins ?? group.outgoing.map((a) => a.isin)).filter(Boolean);
25366
25383
  const newIsins = group.incoming.map((a) => a.isin).filter(Boolean);
25367
- const postings = [];
25368
- for (const out of group.outgoing) {
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 = [];
25391
+ for (let i2 = 0;i2 < group.outgoing.length; i2++) {
25392
+ const out = group.outgoing[i2];
25369
25393
  const qty = formatQuantity(Math.abs(out.quantity));
25370
25394
  const commodity = formatCommodity(out.symbol);
25371
- postings.push({
25372
- account: `assets:investments:stocks:${out.symbol}`,
25373
- amount: `-${qty} ${commodity}`
25374
- });
25375
- postings.push({ account: "equity:conversion", amount: `${qty} ${commodity}` });
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
+ }
25376
25427
  }
25377
- for (const inc of group.incoming) {
25428
+ for (let i2 = 0;i2 < group.incoming.length; i2++) {
25429
+ const inc = group.incoming[i2];
25378
25430
  const qty = formatQuantity(Math.abs(inc.quantity));
25379
25431
  const commodity = formatCommodity(inc.symbol);
25380
- postings.push({ account: "equity:conversion", amount: `-${qty} ${commodity}` });
25381
- postings.push({
25382
- account: `assets:investments:stocks:${inc.symbol}`,
25383
- amount: `${qty} ${commodity}`
25384
- });
25385
- }
25386
- 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}
25387
25445
  `;
25388
- entry += ` ; swissquote:order:${group.orderNum}
25446
+ sub += metadata;
25447
+ sub += formatPostings(postings) + `
25389
25448
  `;
25390
- if (oldIsins.length > 0 || newIsins.length > 0) {
25391
- 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}
25392
25458
  `;
25393
- }
25394
- entry += formatPostings(postings) + `
25459
+ sub += metadata;
25460
+ sub += formatPostings(postings) + `
25395
25461
  `;
25396
- return entry;
25462
+ entries.push(sub);
25463
+ }
25464
+ }
25465
+ return entries.join(`
25466
+ `);
25397
25467
  }
25398
25468
  function generateRightsDistributionEntry(action, logger) {
25399
25469
  const date5 = formatDate(action.date);
@@ -25416,28 +25486,23 @@ function generateRightsDistributionEntry(action, logger) {
25416
25486
  `;
25417
25487
  return entry;
25418
25488
  }
25419
- function generatePendingMergerWorthlessEntry(date5, orderNum, outgoingSymbols, outgoingIsins, outgoingQuantities, totalCostBasis, currency, logger) {
25489
+ function generateDirectWorthlessEntry(date5, orderNum, outgoingSymbols, outgoingIsins, outgoingQuantities, outgoingTotalCosts, totalCostBasis, currency, logger) {
25420
25490
  const formattedDate = formatDate(date5);
25421
25491
  const symbols = outgoingSymbols.join(" + ");
25422
- const description = escapeDescription(`Worthless: ${symbols} (unresolved merger)`);
25423
- logger?.debug(`Generating pending merger worthless entry: ${symbols}, loss: ${totalCostBasis.toFixed(2)} ${currency}`);
25492
+ const description = escapeDescription(`Worthless liquidation: ${symbols}`);
25493
+ logger?.debug(`Generating direct worthless entry: ${symbols}, loss: ${totalCostBasis.toFixed(2)} ${currency}`);
25424
25494
  const postings = [];
25425
- if (outgoingQuantities.length > 0) {
25426
- const totalQty = outgoingQuantities.reduce((a, b) => a + b, 0);
25427
- for (let i2 = 0;i2 < outgoingSymbols.length; i2++) {
25428
- const symbol2 = outgoingSymbols[i2];
25429
- const quantity = outgoingQuantities[i2] || 0;
25430
- if (quantity <= 0)
25431
- continue;
25432
- const proportion = totalQty > 0 ? quantity / totalQty : 0;
25433
- const costForSymbol = totalCostBasis * proportion;
25434
- const costBasisPerUnit = costForSymbol / quantity;
25435
- const commodity = formatCommodity(symbol2);
25436
- postings.push({
25437
- account: "equity:conversion",
25438
- amount: `-${formatQuantity(quantity)} ${commodity} @ ${formatPrice(costBasisPerUnit)} ${currency}`
25439
- });
25440
- }
25495
+ for (let i2 = 0;i2 < outgoingSymbols.length; i2++) {
25496
+ const symbol2 = outgoingSymbols[i2];
25497
+ const quantity = outgoingQuantities[i2] || 0;
25498
+ const cost = outgoingTotalCosts[i2] || 0;
25499
+ if (quantity <= 0)
25500
+ continue;
25501
+ const commodity = formatCommodity(symbol2);
25502
+ postings.push({
25503
+ account: `assets:investments:stocks:${symbol2}`,
25504
+ amount: `-${formatQuantity(quantity)} ${commodity} @@ ${formatAmount2(cost, currency)}`
25505
+ });
25441
25506
  }
25442
25507
  postings.push({
25443
25508
  account: "expenses:losses:capital",
@@ -25447,7 +25512,7 @@ function generatePendingMergerWorthlessEntry(date5, orderNum, outgoingSymbols, o
25447
25512
  `;
25448
25513
  entry += ` ; swissquote:order:${orderNum}
25449
25514
  `;
25450
- entry += ` ; Pending merger resolved as worthless - total loss: ${totalCostBasis.toFixed(2)} ${currency}
25515
+ entry += ` ; total loss: ${totalCostBasis.toFixed(2)} ${currency}
25451
25516
  `;
25452
25517
  if (outgoingIsins.length > 0) {
25453
25518
  entry += ` ; Original ISINs: ${outgoingIsins.join(", ")}
@@ -25624,9 +25689,12 @@ function processNonMergerAction(action, inventory, entries, logger) {
25624
25689
  const oldQty = getHeldQuantity(inventory, action.symbol);
25625
25690
  const newQty = Math.abs(qty);
25626
25691
  if (oldQty > 0) {
25692
+ const lots = inventory[action.symbol] || [];
25693
+ const totalCostBasis = lots.reduce((sum, lot) => sum + lot.quantity * lot.costBasis, 0);
25694
+ const currency = lots[0]?.currency || "CAD";
25627
25695
  const ratio = newQty / oldQty;
25628
25696
  adjustLotsForSplit(inventory, action.symbol, ratio, logger);
25629
- const entry = generateSplitEntry(action, oldQty, newQty, logger);
25697
+ const entry = generateSplitEntry(action, oldQty, newQty, totalCostBasis, currency, logger);
25630
25698
  entries.push(entry);
25631
25699
  } else {
25632
25700
  logger?.warn(`Reverse split for ${action.symbol} but no lots found`);
@@ -25682,12 +25750,25 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25682
25750
  if (group.outgoing.length === 0 && group.incoming.length > 0) {
25683
25751
  const pendingState = loadPendingMerger(projectDir, lotInventoryPath, group.key, logger);
25684
25752
  if (pendingState) {
25753
+ for (let i2 = 0;i2 < pendingState.outgoingSymbols.length; i2++) {
25754
+ group.outgoing.push({
25755
+ date: pendingState.date,
25756
+ orderNum: pendingState.orderNum,
25757
+ type: "Internal exchange of securities",
25758
+ symbol: pendingState.outgoingSymbols[i2],
25759
+ name: "",
25760
+ isin: pendingState.outgoingIsins[i2] || "",
25761
+ quantity: -(pendingState.outgoingQuantities[i2] || 0)
25762
+ });
25763
+ }
25685
25764
  const totalIncomingQty2 = group.incoming.reduce((sum, a) => sum + Math.abs(a.quantity), 0);
25765
+ const incomingTotalCosts2 = [];
25686
25766
  for (const inc of group.incoming) {
25687
25767
  const absQty = Math.abs(inc.quantity);
25688
25768
  const proportion = absQty / totalIncomingQty2;
25689
25769
  const allocatedCost = pendingState.totalCostBasis * proportion;
25690
25770
  const costBasisPerUnit = absQty > 0 ? allocatedCost / absQty : 0;
25771
+ incomingTotalCosts2.push(allocatedCost);
25691
25772
  const lot = {
25692
25773
  date: formatDate(inc.date),
25693
25774
  quantity: absQty,
@@ -25702,7 +25783,12 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25702
25783
  inventory[inc.symbol].push(lot);
25703
25784
  logger?.info(`Cross-currency merger incoming: ${absQty} ${inc.symbol} @ ${costBasisPerUnit.toFixed(2)} ${pendingState.currency}`);
25704
25785
  }
25705
- const entry2 = generateMultiWayMergerEntry(group, pendingState.outgoingSymbols, pendingState.outgoingIsins, logger);
25786
+ const costInfo2 = {
25787
+ outgoingTotalCosts: pendingState.outgoingTotalCosts || [],
25788
+ incomingTotalCosts: incomingTotalCosts2,
25789
+ currency: pendingState.currency
25790
+ };
25791
+ const entry2 = generateMultiWayMergerEntry(group, undefined, undefined, costInfo2, logger);
25706
25792
  entries.push(entry2);
25707
25793
  removePendingMerger(projectDir, lotInventoryPath, group.key, logger);
25708
25794
  } else {
@@ -25713,18 +25799,23 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25713
25799
  let totalCostBasis = 0;
25714
25800
  let outgoingCurrency = "";
25715
25801
  const outgoingSymbols = [];
25802
+ const outgoingTotalCosts = [];
25716
25803
  for (const out of group.outgoing) {
25717
25804
  outgoingSymbols.push(out.symbol);
25718
25805
  const lots = inventory[out.symbol];
25806
+ let symbolCost = 0;
25719
25807
  if (lots) {
25720
25808
  for (const lot of lots) {
25721
- totalCostBasis += lot.quantity * lot.costBasis;
25809
+ symbolCost += lot.quantity * lot.costBasis;
25722
25810
  if (!outgoingCurrency && lot.currency) {
25723
25811
  outgoingCurrency = lot.currency;
25724
25812
  }
25725
25813
  }
25726
25814
  }
25815
+ totalCostBasis += symbolCost;
25816
+ outgoingTotalCosts.push(symbolCost);
25727
25817
  }
25818
+ const costCurrency = outgoingCurrency || "CAD";
25728
25819
  if (group.outgoing.length > 0 && group.incoming.length === 0) {
25729
25820
  for (const out of group.outgoing) {
25730
25821
  removeLots(inventory, out.symbol, logger);
@@ -25735,11 +25826,10 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25735
25826
  outgoingSymbols,
25736
25827
  outgoingIsins: group.outgoing.map((a) => a.isin),
25737
25828
  outgoingQuantities: group.outgoing.map((a) => Math.abs(a.quantity)),
25829
+ outgoingTotalCosts,
25738
25830
  totalCostBasis,
25739
- currency: outgoingCurrency || "CAD"
25831
+ currency: costCurrency
25740
25832
  }, logger);
25741
- const entry2 = generateMultiWayMergerEntry(group, undefined, undefined, logger);
25742
- entries.push(entry2);
25743
25833
  logger?.info(`Cross-currency merger outgoing: ${outgoingSymbols.join(", ")} -> pending (cost basis: ${totalCostBasis.toFixed(2)})`);
25744
25834
  return entries;
25745
25835
  }
@@ -25749,16 +25839,18 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25749
25839
  logger?.debug(`Merger outgoing: removed ${absQty} ${out.symbol}`);
25750
25840
  }
25751
25841
  const totalIncomingQty = group.incoming.reduce((sum, a) => sum + Math.abs(a.quantity), 0);
25842
+ const incomingTotalCosts = [];
25752
25843
  for (const inc of group.incoming) {
25753
25844
  const absQty = Math.abs(inc.quantity);
25754
25845
  const proportion = totalIncomingQty > 0 ? absQty / totalIncomingQty : 0;
25755
25846
  const allocatedCost = totalCostBasis * proportion;
25756
25847
  const costBasisPerUnit = absQty > 0 ? allocatedCost / absQty : 0;
25848
+ incomingTotalCosts.push(allocatedCost);
25757
25849
  const lot = {
25758
25850
  date: formatDate(inc.date),
25759
25851
  quantity: absQty,
25760
25852
  costBasis: costBasisPerUnit,
25761
- currency: outgoingCurrency || "CAD",
25853
+ currency: costCurrency,
25762
25854
  isin: inc.isin,
25763
25855
  orderNum: inc.orderNum
25764
25856
  };
@@ -25768,7 +25860,12 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25768
25860
  inventory[inc.symbol].push(lot);
25769
25861
  logger?.debug(`Merger incoming: added ${absQty} ${inc.symbol} @ ${costBasisPerUnit.toFixed(2)}`);
25770
25862
  }
25771
- const entry = generateMultiWayMergerEntry(group, undefined, undefined, logger);
25863
+ const costInfo = {
25864
+ outgoingTotalCosts,
25865
+ incomingTotalCosts,
25866
+ currency: costCurrency
25867
+ };
25868
+ const entry = generateMultiWayMergerEntry(group, undefined, undefined, costInfo, logger);
25772
25869
  entries.push(entry);
25773
25870
  return entries;
25774
25871
  }
@@ -25830,7 +25927,7 @@ function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
25830
25927
  continue;
25831
25928
  }
25832
25929
  const year = parseInt(dateMatch[1], 10);
25833
- const entry = generatePendingMergerWorthlessEntry(state.date, state.orderNum, state.outgoingSymbols, state.outgoingIsins, state.outgoingQuantities || [], state.totalCostBasis, state.currency, logger);
25930
+ const entry = generateDirectWorthlessEntry(state.date, state.orderNum, state.outgoingSymbols, state.outgoingIsins, state.outgoingQuantities || [], state.outgoingTotalCosts || [], state.totalCostBasis, state.currency, logger);
25834
25931
  const journalFile = path13.join(projectDir, "ledger", "investments", `${year}-${state.currency.toLowerCase()}.journal`);
25835
25932
  if (!entriesByJournal.has(journalFile)) {
25836
25933
  entriesByJournal.set(journalFile, []);
@@ -26137,7 +26234,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26137
26234
  investmentAccounts.add("income:capital-gains:realized");
26138
26235
  investmentAccounts.add("income:capital-gains:rights-distribution");
26139
26236
  investmentAccounts.add("expenses:losses:capital");
26140
- investmentAccounts.add("equity:conversion");
26237
+ investmentAccounts.add(`equity:conversion:${currency.toLowerCase()}`);
26141
26238
  investmentAccounts.add("equity:rounding");
26142
26239
  ensureInvestmentAccountDeclarations(accountJournalPath, investmentAccounts, logger);
26143
26240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.13.15",
3
+ "version": "0.13.16-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",