@fuzzle/opencode-accountant 0.13.13 → 0.13.14-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.
- package/dist/index.js +214 -96
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25416,6 +25416,47 @@ function generateRightsDistributionEntry(action, logger) {
|
|
|
25416
25416
|
`;
|
|
25417
25417
|
return entry;
|
|
25418
25418
|
}
|
|
25419
|
+
function generatePendingMergerWorthlessEntry(date5, orderNum, outgoingSymbols, outgoingIsins, outgoingQuantities, totalCostBasis, currency, logger) {
|
|
25420
|
+
const formattedDate = formatDate(date5);
|
|
25421
|
+
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}`);
|
|
25424
|
+
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
|
+
}
|
|
25441
|
+
}
|
|
25442
|
+
postings.push({
|
|
25443
|
+
account: "expenses:losses:capital",
|
|
25444
|
+
amount: formatAmount2(totalCostBasis, currency)
|
|
25445
|
+
});
|
|
25446
|
+
let entry = `${formattedDate} ${description}
|
|
25447
|
+
`;
|
|
25448
|
+
entry += ` ; swissquote:order:${orderNum}
|
|
25449
|
+
`;
|
|
25450
|
+
entry += ` ; Pending merger resolved as worthless - total loss: ${totalCostBasis.toFixed(2)} ${currency}
|
|
25451
|
+
`;
|
|
25452
|
+
if (outgoingIsins.length > 0) {
|
|
25453
|
+
entry += ` ; Original ISINs: ${outgoingIsins.join(", ")}
|
|
25454
|
+
`;
|
|
25455
|
+
}
|
|
25456
|
+
entry += formatPostings(postings) + `
|
|
25457
|
+
`;
|
|
25458
|
+
return entry;
|
|
25459
|
+
}
|
|
25419
25460
|
function formatJournalFile(entries, year, currency) {
|
|
25420
25461
|
const header = `; Swissquote ${currency.toUpperCase()} investment transactions for ${year}
|
|
25421
25462
|
; Generated by opencode-accountant
|
|
@@ -25573,62 +25614,43 @@ var MERGER_LIKE_TYPES = new Set([
|
|
|
25573
25614
|
"Exchange of securities",
|
|
25574
25615
|
"Corporate Action"
|
|
25575
25616
|
]);
|
|
25576
|
-
function
|
|
25577
|
-
|
|
25578
|
-
|
|
25579
|
-
|
|
25580
|
-
|
|
25581
|
-
|
|
25582
|
-
if (MERGER_LIKE_TYPES.has(action.type)) {
|
|
25583
|
-
mergerActions.push(action);
|
|
25584
|
-
} else {
|
|
25585
|
-
otherActions.push(action);
|
|
25586
|
-
}
|
|
25587
|
-
}
|
|
25588
|
-
const mergerGroups = groupMergerActions(mergerActions);
|
|
25589
|
-
for (const group of mergerGroups) {
|
|
25590
|
-
const groupEntries = processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, logger);
|
|
25591
|
-
entries.push(...groupEntries);
|
|
25592
|
-
}
|
|
25593
|
-
for (const action of otherActions) {
|
|
25594
|
-
switch (action.type) {
|
|
25595
|
-
case "Reverse Split": {
|
|
25596
|
-
const qty = action.quantity;
|
|
25597
|
-
if (qty < 0) {
|
|
25598
|
-
continue;
|
|
25599
|
-
}
|
|
25600
|
-
const oldQty = getHeldQuantity(inventory, action.symbol);
|
|
25601
|
-
const newQty = Math.abs(qty);
|
|
25602
|
-
if (oldQty > 0) {
|
|
25603
|
-
const ratio = newQty / oldQty;
|
|
25604
|
-
adjustLotsForSplit(inventory, action.symbol, ratio, logger);
|
|
25605
|
-
const entry = generateSplitEntry(action, oldQty, newQty, logger);
|
|
25606
|
-
entries.push(entry);
|
|
25607
|
-
} else {
|
|
25608
|
-
logger?.warn(`Reverse split for ${action.symbol} but no lots found`);
|
|
25609
|
-
}
|
|
25610
|
-
break;
|
|
25617
|
+
function processNonMergerAction(action, inventory, entries, logger) {
|
|
25618
|
+
switch (action.type) {
|
|
25619
|
+
case "Reverse Split": {
|
|
25620
|
+
const qty = action.quantity;
|
|
25621
|
+
if (qty < 0) {
|
|
25622
|
+
return;
|
|
25611
25623
|
}
|
|
25612
|
-
|
|
25613
|
-
|
|
25614
|
-
|
|
25615
|
-
|
|
25616
|
-
|
|
25617
|
-
|
|
25618
|
-
|
|
25619
|
-
|
|
25620
|
-
|
|
25624
|
+
const oldQty = getHeldQuantity(inventory, action.symbol);
|
|
25625
|
+
const newQty = Math.abs(qty);
|
|
25626
|
+
if (oldQty > 0) {
|
|
25627
|
+
const ratio = newQty / oldQty;
|
|
25628
|
+
adjustLotsForSplit(inventory, action.symbol, ratio, logger);
|
|
25629
|
+
const entry = generateSplitEntry(action, oldQty, newQty, logger);
|
|
25630
|
+
entries.push(entry);
|
|
25631
|
+
} else {
|
|
25632
|
+
logger?.warn(`Reverse split for ${action.symbol} but no lots found`);
|
|
25621
25633
|
}
|
|
25622
|
-
|
|
25623
|
-
|
|
25624
|
-
|
|
25625
|
-
|
|
25626
|
-
|
|
25627
|
-
|
|
25634
|
+
break;
|
|
25635
|
+
}
|
|
25636
|
+
case "Worthless Liquidation": {
|
|
25637
|
+
const removedLots = removeLots(inventory, action.symbol, logger);
|
|
25638
|
+
if (removedLots.length > 0) {
|
|
25639
|
+
const entry = generateWorthlessEntry(action, removedLots, logger);
|
|
25640
|
+
entries.push(entry);
|
|
25641
|
+
} else {
|
|
25642
|
+
logger?.warn(`Worthless liquidation for ${action.symbol} but no lots found`);
|
|
25643
|
+
}
|
|
25644
|
+
break;
|
|
25645
|
+
}
|
|
25646
|
+
case "Rights Distribution": {
|
|
25647
|
+
if (action.quantity > 0) {
|
|
25648
|
+
const entry = generateRightsDistributionEntry(action, logger);
|
|
25649
|
+
entries.push(entry);
|
|
25628
25650
|
}
|
|
25651
|
+
break;
|
|
25629
25652
|
}
|
|
25630
25653
|
}
|
|
25631
|
-
return entries;
|
|
25632
25654
|
}
|
|
25633
25655
|
function groupMergerActions(actions) {
|
|
25634
25656
|
const groupMap = new Map;
|
|
@@ -25712,6 +25734,7 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
|
|
|
25712
25734
|
orderNum: group.orderNum,
|
|
25713
25735
|
outgoingSymbols,
|
|
25714
25736
|
outgoingIsins: group.outgoing.map((a) => a.isin),
|
|
25737
|
+
outgoingQuantities: group.outgoing.map((a) => Math.abs(a.quantity)),
|
|
25715
25738
|
totalCostBasis,
|
|
25716
25739
|
currency: outgoingCurrency || "CAD"
|
|
25717
25740
|
}, logger);
|
|
@@ -25785,6 +25808,68 @@ function removePendingMerger(projectDir, lotInventoryPath, key, logger) {
|
|
|
25785
25808
|
logger?.debug(`Removed pending merger state: ${key}`);
|
|
25786
25809
|
}
|
|
25787
25810
|
}
|
|
25811
|
+
function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
|
|
25812
|
+
const pendingDir = getPendingMergerDir(projectDir, lotInventoryPath);
|
|
25813
|
+
if (!fs18.existsSync(pendingDir)) {
|
|
25814
|
+
return { resolved: 0, journalFilesUpdated: [] };
|
|
25815
|
+
}
|
|
25816
|
+
const files = fs18.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
|
|
25817
|
+
if (files.length === 0) {
|
|
25818
|
+
return { resolved: 0, journalFilesUpdated: [] };
|
|
25819
|
+
}
|
|
25820
|
+
const entriesByJournal = new Map;
|
|
25821
|
+
const resolvedFiles = [];
|
|
25822
|
+
for (const file2 of files) {
|
|
25823
|
+
const filePath = path13.join(pendingDir, file2);
|
|
25824
|
+
try {
|
|
25825
|
+
const content = fs18.readFileSync(filePath, "utf-8");
|
|
25826
|
+
const state = JSON.parse(content);
|
|
25827
|
+
const dateMatch = state.date.match(/\d{2}-\d{2}-(\d{4})/);
|
|
25828
|
+
if (!dateMatch) {
|
|
25829
|
+
logger?.warn(`Invalid date in pending merger ${file2}: ${state.date}`);
|
|
25830
|
+
continue;
|
|
25831
|
+
}
|
|
25832
|
+
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);
|
|
25834
|
+
const journalFile = path13.join(projectDir, "ledger", "investments", `${year}-${state.currency.toLowerCase()}.journal`);
|
|
25835
|
+
if (!entriesByJournal.has(journalFile)) {
|
|
25836
|
+
entriesByJournal.set(journalFile, []);
|
|
25837
|
+
}
|
|
25838
|
+
entriesByJournal.get(journalFile).push(entry);
|
|
25839
|
+
fs18.unlinkSync(filePath);
|
|
25840
|
+
resolvedFiles.push(file2);
|
|
25841
|
+
logger?.info(`Resolved pending merger ${file2} as worthless: ${state.outgoingSymbols.join(", ")} (${state.totalCostBasis.toFixed(2)} ${state.currency})`);
|
|
25842
|
+
} catch (error45) {
|
|
25843
|
+
const message = error45 instanceof Error ? error45.message : String(error45);
|
|
25844
|
+
logger?.warn(`Failed to resolve pending merger ${file2}: ${message}`);
|
|
25845
|
+
}
|
|
25846
|
+
}
|
|
25847
|
+
const journalFilesUpdated = [];
|
|
25848
|
+
for (const [journalFile, journalEntries] of entriesByJournal) {
|
|
25849
|
+
if (fs18.existsSync(journalFile)) {
|
|
25850
|
+
fs18.appendFileSync(journalFile, `
|
|
25851
|
+
` + journalEntries.join(`
|
|
25852
|
+
`));
|
|
25853
|
+
} else {
|
|
25854
|
+
const basename5 = path13.basename(journalFile, ".journal");
|
|
25855
|
+
const parts = basename5.split("-");
|
|
25856
|
+
const yearStr = parts[0];
|
|
25857
|
+
const currency = parts.slice(1).join("-");
|
|
25858
|
+
const header = formatJournalFile(journalEntries, parseInt(yearStr, 10), currency);
|
|
25859
|
+
fs18.writeFileSync(journalFile, header);
|
|
25860
|
+
}
|
|
25861
|
+
journalFilesUpdated.push(journalFile);
|
|
25862
|
+
logger?.info(`Updated ${path13.basename(journalFile)} with worthless resolution entries`);
|
|
25863
|
+
}
|
|
25864
|
+
const remaining = fs18.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
|
|
25865
|
+
if (remaining.length === 0) {
|
|
25866
|
+
try {
|
|
25867
|
+
fs18.rmdirSync(pendingDir);
|
|
25868
|
+
logger?.debug("Removed empty pending-mergers directory");
|
|
25869
|
+
} catch {}
|
|
25870
|
+
}
|
|
25871
|
+
return { resolved: resolvedFiles.length, journalFilesUpdated };
|
|
25872
|
+
}
|
|
25788
25873
|
async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInventoryPath = DEFAULT_LOT_INVENTORY_PATH, symbolMap = {}, logger) {
|
|
25789
25874
|
logger?.startSection(`Swissquote Preprocessing: ${path13.basename(csvPath)}`);
|
|
25790
25875
|
const stats = {
|
|
@@ -25898,57 +25983,86 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
|
|
|
25898
25983
|
logger?.logStep("route", "success", `Simple: ${stats.simpleTransactions}, Trades: ${stats.trades}, Dividends: ${stats.dividends}, Corporate: ${stats.corporateActions}, Forex: ${stats.forexTransactions}, Skipped: ${stats.skipped}`);
|
|
25899
25984
|
logger?.endSection();
|
|
25900
25985
|
const journalEntries = [];
|
|
25901
|
-
|
|
25902
|
-
|
|
25903
|
-
|
|
25904
|
-
|
|
25905
|
-
|
|
25906
|
-
|
|
25907
|
-
|
|
25908
|
-
|
|
25909
|
-
|
|
25910
|
-
|
|
25911
|
-
|
|
25912
|
-
|
|
25913
|
-
|
|
25914
|
-
|
|
25915
|
-
|
|
25916
|
-
|
|
25917
|
-
|
|
25918
|
-
|
|
25919
|
-
|
|
25920
|
-
|
|
25921
|
-
|
|
25922
|
-
|
|
25986
|
+
const timeline = [];
|
|
25987
|
+
for (const trade of trades) {
|
|
25988
|
+
timeline.push({ kind: "trade", sortDate: formatDate(trade.date), trade });
|
|
25989
|
+
}
|
|
25990
|
+
for (const dividend of dividends) {
|
|
25991
|
+
timeline.push({ kind: "dividend", sortDate: formatDate(dividend.date), dividend });
|
|
25992
|
+
}
|
|
25993
|
+
const mergerActions = [];
|
|
25994
|
+
const otherCorpActions = [];
|
|
25995
|
+
for (const action of corporateActions) {
|
|
25996
|
+
if (MERGER_LIKE_TYPES.has(action.type)) {
|
|
25997
|
+
mergerActions.push(action);
|
|
25998
|
+
} else {
|
|
25999
|
+
otherCorpActions.push(action);
|
|
26000
|
+
}
|
|
26001
|
+
}
|
|
26002
|
+
const mergerGroups = groupMergerActions(mergerActions);
|
|
26003
|
+
for (const group of mergerGroups) {
|
|
26004
|
+
timeline.push({ kind: "mergerGroup", sortDate: formatDate(group.date), group });
|
|
26005
|
+
}
|
|
26006
|
+
for (const action of otherCorpActions) {
|
|
26007
|
+
timeline.push({ kind: "corpAction", sortDate: formatDate(action.date), action });
|
|
26008
|
+
}
|
|
26009
|
+
timeline.sort((a, b) => a.sortDate.localeCompare(b.sortDate));
|
|
26010
|
+
if (timeline.length > 0) {
|
|
26011
|
+
logger?.startSection("Chronological Event Processing", 2);
|
|
26012
|
+
for (const event of timeline) {
|
|
26013
|
+
switch (event.kind) {
|
|
26014
|
+
case "trade": {
|
|
26015
|
+
const trade = event.trade;
|
|
26016
|
+
try {
|
|
26017
|
+
if (trade.type === "Buy") {
|
|
26018
|
+
const tradeInfo = {
|
|
26019
|
+
date: formatDate(trade.date),
|
|
26020
|
+
orderNum: trade.orderNum,
|
|
26021
|
+
symbol: trade.symbol,
|
|
26022
|
+
isin: trade.isin,
|
|
26023
|
+
quantity: trade.quantity,
|
|
26024
|
+
unitPrice: trade.unitPrice,
|
|
26025
|
+
currency: trade.currency
|
|
26026
|
+
};
|
|
26027
|
+
addLot(inventory, tradeInfo, logger);
|
|
26028
|
+
const entry = generateBuyEntry(trade, logger);
|
|
26029
|
+
journalEntries.push(entry);
|
|
26030
|
+
logger?.logStep("trade-buy", "success", `Buy ${trade.quantity} ${trade.symbol} @ ${trade.unitPrice} ${trade.currency}`);
|
|
26031
|
+
} else if (trade.type === "Sell") {
|
|
26032
|
+
const consumed = consumeLotsFIFO(inventory, trade.symbol, trade.quantity, logger);
|
|
26033
|
+
const entry = generateSellEntry(trade, consumed, logger);
|
|
26034
|
+
journalEntries.push(entry);
|
|
26035
|
+
const totalCost = consumed.reduce((sum, c) => sum + c.totalCost, 0);
|
|
26036
|
+
const gain = trade.quantity * trade.unitPrice - totalCost;
|
|
26037
|
+
logger?.logStep("trade-sell", "success", `Sell ${trade.quantity} ${trade.symbol} @ ${trade.unitPrice} ${trade.currency}, gain: ${gain.toFixed(2)}`);
|
|
26038
|
+
}
|
|
26039
|
+
} catch (error45) {
|
|
26040
|
+
const message = error45 instanceof Error ? error45.message : String(error45);
|
|
26041
|
+
logger?.logStep("trade", "error", `Failed to process trade: ${message}`);
|
|
26042
|
+
logger?.error(`Trade processing error for ${trade.symbol}`, error45 instanceof Error ? error45 : undefined);
|
|
26043
|
+
}
|
|
26044
|
+
break;
|
|
26045
|
+
}
|
|
26046
|
+
case "dividend": {
|
|
26047
|
+
const dividend = event.dividend;
|
|
26048
|
+
const entry = generateDividendEntry(dividend, logger);
|
|
25923
26049
|
journalEntries.push(entry);
|
|
25924
|
-
|
|
25925
|
-
|
|
25926
|
-
|
|
26050
|
+
logger?.logStep("dividend", "success", `Dividend ${dividend.symbol}: ${dividend.netAmount} ${dividend.currency} (tax: ${dividend.withholdingTax})`);
|
|
26051
|
+
break;
|
|
26052
|
+
}
|
|
26053
|
+
case "mergerGroup": {
|
|
26054
|
+
const groupEntries = processMultiWayMerger(event.group, inventory, lotInventoryPath, projectDir, logger);
|
|
26055
|
+
journalEntries.push(...groupEntries);
|
|
26056
|
+
break;
|
|
26057
|
+
}
|
|
26058
|
+
case "corpAction": {
|
|
26059
|
+
processNonMergerAction(event.action, inventory, journalEntries, logger);
|
|
26060
|
+
break;
|
|
25927
26061
|
}
|
|
25928
|
-
} catch (error45) {
|
|
25929
|
-
const message = error45 instanceof Error ? error45.message : String(error45);
|
|
25930
|
-
logger?.logStep("trade", "error", `Failed to process trade: ${message}`);
|
|
25931
|
-
logger?.error(`Trade processing error for ${trade.symbol}`, error45 instanceof Error ? error45 : undefined);
|
|
25932
26062
|
}
|
|
25933
26063
|
}
|
|
25934
26064
|
logger?.endSection();
|
|
25935
26065
|
}
|
|
25936
|
-
if (dividends.length > 0) {
|
|
25937
|
-
logger?.startSection("Dividend Processing", 2);
|
|
25938
|
-
for (const dividend of dividends) {
|
|
25939
|
-
const entry = generateDividendEntry(dividend, logger);
|
|
25940
|
-
journalEntries.push(entry);
|
|
25941
|
-
logger?.logStep("dividend", "success", `Dividend ${dividend.symbol}: ${dividend.netAmount} ${dividend.currency} (tax: ${dividend.withholdingTax})`);
|
|
25942
|
-
}
|
|
25943
|
-
logger?.endSection();
|
|
25944
|
-
}
|
|
25945
|
-
if (corporateActions.length > 0) {
|
|
25946
|
-
logger?.startSection("Corporate Actions Processing", 2);
|
|
25947
|
-
const corpEntries = processCorporateActions(corporateActions, inventory, lotInventoryPath, projectDir, logger);
|
|
25948
|
-
journalEntries.push(...corpEntries);
|
|
25949
|
-
logger?.logStep("corporate", "success", `Processed ${corporateActions.length} corporate actions`);
|
|
25950
|
-
logger?.endSection();
|
|
25951
|
-
}
|
|
25952
26066
|
logger?.logStep("save-inventory", "start", "Saving lot inventory");
|
|
25953
26067
|
saveLotInventory(projectDir, lotInventoryPath, inventory, logger);
|
|
25954
26068
|
logger?.logStep("save-inventory", "success", "Lot inventory saved");
|
|
@@ -26730,6 +26844,10 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
26730
26844
|
}
|
|
26731
26845
|
logger?.logStep("Swissquote Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.corporateActions} corporate actions, ${result.stats.forexTransactions} forex`);
|
|
26732
26846
|
}
|
|
26847
|
+
const pendingResolution = resolveRemainingPendingMergers(context.directory, lotInventoryPath, logger);
|
|
26848
|
+
if (pendingResolution.resolved > 0) {
|
|
26849
|
+
logger?.logStep("Swissquote Pending Mergers", "success", `Resolved ${pendingResolution.resolved} pending merger(s) as worthless`);
|
|
26850
|
+
}
|
|
26733
26851
|
if (allForexRows.length > 0) {
|
|
26734
26852
|
const firstCtx = swissquoteContexts[0];
|
|
26735
26853
|
const yearJournalPath = path16.join(context.directory, "ledger", `${firstCtx.year}.journal`);
|
package/package.json
CHANGED