@fuzzle/opencode-accountant 0.13.13 → 0.13.14
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 +159 -40
- 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,6 +25614,44 @@ var MERGER_LIKE_TYPES = new Set([
|
|
|
25573
25614
|
"Exchange of securities",
|
|
25574
25615
|
"Corporate Action"
|
|
25575
25616
|
]);
|
|
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;
|
|
25623
|
+
}
|
|
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`);
|
|
25633
|
+
}
|
|
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);
|
|
25650
|
+
}
|
|
25651
|
+
break;
|
|
25652
|
+
}
|
|
25653
|
+
}
|
|
25654
|
+
}
|
|
25576
25655
|
function processCorporateActions(actions, inventory, lotInventoryPath, projectDir, logger) {
|
|
25577
25656
|
const entries = [];
|
|
25578
25657
|
actions.sort((a, b) => formatDate(a.date).localeCompare(formatDate(b.date)));
|
|
@@ -25586,46 +25665,19 @@ function processCorporateActions(actions, inventory, lotInventoryPath, projectDi
|
|
|
25586
25665
|
}
|
|
25587
25666
|
}
|
|
25588
25667
|
const mergerGroups = groupMergerActions(mergerActions);
|
|
25589
|
-
|
|
25590
|
-
|
|
25591
|
-
|
|
25592
|
-
|
|
25593
|
-
|
|
25594
|
-
|
|
25595
|
-
|
|
25596
|
-
|
|
25597
|
-
|
|
25598
|
-
|
|
25599
|
-
|
|
25600
|
-
|
|
25601
|
-
|
|
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;
|
|
25611
|
-
}
|
|
25612
|
-
case "Worthless Liquidation": {
|
|
25613
|
-
const removedLots = removeLots(inventory, action.symbol, logger);
|
|
25614
|
-
if (removedLots.length > 0) {
|
|
25615
|
-
const entry = generateWorthlessEntry(action, removedLots, logger);
|
|
25616
|
-
entries.push(entry);
|
|
25617
|
-
} else {
|
|
25618
|
-
logger?.warn(`Worthless liquidation for ${action.symbol} but no lots found`);
|
|
25619
|
-
}
|
|
25620
|
-
break;
|
|
25621
|
-
}
|
|
25622
|
-
case "Rights Distribution": {
|
|
25623
|
-
if (action.quantity > 0) {
|
|
25624
|
-
const entry = generateRightsDistributionEntry(action, logger);
|
|
25625
|
-
entries.push(entry);
|
|
25626
|
-
}
|
|
25627
|
-
break;
|
|
25628
|
-
}
|
|
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++;
|
|
25629
25681
|
}
|
|
25630
25682
|
}
|
|
25631
25683
|
return entries;
|
|
@@ -25712,6 +25764,7 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
|
|
|
25712
25764
|
orderNum: group.orderNum,
|
|
25713
25765
|
outgoingSymbols,
|
|
25714
25766
|
outgoingIsins: group.outgoing.map((a) => a.isin),
|
|
25767
|
+
outgoingQuantities: group.outgoing.map((a) => Math.abs(a.quantity)),
|
|
25715
25768
|
totalCostBasis,
|
|
25716
25769
|
currency: outgoingCurrency || "CAD"
|
|
25717
25770
|
}, logger);
|
|
@@ -25785,6 +25838,68 @@ function removePendingMerger(projectDir, lotInventoryPath, key, logger) {
|
|
|
25785
25838
|
logger?.debug(`Removed pending merger state: ${key}`);
|
|
25786
25839
|
}
|
|
25787
25840
|
}
|
|
25841
|
+
function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
|
|
25842
|
+
const pendingDir = getPendingMergerDir(projectDir, lotInventoryPath);
|
|
25843
|
+
if (!fs18.existsSync(pendingDir)) {
|
|
25844
|
+
return { resolved: 0, journalFilesUpdated: [] };
|
|
25845
|
+
}
|
|
25846
|
+
const files = fs18.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
|
|
25847
|
+
if (files.length === 0) {
|
|
25848
|
+
return { resolved: 0, journalFilesUpdated: [] };
|
|
25849
|
+
}
|
|
25850
|
+
const entriesByJournal = new Map;
|
|
25851
|
+
const resolvedFiles = [];
|
|
25852
|
+
for (const file2 of files) {
|
|
25853
|
+
const filePath = path13.join(pendingDir, file2);
|
|
25854
|
+
try {
|
|
25855
|
+
const content = fs18.readFileSync(filePath, "utf-8");
|
|
25856
|
+
const state = JSON.parse(content);
|
|
25857
|
+
const dateMatch = state.date.match(/\d{2}-\d{2}-(\d{4})/);
|
|
25858
|
+
if (!dateMatch) {
|
|
25859
|
+
logger?.warn(`Invalid date in pending merger ${file2}: ${state.date}`);
|
|
25860
|
+
continue;
|
|
25861
|
+
}
|
|
25862
|
+
const year = parseInt(dateMatch[1], 10);
|
|
25863
|
+
const entry = generatePendingMergerWorthlessEntry(state.date, state.orderNum, state.outgoingSymbols, state.outgoingIsins, state.outgoingQuantities || [], state.totalCostBasis, state.currency, logger);
|
|
25864
|
+
const journalFile = path13.join(projectDir, "ledger", "investments", `${year}-${state.currency.toLowerCase()}.journal`);
|
|
25865
|
+
if (!entriesByJournal.has(journalFile)) {
|
|
25866
|
+
entriesByJournal.set(journalFile, []);
|
|
25867
|
+
}
|
|
25868
|
+
entriesByJournal.get(journalFile).push(entry);
|
|
25869
|
+
fs18.unlinkSync(filePath);
|
|
25870
|
+
resolvedFiles.push(file2);
|
|
25871
|
+
logger?.info(`Resolved pending merger ${file2} as worthless: ${state.outgoingSymbols.join(", ")} (${state.totalCostBasis.toFixed(2)} ${state.currency})`);
|
|
25872
|
+
} catch (error45) {
|
|
25873
|
+
const message = error45 instanceof Error ? error45.message : String(error45);
|
|
25874
|
+
logger?.warn(`Failed to resolve pending merger ${file2}: ${message}`);
|
|
25875
|
+
}
|
|
25876
|
+
}
|
|
25877
|
+
const journalFilesUpdated = [];
|
|
25878
|
+
for (const [journalFile, journalEntries] of entriesByJournal) {
|
|
25879
|
+
if (fs18.existsSync(journalFile)) {
|
|
25880
|
+
fs18.appendFileSync(journalFile, `
|
|
25881
|
+
` + journalEntries.join(`
|
|
25882
|
+
`));
|
|
25883
|
+
} else {
|
|
25884
|
+
const basename5 = path13.basename(journalFile, ".journal");
|
|
25885
|
+
const parts = basename5.split("-");
|
|
25886
|
+
const yearStr = parts[0];
|
|
25887
|
+
const currency = parts.slice(1).join("-");
|
|
25888
|
+
const header = formatJournalFile(journalEntries, parseInt(yearStr, 10), currency);
|
|
25889
|
+
fs18.writeFileSync(journalFile, header);
|
|
25890
|
+
}
|
|
25891
|
+
journalFilesUpdated.push(journalFile);
|
|
25892
|
+
logger?.info(`Updated ${path13.basename(journalFile)} with worthless resolution entries`);
|
|
25893
|
+
}
|
|
25894
|
+
const remaining = fs18.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
|
|
25895
|
+
if (remaining.length === 0) {
|
|
25896
|
+
try {
|
|
25897
|
+
fs18.rmdirSync(pendingDir);
|
|
25898
|
+
logger?.debug("Removed empty pending-mergers directory");
|
|
25899
|
+
} catch {}
|
|
25900
|
+
}
|
|
25901
|
+
return { resolved: resolvedFiles.length, journalFilesUpdated };
|
|
25902
|
+
}
|
|
25788
25903
|
async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInventoryPath = DEFAULT_LOT_INVENTORY_PATH, symbolMap = {}, logger) {
|
|
25789
25904
|
logger?.startSection(`Swissquote Preprocessing: ${path13.basename(csvPath)}`);
|
|
25790
25905
|
const stats = {
|
|
@@ -26730,6 +26845,10 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
26730
26845
|
}
|
|
26731
26846
|
logger?.logStep("Swissquote Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.corporateActions} corporate actions, ${result.stats.forexTransactions} forex`);
|
|
26732
26847
|
}
|
|
26848
|
+
const pendingResolution = resolveRemainingPendingMergers(context.directory, lotInventoryPath, logger);
|
|
26849
|
+
if (pendingResolution.resolved > 0) {
|
|
26850
|
+
logger?.logStep("Swissquote Pending Mergers", "success", `Resolved ${pendingResolution.resolved} pending merger(s) as worthless`);
|
|
26851
|
+
}
|
|
26733
26852
|
if (allForexRows.length > 0) {
|
|
26734
26853
|
const firstCtx = swissquoteContexts[0];
|
|
26735
26854
|
const yearJournalPath = path16.join(context.directory, "ledger", `${firstCtx.year}.journal`);
|