@fuzzle/opencode-accountant 0.13.14 → 0.13.15-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 +134 -94
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -25298,23 +25298,30 @@ 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 totalCost = formatAmount2(totalCostBasis, currency);
25308
25309
  const postings = [
25309
25310
  {
25310
25311
  account: `assets:investments:stocks:${action.symbol}`,
25311
- amount: `-${formatQuantity(oldQuantity)} ${commodity}`
25312
+ amount: `-${formatQuantity(oldQuantity)} ${commodity} @@ ${totalCost}`
25313
+ },
25314
+ {
25315
+ account: "equity:conversion",
25316
+ amount: `${formatQuantity(oldQuantity)} ${commodity} @@ ${totalCost}`
25317
+ },
25318
+ {
25319
+ account: "equity:conversion",
25320
+ amount: `-${formatQuantity(newQuantity)} ${commodity} @@ ${totalCost}`
25312
25321
  },
25313
- { account: "equity:conversion", amount: `${formatQuantity(oldQuantity)} ${commodity}` },
25314
- { account: "equity:conversion", amount: `-${formatQuantity(newQuantity)} ${commodity}` },
25315
25322
  {
25316
25323
  account: `assets:investments:stocks:${action.symbol}`,
25317
- amount: `${formatQuantity(newQuantity)} ${commodity}`
25324
+ amount: `${formatQuantity(newQuantity)} ${commodity} @@ ${totalCost}`
25318
25325
  }
25319
25326
  ];
25320
25327
  let entry = `${date5} ${description}
@@ -25355,7 +25362,7 @@ function generateWorthlessEntry(action, removedLots, logger) {
25355
25362
  `;
25356
25363
  return entry;
25357
25364
  }
25358
- function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossCurrencyOutgoingIsins, logger) {
25365
+ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossCurrencyOutgoingIsins, costInfo, logger) {
25359
25366
  const date5 = formatDate(group.date);
25360
25367
  const outSymbols = crossCurrencyOutgoingSymbols ?? group.outgoing.map((a) => a.symbol);
25361
25368
  const inSymbols = group.incoming.map((a) => a.symbol);
@@ -25365,22 +25372,29 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossC
25365
25372
  const oldIsins = (crossCurrencyOutgoingIsins ?? group.outgoing.map((a) => a.isin)).filter(Boolean);
25366
25373
  const newIsins = group.incoming.map((a) => a.isin).filter(Boolean);
25367
25374
  const postings = [];
25368
- for (const out of group.outgoing) {
25375
+ for (let i2 = 0;i2 < group.outgoing.length; i2++) {
25376
+ const out = group.outgoing[i2];
25369
25377
  const qty = formatQuantity(Math.abs(out.quantity));
25370
25378
  const commodity = formatCommodity(out.symbol);
25379
+ const costAnnotation = costInfo && costInfo.outgoingTotalCosts[i2] != null ? ` @@ ${formatAmount2(costInfo.outgoingTotalCosts[i2], costInfo.currency)}` : "";
25371
25380
  postings.push({
25372
25381
  account: `assets:investments:stocks:${out.symbol}`,
25373
- amount: `-${qty} ${commodity}`
25382
+ amount: `-${qty} ${commodity}${costAnnotation}`
25374
25383
  });
25375
- postings.push({ account: "equity:conversion", amount: `${qty} ${commodity}` });
25384
+ postings.push({ account: "equity:conversion", amount: `${qty} ${commodity}${costAnnotation}` });
25376
25385
  }
25377
- for (const inc of group.incoming) {
25386
+ for (let i2 = 0;i2 < group.incoming.length; i2++) {
25387
+ const inc = group.incoming[i2];
25378
25388
  const qty = formatQuantity(Math.abs(inc.quantity));
25379
25389
  const commodity = formatCommodity(inc.symbol);
25380
- postings.push({ account: "equity:conversion", amount: `-${qty} ${commodity}` });
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
+ });
25381
25395
  postings.push({
25382
25396
  account: `assets:investments:stocks:${inc.symbol}`,
25383
- amount: `${qty} ${commodity}`
25397
+ amount: `${qty} ${commodity}${costAnnotation}`
25384
25398
  });
25385
25399
  }
25386
25400
  let entry = `${date5} ${description}
@@ -25624,9 +25638,12 @@ function processNonMergerAction(action, inventory, entries, logger) {
25624
25638
  const oldQty = getHeldQuantity(inventory, action.symbol);
25625
25639
  const newQty = Math.abs(qty);
25626
25640
  if (oldQty > 0) {
25641
+ const lots = inventory[action.symbol] || [];
25642
+ const totalCostBasis = lots.reduce((sum, lot) => sum + lot.quantity * lot.costBasis, 0);
25643
+ const currency = lots[0]?.currency || "CAD";
25627
25644
  const ratio = newQty / oldQty;
25628
25645
  adjustLotsForSplit(inventory, action.symbol, ratio, logger);
25629
- const entry = generateSplitEntry(action, oldQty, newQty, logger);
25646
+ const entry = generateSplitEntry(action, oldQty, newQty, totalCostBasis, currency, logger);
25630
25647
  entries.push(entry);
25631
25648
  } else {
25632
25649
  logger?.warn(`Reverse split for ${action.symbol} but no lots found`);
@@ -25652,36 +25669,6 @@ function processNonMergerAction(action, inventory, entries, logger) {
25652
25669
  }
25653
25670
  }
25654
25671
  }
25655
- function processCorporateActions(actions, inventory, lotInventoryPath, projectDir, logger) {
25656
- const entries = [];
25657
- actions.sort((a, b) => formatDate(a.date).localeCompare(formatDate(b.date)));
25658
- const mergerActions = [];
25659
- const otherActions = [];
25660
- for (const action of actions) {
25661
- if (MERGER_LIKE_TYPES.has(action.type)) {
25662
- mergerActions.push(action);
25663
- } else {
25664
- otherActions.push(action);
25665
- }
25666
- }
25667
- const mergerGroups = groupMergerActions(mergerActions);
25668
- mergerGroups.sort((a, b) => formatDate(a.date).localeCompare(formatDate(b.date)));
25669
- let mi = 0;
25670
- let oi = 0;
25671
- while (mi < mergerGroups.length || oi < otherActions.length) {
25672
- const mergerDate = mi < mergerGroups.length ? formatDate(mergerGroups[mi].date) : "\uFFFF";
25673
- const otherDate = oi < otherActions.length ? formatDate(otherActions[oi].date) : "\uFFFF";
25674
- if (mergerDate <= otherDate) {
25675
- const groupEntries = processMultiWayMerger(mergerGroups[mi], inventory, lotInventoryPath, projectDir, logger);
25676
- entries.push(...groupEntries);
25677
- mi++;
25678
- } else {
25679
- processNonMergerAction(otherActions[oi], inventory, entries, logger);
25680
- oi++;
25681
- }
25682
- }
25683
- return entries;
25684
- }
25685
25672
  function groupMergerActions(actions) {
25686
25673
  const groupMap = new Map;
25687
25674
  for (const action of actions) {
@@ -25713,11 +25700,13 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25713
25700
  const pendingState = loadPendingMerger(projectDir, lotInventoryPath, group.key, logger);
25714
25701
  if (pendingState) {
25715
25702
  const totalIncomingQty2 = group.incoming.reduce((sum, a) => sum + Math.abs(a.quantity), 0);
25703
+ const incomingTotalCosts2 = [];
25716
25704
  for (const inc of group.incoming) {
25717
25705
  const absQty = Math.abs(inc.quantity);
25718
25706
  const proportion = absQty / totalIncomingQty2;
25719
25707
  const allocatedCost = pendingState.totalCostBasis * proportion;
25720
25708
  const costBasisPerUnit = absQty > 0 ? allocatedCost / absQty : 0;
25709
+ incomingTotalCosts2.push(allocatedCost);
25721
25710
  const lot = {
25722
25711
  date: formatDate(inc.date),
25723
25712
  quantity: absQty,
@@ -25732,7 +25721,12 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25732
25721
  inventory[inc.symbol].push(lot);
25733
25722
  logger?.info(`Cross-currency merger incoming: ${absQty} ${inc.symbol} @ ${costBasisPerUnit.toFixed(2)} ${pendingState.currency}`);
25734
25723
  }
25735
- const entry2 = generateMultiWayMergerEntry(group, pendingState.outgoingSymbols, pendingState.outgoingIsins, logger);
25724
+ const costInfo2 = {
25725
+ outgoingTotalCosts: [],
25726
+ incomingTotalCosts: incomingTotalCosts2,
25727
+ currency: pendingState.currency
25728
+ };
25729
+ const entry2 = generateMultiWayMergerEntry(group, pendingState.outgoingSymbols, pendingState.outgoingIsins, costInfo2, logger);
25736
25730
  entries.push(entry2);
25737
25731
  removePendingMerger(projectDir, lotInventoryPath, group.key, logger);
25738
25732
  } else {
@@ -25743,18 +25737,23 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25743
25737
  let totalCostBasis = 0;
25744
25738
  let outgoingCurrency = "";
25745
25739
  const outgoingSymbols = [];
25740
+ const outgoingTotalCosts = [];
25746
25741
  for (const out of group.outgoing) {
25747
25742
  outgoingSymbols.push(out.symbol);
25748
25743
  const lots = inventory[out.symbol];
25744
+ let symbolCost = 0;
25749
25745
  if (lots) {
25750
25746
  for (const lot of lots) {
25751
- totalCostBasis += lot.quantity * lot.costBasis;
25747
+ symbolCost += lot.quantity * lot.costBasis;
25752
25748
  if (!outgoingCurrency && lot.currency) {
25753
25749
  outgoingCurrency = lot.currency;
25754
25750
  }
25755
25751
  }
25756
25752
  }
25753
+ totalCostBasis += symbolCost;
25754
+ outgoingTotalCosts.push(symbolCost);
25757
25755
  }
25756
+ const costCurrency = outgoingCurrency || "CAD";
25758
25757
  if (group.outgoing.length > 0 && group.incoming.length === 0) {
25759
25758
  for (const out of group.outgoing) {
25760
25759
  removeLots(inventory, out.symbol, logger);
@@ -25766,9 +25765,14 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25766
25765
  outgoingIsins: group.outgoing.map((a) => a.isin),
25767
25766
  outgoingQuantities: group.outgoing.map((a) => Math.abs(a.quantity)),
25768
25767
  totalCostBasis,
25769
- currency: outgoingCurrency || "CAD"
25768
+ currency: costCurrency
25770
25769
  }, logger);
25771
- const entry2 = generateMultiWayMergerEntry(group, undefined, undefined, logger);
25770
+ const costInfo2 = {
25771
+ outgoingTotalCosts,
25772
+ incomingTotalCosts: [],
25773
+ currency: costCurrency
25774
+ };
25775
+ const entry2 = generateMultiWayMergerEntry(group, undefined, undefined, costInfo2, logger);
25772
25776
  entries.push(entry2);
25773
25777
  logger?.info(`Cross-currency merger outgoing: ${outgoingSymbols.join(", ")} -> pending (cost basis: ${totalCostBasis.toFixed(2)})`);
25774
25778
  return entries;
@@ -25779,16 +25783,18 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25779
25783
  logger?.debug(`Merger outgoing: removed ${absQty} ${out.symbol}`);
25780
25784
  }
25781
25785
  const totalIncomingQty = group.incoming.reduce((sum, a) => sum + Math.abs(a.quantity), 0);
25786
+ const incomingTotalCosts = [];
25782
25787
  for (const inc of group.incoming) {
25783
25788
  const absQty = Math.abs(inc.quantity);
25784
25789
  const proportion = totalIncomingQty > 0 ? absQty / totalIncomingQty : 0;
25785
25790
  const allocatedCost = totalCostBasis * proportion;
25786
25791
  const costBasisPerUnit = absQty > 0 ? allocatedCost / absQty : 0;
25792
+ incomingTotalCosts.push(allocatedCost);
25787
25793
  const lot = {
25788
25794
  date: formatDate(inc.date),
25789
25795
  quantity: absQty,
25790
25796
  costBasis: costBasisPerUnit,
25791
- currency: outgoingCurrency || "CAD",
25797
+ currency: costCurrency,
25792
25798
  isin: inc.isin,
25793
25799
  orderNum: inc.orderNum
25794
25800
  };
@@ -25798,7 +25804,12 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25798
25804
  inventory[inc.symbol].push(lot);
25799
25805
  logger?.debug(`Merger incoming: added ${absQty} ${inc.symbol} @ ${costBasisPerUnit.toFixed(2)}`);
25800
25806
  }
25801
- const entry = generateMultiWayMergerEntry(group, undefined, undefined, logger);
25807
+ const costInfo = {
25808
+ outgoingTotalCosts,
25809
+ incomingTotalCosts,
25810
+ currency: costCurrency
25811
+ };
25812
+ const entry = generateMultiWayMergerEntry(group, undefined, undefined, costInfo, logger);
25802
25813
  entries.push(entry);
25803
25814
  return entries;
25804
25815
  }
@@ -26013,57 +26024,86 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26013
26024
  logger?.logStep("route", "success", `Simple: ${stats.simpleTransactions}, Trades: ${stats.trades}, Dividends: ${stats.dividends}, Corporate: ${stats.corporateActions}, Forex: ${stats.forexTransactions}, Skipped: ${stats.skipped}`);
26014
26025
  logger?.endSection();
26015
26026
  const journalEntries = [];
26016
- if (trades.length > 0) {
26017
- logger?.startSection("Trade Processing", 2);
26018
- trades.sort((a, b) => formatDate(a.date).localeCompare(formatDate(b.date)));
26019
- for (const trade of trades) {
26020
- try {
26021
- if (trade.type === "Buy") {
26022
- const tradeInfo = {
26023
- date: formatDate(trade.date),
26024
- orderNum: trade.orderNum,
26025
- symbol: trade.symbol,
26026
- isin: trade.isin,
26027
- quantity: trade.quantity,
26028
- unitPrice: trade.unitPrice,
26029
- currency: trade.currency
26030
- };
26031
- addLot(inventory, tradeInfo, logger);
26032
- const entry = generateBuyEntry(trade, logger);
26033
- journalEntries.push(entry);
26034
- logger?.logStep("trade-buy", "success", `Buy ${trade.quantity} ${trade.symbol} @ ${trade.unitPrice} ${trade.currency}`);
26035
- } else if (trade.type === "Sell") {
26036
- const consumed = consumeLotsFIFO(inventory, trade.symbol, trade.quantity, logger);
26037
- const entry = generateSellEntry(trade, consumed, logger);
26027
+ const timeline = [];
26028
+ for (const trade of trades) {
26029
+ timeline.push({ kind: "trade", sortDate: formatDate(trade.date), trade });
26030
+ }
26031
+ for (const dividend of dividends) {
26032
+ timeline.push({ kind: "dividend", sortDate: formatDate(dividend.date), dividend });
26033
+ }
26034
+ const mergerActions = [];
26035
+ const otherCorpActions = [];
26036
+ for (const action of corporateActions) {
26037
+ if (MERGER_LIKE_TYPES.has(action.type)) {
26038
+ mergerActions.push(action);
26039
+ } else {
26040
+ otherCorpActions.push(action);
26041
+ }
26042
+ }
26043
+ const mergerGroups = groupMergerActions(mergerActions);
26044
+ for (const group of mergerGroups) {
26045
+ timeline.push({ kind: "mergerGroup", sortDate: formatDate(group.date), group });
26046
+ }
26047
+ for (const action of otherCorpActions) {
26048
+ timeline.push({ kind: "corpAction", sortDate: formatDate(action.date), action });
26049
+ }
26050
+ timeline.sort((a, b) => a.sortDate.localeCompare(b.sortDate));
26051
+ if (timeline.length > 0) {
26052
+ logger?.startSection("Chronological Event Processing", 2);
26053
+ for (const event of timeline) {
26054
+ switch (event.kind) {
26055
+ case "trade": {
26056
+ const trade = event.trade;
26057
+ try {
26058
+ if (trade.type === "Buy") {
26059
+ const tradeInfo = {
26060
+ date: formatDate(trade.date),
26061
+ orderNum: trade.orderNum,
26062
+ symbol: trade.symbol,
26063
+ isin: trade.isin,
26064
+ quantity: trade.quantity,
26065
+ unitPrice: trade.unitPrice,
26066
+ currency: trade.currency
26067
+ };
26068
+ addLot(inventory, tradeInfo, logger);
26069
+ const entry = generateBuyEntry(trade, logger);
26070
+ journalEntries.push(entry);
26071
+ logger?.logStep("trade-buy", "success", `Buy ${trade.quantity} ${trade.symbol} @ ${trade.unitPrice} ${trade.currency}`);
26072
+ } else if (trade.type === "Sell") {
26073
+ const consumed = consumeLotsFIFO(inventory, trade.symbol, trade.quantity, logger);
26074
+ const entry = generateSellEntry(trade, consumed, logger);
26075
+ journalEntries.push(entry);
26076
+ const totalCost = consumed.reduce((sum, c) => sum + c.totalCost, 0);
26077
+ const gain = trade.quantity * trade.unitPrice - totalCost;
26078
+ logger?.logStep("trade-sell", "success", `Sell ${trade.quantity} ${trade.symbol} @ ${trade.unitPrice} ${trade.currency}, gain: ${gain.toFixed(2)}`);
26079
+ }
26080
+ } catch (error45) {
26081
+ const message = error45 instanceof Error ? error45.message : String(error45);
26082
+ logger?.logStep("trade", "error", `Failed to process trade: ${message}`);
26083
+ logger?.error(`Trade processing error for ${trade.symbol}`, error45 instanceof Error ? error45 : undefined);
26084
+ }
26085
+ break;
26086
+ }
26087
+ case "dividend": {
26088
+ const dividend = event.dividend;
26089
+ const entry = generateDividendEntry(dividend, logger);
26038
26090
  journalEntries.push(entry);
26039
- const totalCost = consumed.reduce((sum, c) => sum + c.totalCost, 0);
26040
- const gain = trade.quantity * trade.unitPrice - totalCost;
26041
- logger?.logStep("trade-sell", "success", `Sell ${trade.quantity} ${trade.symbol} @ ${trade.unitPrice} ${trade.currency}, gain: ${gain.toFixed(2)}`);
26091
+ logger?.logStep("dividend", "success", `Dividend ${dividend.symbol}: ${dividend.netAmount} ${dividend.currency} (tax: ${dividend.withholdingTax})`);
26092
+ break;
26093
+ }
26094
+ case "mergerGroup": {
26095
+ const groupEntries = processMultiWayMerger(event.group, inventory, lotInventoryPath, projectDir, logger);
26096
+ journalEntries.push(...groupEntries);
26097
+ break;
26098
+ }
26099
+ case "corpAction": {
26100
+ processNonMergerAction(event.action, inventory, journalEntries, logger);
26101
+ break;
26042
26102
  }
26043
- } catch (error45) {
26044
- const message = error45 instanceof Error ? error45.message : String(error45);
26045
- logger?.logStep("trade", "error", `Failed to process trade: ${message}`);
26046
- logger?.error(`Trade processing error for ${trade.symbol}`, error45 instanceof Error ? error45 : undefined);
26047
26103
  }
26048
26104
  }
26049
26105
  logger?.endSection();
26050
26106
  }
26051
- if (dividends.length > 0) {
26052
- logger?.startSection("Dividend Processing", 2);
26053
- for (const dividend of dividends) {
26054
- const entry = generateDividendEntry(dividend, logger);
26055
- journalEntries.push(entry);
26056
- logger?.logStep("dividend", "success", `Dividend ${dividend.symbol}: ${dividend.netAmount} ${dividend.currency} (tax: ${dividend.withholdingTax})`);
26057
- }
26058
- logger?.endSection();
26059
- }
26060
- if (corporateActions.length > 0) {
26061
- logger?.startSection("Corporate Actions Processing", 2);
26062
- const corpEntries = processCorporateActions(corporateActions, inventory, lotInventoryPath, projectDir, logger);
26063
- journalEntries.push(...corpEntries);
26064
- logger?.logStep("corporate", "success", `Processed ${corporateActions.length} corporate actions`);
26065
- logger?.endSection();
26066
- }
26067
26107
  logger?.logStep("save-inventory", "start", "Saving lot inventory");
26068
26108
  saveLotInventory(projectDir, lotInventoryPath, inventory, logger);
26069
26109
  logger?.logStep("save-inventory", "success", "Lot inventory saved");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.13.14",
3
+ "version": "0.13.15-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",