@fuzzle/opencode-accountant 0.10.1 → 0.10.2-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 +319 -109
- package/docs/configuration/symbol-map.md +70 -0
- package/docs/tools/import-pipeline.md +27 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,6 +29,24 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
29
29
|
var __require = import.meta.require;
|
|
30
30
|
|
|
31
31
|
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
32
|
+
var exports_js_yaml = {};
|
|
33
|
+
__export(exports_js_yaml, {
|
|
34
|
+
types: () => types,
|
|
35
|
+
safeLoadAll: () => safeLoadAll,
|
|
36
|
+
safeLoad: () => safeLoad,
|
|
37
|
+
safeDump: () => safeDump,
|
|
38
|
+
loadAll: () => loadAll,
|
|
39
|
+
load: () => load,
|
|
40
|
+
dump: () => dump,
|
|
41
|
+
default: () => jsYaml,
|
|
42
|
+
YAMLException: () => YAMLException,
|
|
43
|
+
Type: () => Type,
|
|
44
|
+
Schema: () => Schema,
|
|
45
|
+
JSON_SCHEMA: () => JSON_SCHEMA,
|
|
46
|
+
FAILSAFE_SCHEMA: () => FAILSAFE_SCHEMA,
|
|
47
|
+
DEFAULT_SCHEMA: () => DEFAULT_SCHEMA,
|
|
48
|
+
CORE_SCHEMA: () => CORE_SCHEMA
|
|
49
|
+
});
|
|
32
50
|
function isNothing(subject) {
|
|
33
51
|
return typeof subject === "undefined" || subject === null;
|
|
34
52
|
}
|
|
@@ -23060,19 +23078,12 @@ function findRulesForCsv(csvPath, mapping) {
|
|
|
23060
23078
|
|
|
23061
23079
|
// src/utils/hledgerExecutor.ts
|
|
23062
23080
|
var {$: $2 } = globalThis.Bun;
|
|
23063
|
-
var STDERR_TRUNCATE_LENGTH = 500;
|
|
23064
23081
|
var TX_HEADER_PATTERN = /^(\d{4})-(\d{2}-\d{2})(\s+(.+))?$/;
|
|
23065
23082
|
async function defaultHledgerExecutor(cmdArgs) {
|
|
23066
23083
|
try {
|
|
23067
23084
|
const result = await $2`hledger ${cmdArgs}`.quiet().nothrow();
|
|
23068
23085
|
const stdout = result.stdout.toString();
|
|
23069
23086
|
const stderr = result.stderr.toString();
|
|
23070
|
-
if (result.exitCode !== 0 && stderr) {
|
|
23071
|
-
process.stderr.write(`[hledger] command failed (exit ${result.exitCode}): hledger ${cmdArgs.join(" ")}
|
|
23072
|
-
`);
|
|
23073
|
-
process.stderr.write(`[hledger] stderr: ${stderr.slice(0, STDERR_TRUNCATE_LENGTH)}
|
|
23074
|
-
`);
|
|
23075
|
-
}
|
|
23076
23087
|
return {
|
|
23077
23088
|
stdout,
|
|
23078
23089
|
stderr,
|
|
@@ -23080,8 +23091,6 @@ async function defaultHledgerExecutor(cmdArgs) {
|
|
|
23080
23091
|
};
|
|
23081
23092
|
} catch (error45) {
|
|
23082
23093
|
const errorMessage = error45 instanceof Error ? error45.message : String(error45);
|
|
23083
|
-
process.stderr.write(`[hledger] exception: ${errorMessage}
|
|
23084
|
-
`);
|
|
23085
23094
|
return {
|
|
23086
23095
|
stdout: "",
|
|
23087
23096
|
stderr: errorMessage,
|
|
@@ -24092,6 +24101,7 @@ Note: This tool requires a contextId from a prior classify/import step.`,
|
|
|
24092
24101
|
}
|
|
24093
24102
|
});
|
|
24094
24103
|
// src/tools/import-pipeline.ts
|
|
24104
|
+
import * as fs20 from "fs";
|
|
24095
24105
|
import * as path14 from "path";
|
|
24096
24106
|
|
|
24097
24107
|
// src/utils/accountDeclarations.ts
|
|
@@ -24951,24 +24961,6 @@ function adjustLotsForSplit(inventory, symbol2, ratio, logger) {
|
|
|
24951
24961
|
}
|
|
24952
24962
|
logger?.info(`Adjusted ${lots.length} lots for ${symbol2} split (ratio: ${ratio})`);
|
|
24953
24963
|
}
|
|
24954
|
-
function adjustLotsForMerger(inventory, oldSymbol, newSymbol, ratio, logger) {
|
|
24955
|
-
const oldLots = inventory[oldSymbol];
|
|
24956
|
-
if (!oldLots || oldLots.length === 0) {
|
|
24957
|
-
logger?.warn(`No lots found for ${oldSymbol} during merger adjustment`);
|
|
24958
|
-
return;
|
|
24959
|
-
}
|
|
24960
|
-
const newLots = oldLots.map((lot) => ({
|
|
24961
|
-
...lot,
|
|
24962
|
-
quantity: lot.quantity * ratio,
|
|
24963
|
-
costBasis: lot.costBasis / ratio
|
|
24964
|
-
}));
|
|
24965
|
-
if (!inventory[newSymbol]) {
|
|
24966
|
-
inventory[newSymbol] = [];
|
|
24967
|
-
}
|
|
24968
|
-
inventory[newSymbol].push(...newLots);
|
|
24969
|
-
delete inventory[oldSymbol];
|
|
24970
|
-
logger?.info(`Merger: ${oldLots.length} lots moved from ${oldSymbol} to ${newSymbol} (ratio: ${ratio})`);
|
|
24971
|
-
}
|
|
24972
24964
|
function removeLots(inventory, symbol2, logger) {
|
|
24973
24965
|
const lots = inventory[symbol2];
|
|
24974
24966
|
if (!lots || lots.length === 0) {
|
|
@@ -25088,27 +25080,6 @@ function generateDividendEntry(dividend, logger) {
|
|
|
25088
25080
|
`;
|
|
25089
25081
|
return entry;
|
|
25090
25082
|
}
|
|
25091
|
-
function generateMergerEntry(action, oldQuantity, newQuantity, logger) {
|
|
25092
|
-
const date5 = formatDate(action.date);
|
|
25093
|
-
const newSymbol = action.newSymbol || "UNKNOWN";
|
|
25094
|
-
const description = escapeDescription(`Merger: ${action.symbol} -> ${newSymbol}`);
|
|
25095
|
-
logger?.debug(`Generating Merger entry: ${oldQuantity} ${action.symbol} -> ${newQuantity} ${newSymbol}`);
|
|
25096
|
-
let entry = `${date5} ${description}
|
|
25097
|
-
`;
|
|
25098
|
-
entry += ` ; swissquote:order:${action.orderNum}
|
|
25099
|
-
`;
|
|
25100
|
-
entry += ` ; Old ISIN: ${action.isin}, New ISIN: ${action.newIsin || "unknown"}
|
|
25101
|
-
`;
|
|
25102
|
-
entry += ` assets:investments:stocks:${action.symbol} -${formatQuantity(oldQuantity)} ${action.symbol}
|
|
25103
|
-
`;
|
|
25104
|
-
entry += ` equity:conversion ${formatQuantity(oldQuantity)} ${action.symbol}
|
|
25105
|
-
`;
|
|
25106
|
-
entry += ` equity:conversion -${formatQuantity(newQuantity)} ${newSymbol}
|
|
25107
|
-
`;
|
|
25108
|
-
entry += ` assets:investments:stocks:${newSymbol} ${formatQuantity(newQuantity)} ${newSymbol}
|
|
25109
|
-
`;
|
|
25110
|
-
return entry;
|
|
25111
|
-
}
|
|
25112
25083
|
function generateSplitEntry(action, oldQuantity, newQuantity, logger) {
|
|
25113
25084
|
const date5 = formatDate(action.date);
|
|
25114
25085
|
const ratio = action.ratio || newQuantity / oldQuantity;
|
|
@@ -25154,6 +25125,53 @@ function generateWorthlessEntry(action, removedLots, logger) {
|
|
|
25154
25125
|
`;
|
|
25155
25126
|
return entry;
|
|
25156
25127
|
}
|
|
25128
|
+
function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, logger) {
|
|
25129
|
+
const date5 = formatDate(group.date);
|
|
25130
|
+
const outSymbols = crossCurrencyOutgoingSymbols ?? group.outgoing.map((a) => a.symbol);
|
|
25131
|
+
const inSymbols = group.incoming.map((a) => a.symbol);
|
|
25132
|
+
const description = escapeDescription(`Merger: ${outSymbols.join(" + ")} -> ${inSymbols.join(" + ")}`);
|
|
25133
|
+
logger?.debug(`Generating multi-way merger entry: ${outSymbols.join(", ")} -> ${inSymbols.join(", ")}`);
|
|
25134
|
+
let entry = `${date5} ${description}
|
|
25135
|
+
`;
|
|
25136
|
+
entry += ` ; swissquote:order:${group.orderNum}
|
|
25137
|
+
`;
|
|
25138
|
+
const oldIsins = group.outgoing.map((a) => a.isin).filter(Boolean);
|
|
25139
|
+
const newIsins = group.incoming.map((a) => a.isin).filter(Boolean);
|
|
25140
|
+
if (oldIsins.length > 0 || newIsins.length > 0) {
|
|
25141
|
+
entry += ` ; Old ISIN: ${oldIsins.join(", ") || "n/a"}, New ISINs: ${newIsins.join(", ") || "n/a"}
|
|
25142
|
+
`;
|
|
25143
|
+
}
|
|
25144
|
+
for (const out of group.outgoing) {
|
|
25145
|
+
const qty = formatQuantity(Math.abs(out.quantity));
|
|
25146
|
+
entry += ` assets:investments:stocks:${out.symbol} -${qty} ${out.symbol}
|
|
25147
|
+
`;
|
|
25148
|
+
entry += ` equity:conversion ${qty} ${out.symbol}
|
|
25149
|
+
`;
|
|
25150
|
+
}
|
|
25151
|
+
for (const inc of group.incoming) {
|
|
25152
|
+
const qty = formatQuantity(Math.abs(inc.quantity));
|
|
25153
|
+
entry += ` equity:conversion -${qty} ${inc.symbol}
|
|
25154
|
+
`;
|
|
25155
|
+
entry += ` assets:investments:stocks:${inc.symbol} ${qty} ${inc.symbol}
|
|
25156
|
+
`;
|
|
25157
|
+
}
|
|
25158
|
+
return entry;
|
|
25159
|
+
}
|
|
25160
|
+
function generateRightsDistributionEntry(action, logger) {
|
|
25161
|
+
const date5 = formatDate(action.date);
|
|
25162
|
+
const qty = formatQuantity(Math.abs(action.quantity));
|
|
25163
|
+
const description = escapeDescription(`Rights Distribution: ${action.symbol} - ${action.name}`);
|
|
25164
|
+
logger?.debug(`Generating Rights Distribution entry: ${qty} ${action.symbol}`);
|
|
25165
|
+
let entry = `${date5} ${description}
|
|
25166
|
+
`;
|
|
25167
|
+
entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
|
|
25168
|
+
`;
|
|
25169
|
+
entry += ` assets:investments:stocks:${action.symbol} ${qty} ${action.symbol} @ 0.00 CAD
|
|
25170
|
+
`;
|
|
25171
|
+
entry += ` income:capital-gains:rights-distribution 0.00 CAD
|
|
25172
|
+
`;
|
|
25173
|
+
return entry;
|
|
25174
|
+
}
|
|
25157
25175
|
function formatJournalFile(entries, year, currency) {
|
|
25158
25176
|
const header = `; Swissquote ${currency.toUpperCase()} investment transactions for ${year}
|
|
25159
25177
|
; Generated by opencode-accountant
|
|
@@ -25172,8 +25190,15 @@ var SIMPLE_TRANSACTION_TYPES = new Set([
|
|
|
25172
25190
|
"Forex debit",
|
|
25173
25191
|
"Interest on debits",
|
|
25174
25192
|
"Interest on credits",
|
|
25193
|
+
"Interest on deposits",
|
|
25175
25194
|
"Exchange fees",
|
|
25176
|
-
"Exchange fees rectif."
|
|
25195
|
+
"Exchange fees rectif.",
|
|
25196
|
+
"Payment",
|
|
25197
|
+
"Debit",
|
|
25198
|
+
"Fees Tax Statement",
|
|
25199
|
+
"Dividend (Reversed)",
|
|
25200
|
+
"Reversal (Dividend)",
|
|
25201
|
+
"Redemption"
|
|
25177
25202
|
]);
|
|
25178
25203
|
var TRADE_TYPES = new Set(["Buy", "Sell"]);
|
|
25179
25204
|
var DIVIDEND_TYPES = new Set(["Dividend"]);
|
|
@@ -25181,7 +25206,10 @@ var CORPORATE_ACTION_TYPES = new Set([
|
|
|
25181
25206
|
"Merger",
|
|
25182
25207
|
"Reverse Split",
|
|
25183
25208
|
"Worthless Liquidation",
|
|
25184
|
-
"Internal exchange of securities"
|
|
25209
|
+
"Internal exchange of securities",
|
|
25210
|
+
"Exchange of securities",
|
|
25211
|
+
"Corporate Action",
|
|
25212
|
+
"Rights Distribution"
|
|
25185
25213
|
]);
|
|
25186
25214
|
var SKIP_TYPES = new Set([
|
|
25187
25215
|
"Internal exchange",
|
|
@@ -25290,45 +25318,41 @@ function toCorporateActionEntry(txn) {
|
|
|
25290
25318
|
symbol: txn.symbol,
|
|
25291
25319
|
name: txn.name,
|
|
25292
25320
|
isin: txn.isin,
|
|
25293
|
-
quantity:
|
|
25321
|
+
quantity: parseNumber(txn.quantity)
|
|
25294
25322
|
};
|
|
25295
25323
|
}
|
|
25296
|
-
|
|
25324
|
+
var MERGER_LIKE_TYPES = new Set([
|
|
25325
|
+
"Merger",
|
|
25326
|
+
"Internal exchange of securities",
|
|
25327
|
+
"Exchange of securities",
|
|
25328
|
+
"Corporate Action"
|
|
25329
|
+
]);
|
|
25330
|
+
function processCorporateActions(actions, inventory, lotInventoryPath, projectDir, logger) {
|
|
25297
25331
|
const entries = [];
|
|
25298
|
-
const pendingMergers = new Map;
|
|
25299
25332
|
actions.sort((a, b) => a.date.localeCompare(b.date));
|
|
25333
|
+
const mergerActions = [];
|
|
25334
|
+
const otherActions = [];
|
|
25300
25335
|
for (const action of actions) {
|
|
25336
|
+
if (MERGER_LIKE_TYPES.has(action.type)) {
|
|
25337
|
+
mergerActions.push(action);
|
|
25338
|
+
} else {
|
|
25339
|
+
otherActions.push(action);
|
|
25340
|
+
}
|
|
25341
|
+
}
|
|
25342
|
+
const mergerGroups = groupMergerActions(mergerActions);
|
|
25343
|
+
for (const group of mergerGroups) {
|
|
25344
|
+
const groupEntries = processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, logger);
|
|
25345
|
+
entries.push(...groupEntries);
|
|
25346
|
+
}
|
|
25347
|
+
for (const action of otherActions) {
|
|
25301
25348
|
switch (action.type) {
|
|
25302
|
-
case "Merger":
|
|
25303
|
-
case "Internal exchange of securities": {
|
|
25304
|
-
const key = `${action.date}-${action.orderNum}`;
|
|
25305
|
-
if (action.quantity < 0 || pendingMergers.has(key)) {
|
|
25306
|
-
const pending = pendingMergers.get(key);
|
|
25307
|
-
if (pending && pending.outgoing) {
|
|
25308
|
-
pending.incoming = action;
|
|
25309
|
-
const oldQty = Math.abs(parseNumber(String(pending.outgoing.quantity)));
|
|
25310
|
-
const newQty = Math.abs(parseNumber(String(action.quantity)));
|
|
25311
|
-
const ratio = newQty / oldQty;
|
|
25312
|
-
adjustLotsForMerger(inventory, pending.outgoing.symbol, action.symbol, ratio, logger);
|
|
25313
|
-
const entry = generateMergerEntry({
|
|
25314
|
-
...pending.outgoing,
|
|
25315
|
-
newSymbol: action.symbol,
|
|
25316
|
-
newIsin: action.isin,
|
|
25317
|
-
ratio
|
|
25318
|
-
}, oldQty, newQty, logger);
|
|
25319
|
-
entries.push(entry);
|
|
25320
|
-
pendingMergers.delete(key);
|
|
25321
|
-
} else {
|
|
25322
|
-
pendingMergers.set(key, { outgoing: action });
|
|
25323
|
-
}
|
|
25324
|
-
} else {
|
|
25325
|
-
pendingMergers.set(key, { outgoing: action });
|
|
25326
|
-
}
|
|
25327
|
-
break;
|
|
25328
|
-
}
|
|
25329
25349
|
case "Reverse Split": {
|
|
25350
|
+
const qty = action.quantity;
|
|
25351
|
+
if (qty < 0) {
|
|
25352
|
+
continue;
|
|
25353
|
+
}
|
|
25330
25354
|
const oldQty = getHeldQuantity(inventory, action.symbol);
|
|
25331
|
-
const newQty =
|
|
25355
|
+
const newQty = Math.abs(qty);
|
|
25332
25356
|
if (oldQty > 0) {
|
|
25333
25357
|
const ratio = newQty / oldQty;
|
|
25334
25358
|
adjustLotsForSplit(inventory, action.symbol, ratio, logger);
|
|
@@ -25349,14 +25373,170 @@ function processCorporateActions(actions, inventory, logger) {
|
|
|
25349
25373
|
}
|
|
25350
25374
|
break;
|
|
25351
25375
|
}
|
|
25376
|
+
case "Rights Distribution": {
|
|
25377
|
+
if (action.quantity > 0) {
|
|
25378
|
+
const entry = generateRightsDistributionEntry(action, logger);
|
|
25379
|
+
entries.push(entry);
|
|
25380
|
+
}
|
|
25381
|
+
break;
|
|
25382
|
+
}
|
|
25352
25383
|
}
|
|
25353
25384
|
}
|
|
25354
|
-
|
|
25355
|
-
|
|
25385
|
+
return entries;
|
|
25386
|
+
}
|
|
25387
|
+
function groupMergerActions(actions) {
|
|
25388
|
+
const groupMap = new Map;
|
|
25389
|
+
for (const action of actions) {
|
|
25390
|
+
const key = `${action.date}-${action.orderNum}`;
|
|
25391
|
+
if (!groupMap.has(key)) {
|
|
25392
|
+
groupMap.set(key, {
|
|
25393
|
+
key,
|
|
25394
|
+
date: action.date,
|
|
25395
|
+
orderNum: action.orderNum,
|
|
25396
|
+
outgoing: [],
|
|
25397
|
+
incoming: []
|
|
25398
|
+
});
|
|
25399
|
+
}
|
|
25400
|
+
const group = groupMap.get(key);
|
|
25401
|
+
if (action.quantity < 0) {
|
|
25402
|
+
group.outgoing.push(action);
|
|
25403
|
+
} else if (action.quantity > 0) {
|
|
25404
|
+
group.incoming.push(action);
|
|
25405
|
+
}
|
|
25356
25406
|
}
|
|
25407
|
+
return Array.from(groupMap.values());
|
|
25408
|
+
}
|
|
25409
|
+
function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, logger) {
|
|
25410
|
+
const entries = [];
|
|
25411
|
+
if (group.outgoing.length === 0 && group.incoming.length === 0) {
|
|
25412
|
+
return entries;
|
|
25413
|
+
}
|
|
25414
|
+
if (group.outgoing.length === 0 && group.incoming.length > 0) {
|
|
25415
|
+
const pendingState = loadPendingMerger(projectDir, lotInventoryPath, group.key, logger);
|
|
25416
|
+
if (pendingState) {
|
|
25417
|
+
const totalIncomingQty2 = group.incoming.reduce((sum, a) => sum + Math.abs(a.quantity), 0);
|
|
25418
|
+
for (const inc of group.incoming) {
|
|
25419
|
+
const absQty = Math.abs(inc.quantity);
|
|
25420
|
+
const proportion = absQty / totalIncomingQty2;
|
|
25421
|
+
const allocatedCost = pendingState.totalCostBasis * proportion;
|
|
25422
|
+
const costBasisPerUnit = absQty > 0 ? allocatedCost / absQty : 0;
|
|
25423
|
+
const lot = {
|
|
25424
|
+
date: formatDate(inc.date),
|
|
25425
|
+
quantity: absQty,
|
|
25426
|
+
costBasis: costBasisPerUnit,
|
|
25427
|
+
currency: pendingState.currency,
|
|
25428
|
+
isin: inc.isin,
|
|
25429
|
+
orderNum: inc.orderNum
|
|
25430
|
+
};
|
|
25431
|
+
if (!inventory[inc.symbol]) {
|
|
25432
|
+
inventory[inc.symbol] = [];
|
|
25433
|
+
}
|
|
25434
|
+
inventory[inc.symbol].push(lot);
|
|
25435
|
+
logger?.info(`Cross-currency merger incoming: ${absQty} ${inc.symbol} @ ${costBasisPerUnit.toFixed(2)} ${pendingState.currency}`);
|
|
25436
|
+
}
|
|
25437
|
+
const entry2 = generateMultiWayMergerEntry(group, pendingState.outgoingSymbols, logger);
|
|
25438
|
+
entries.push(entry2);
|
|
25439
|
+
removePendingMerger(projectDir, lotInventoryPath, group.key, logger);
|
|
25440
|
+
} else {
|
|
25441
|
+
logger?.warn(`Incoming-only merger group ${group.key} with no pending state \u2014 skipping`);
|
|
25442
|
+
}
|
|
25443
|
+
return entries;
|
|
25444
|
+
}
|
|
25445
|
+
let totalCostBasis = 0;
|
|
25446
|
+
let outgoingCurrency = "";
|
|
25447
|
+
const outgoingSymbols = [];
|
|
25448
|
+
for (const out of group.outgoing) {
|
|
25449
|
+
outgoingSymbols.push(out.symbol);
|
|
25450
|
+
const lots = inventory[out.symbol];
|
|
25451
|
+
if (lots) {
|
|
25452
|
+
for (const lot of lots) {
|
|
25453
|
+
totalCostBasis += lot.quantity * lot.costBasis;
|
|
25454
|
+
if (!outgoingCurrency && lot.currency) {
|
|
25455
|
+
outgoingCurrency = lot.currency;
|
|
25456
|
+
}
|
|
25457
|
+
}
|
|
25458
|
+
}
|
|
25459
|
+
}
|
|
25460
|
+
if (group.outgoing.length > 0 && group.incoming.length === 0) {
|
|
25461
|
+
for (const out of group.outgoing) {
|
|
25462
|
+
removeLots(inventory, out.symbol, logger);
|
|
25463
|
+
}
|
|
25464
|
+
savePendingMerger(projectDir, lotInventoryPath, group.key, {
|
|
25465
|
+
date: group.date,
|
|
25466
|
+
orderNum: group.orderNum,
|
|
25467
|
+
outgoingSymbols,
|
|
25468
|
+
totalCostBasis,
|
|
25469
|
+
currency: outgoingCurrency || "CAD"
|
|
25470
|
+
}, logger);
|
|
25471
|
+
logger?.info(`Cross-currency merger outgoing: ${outgoingSymbols.join(", ")} -> pending (cost basis: ${totalCostBasis.toFixed(2)})`);
|
|
25472
|
+
return entries;
|
|
25473
|
+
}
|
|
25474
|
+
for (const out of group.outgoing) {
|
|
25475
|
+
const absQty = Math.abs(out.quantity);
|
|
25476
|
+
removeLots(inventory, out.symbol, logger);
|
|
25477
|
+
logger?.debug(`Merger outgoing: removed ${absQty} ${out.symbol}`);
|
|
25478
|
+
}
|
|
25479
|
+
const totalIncomingQty = group.incoming.reduce((sum, a) => sum + Math.abs(a.quantity), 0);
|
|
25480
|
+
for (const inc of group.incoming) {
|
|
25481
|
+
const absQty = Math.abs(inc.quantity);
|
|
25482
|
+
const proportion = totalIncomingQty > 0 ? absQty / totalIncomingQty : 0;
|
|
25483
|
+
const allocatedCost = totalCostBasis * proportion;
|
|
25484
|
+
const costBasisPerUnit = absQty > 0 ? allocatedCost / absQty : 0;
|
|
25485
|
+
const lot = {
|
|
25486
|
+
date: formatDate(inc.date),
|
|
25487
|
+
quantity: absQty,
|
|
25488
|
+
costBasis: costBasisPerUnit,
|
|
25489
|
+
currency: outgoingCurrency || "CAD",
|
|
25490
|
+
isin: inc.isin,
|
|
25491
|
+
orderNum: inc.orderNum
|
|
25492
|
+
};
|
|
25493
|
+
if (!inventory[inc.symbol]) {
|
|
25494
|
+
inventory[inc.symbol] = [];
|
|
25495
|
+
}
|
|
25496
|
+
inventory[inc.symbol].push(lot);
|
|
25497
|
+
logger?.debug(`Merger incoming: added ${absQty} ${inc.symbol} @ ${costBasisPerUnit.toFixed(2)}`);
|
|
25498
|
+
}
|
|
25499
|
+
const entry = generateMultiWayMergerEntry(group, undefined, logger);
|
|
25500
|
+
entries.push(entry);
|
|
25357
25501
|
return entries;
|
|
25358
25502
|
}
|
|
25359
|
-
|
|
25503
|
+
function getPendingMergerDir(projectDir, lotInventoryPath) {
|
|
25504
|
+
const lotDir = lotInventoryPath.replace(/{symbol}[^/]*$/, "");
|
|
25505
|
+
return path13.join(projectDir, lotDir, "pending-mergers");
|
|
25506
|
+
}
|
|
25507
|
+
function savePendingMerger(projectDir, lotInventoryPath, key, state, logger) {
|
|
25508
|
+
const dir = getPendingMergerDir(projectDir, lotInventoryPath);
|
|
25509
|
+
if (!fs18.existsSync(dir)) {
|
|
25510
|
+
fs18.mkdirSync(dir, { recursive: true });
|
|
25511
|
+
}
|
|
25512
|
+
const filePath = path13.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
|
|
25513
|
+
fs18.writeFileSync(filePath, JSON.stringify(state, null, 2));
|
|
25514
|
+
logger?.debug(`Saved pending merger state: ${key}`);
|
|
25515
|
+
}
|
|
25516
|
+
function loadPendingMerger(projectDir, lotInventoryPath, key, logger) {
|
|
25517
|
+
const dir = getPendingMergerDir(projectDir, lotInventoryPath);
|
|
25518
|
+
const filePath = path13.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
|
|
25519
|
+
if (!fs18.existsSync(filePath)) {
|
|
25520
|
+
return null;
|
|
25521
|
+
}
|
|
25522
|
+
try {
|
|
25523
|
+
const content = fs18.readFileSync(filePath, "utf-8");
|
|
25524
|
+
return JSON.parse(content);
|
|
25525
|
+
} catch (error45) {
|
|
25526
|
+
const message = error45 instanceof Error ? error45.message : String(error45);
|
|
25527
|
+
logger?.warn(`Failed to load pending merger state for ${key}: ${message}`);
|
|
25528
|
+
return null;
|
|
25529
|
+
}
|
|
25530
|
+
}
|
|
25531
|
+
function removePendingMerger(projectDir, lotInventoryPath, key, logger) {
|
|
25532
|
+
const dir = getPendingMergerDir(projectDir, lotInventoryPath);
|
|
25533
|
+
const filePath = path13.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
|
|
25534
|
+
if (fs18.existsSync(filePath)) {
|
|
25535
|
+
fs18.unlinkSync(filePath);
|
|
25536
|
+
logger?.debug(`Removed pending merger state: ${key}`);
|
|
25537
|
+
}
|
|
25538
|
+
}
|
|
25539
|
+
async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInventoryPath = DEFAULT_LOT_INVENTORY_PATH, symbolMap = {}, logger) {
|
|
25360
25540
|
logger?.startSection(`Swissquote Preprocessing: ${path13.basename(csvPath)}`);
|
|
25361
25541
|
const stats = {
|
|
25362
25542
|
totalRows: 0,
|
|
@@ -25392,14 +25572,22 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
|
|
|
25392
25572
|
};
|
|
25393
25573
|
}
|
|
25394
25574
|
const transactions = [];
|
|
25575
|
+
const hasSymbolMap = Object.keys(symbolMap).length > 0;
|
|
25395
25576
|
for (let i2 = 1;i2 < lines.length; i2++) {
|
|
25396
25577
|
const txn = parseTransaction(lines[i2]);
|
|
25397
25578
|
if (txn) {
|
|
25579
|
+
if (hasSymbolMap && txn.symbol) {
|
|
25580
|
+
txn.symbol = symbolMap[txn.symbol] ?? txn.symbol;
|
|
25581
|
+
}
|
|
25398
25582
|
transactions.push(txn);
|
|
25399
25583
|
}
|
|
25400
25584
|
}
|
|
25401
25585
|
stats.totalRows = transactions.length;
|
|
25402
|
-
|
|
25586
|
+
if (hasSymbolMap) {
|
|
25587
|
+
logger?.logStep("parse-csv", "success", `Parsed ${stats.totalRows} transactions (symbol map applied)`);
|
|
25588
|
+
} else {
|
|
25589
|
+
logger?.logStep("parse-csv", "success", `Parsed ${stats.totalRows} transactions`);
|
|
25590
|
+
}
|
|
25403
25591
|
logger?.logStep("load-inventory", "start", "Loading lot inventory");
|
|
25404
25592
|
const inventory = loadLotInventory(projectDir, lotInventoryPath, logger);
|
|
25405
25593
|
logger?.logStep("load-inventory", "success", "Lot inventory loaded");
|
|
@@ -25424,8 +25612,17 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
|
|
|
25424
25612
|
stats.dividends++;
|
|
25425
25613
|
break;
|
|
25426
25614
|
case "corporate":
|
|
25427
|
-
|
|
25428
|
-
|
|
25615
|
+
if (!txn.symbol || txn.symbol.trim() === "") {
|
|
25616
|
+
if (txn.netAmount && txn.netAmount !== "-") {
|
|
25617
|
+
simpleTransactions.push(txn);
|
|
25618
|
+
stats.simpleTransactions++;
|
|
25619
|
+
} else {
|
|
25620
|
+
stats.skipped++;
|
|
25621
|
+
}
|
|
25622
|
+
} else {
|
|
25623
|
+
corporateActions.push(toCorporateActionEntry(txn));
|
|
25624
|
+
stats.corporateActions++;
|
|
25625
|
+
}
|
|
25429
25626
|
break;
|
|
25430
25627
|
case "skip":
|
|
25431
25628
|
stats.skipped++;
|
|
@@ -25481,7 +25678,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
|
|
|
25481
25678
|
}
|
|
25482
25679
|
if (corporateActions.length > 0) {
|
|
25483
25680
|
logger?.startSection("Corporate Actions Processing", 2);
|
|
25484
|
-
const corpEntries = processCorporateActions(corporateActions, inventory, logger);
|
|
25681
|
+
const corpEntries = processCorporateActions(corporateActions, inventory, lotInventoryPath, projectDir, logger);
|
|
25485
25682
|
journalEntries.push(...corpEntries);
|
|
25486
25683
|
logger?.logStep("corporate", "success", `Processed ${corporateActions.length} corporate actions`);
|
|
25487
25684
|
logger?.endSection();
|
|
@@ -25737,14 +25934,10 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
25737
25934
|
const importCtx = loadContext(context.directory, contextId);
|
|
25738
25935
|
if (importCtx.provider === "swissquote") {
|
|
25739
25936
|
const csvPath = path14.join(context.directory, importCtx.filePath);
|
|
25740
|
-
const
|
|
25937
|
+
const toDateMatch = importCtx.filename.match(/to-\d{4}(\d{4})\./);
|
|
25741
25938
|
let year = new Date().getFullYear();
|
|
25742
|
-
if (
|
|
25743
|
-
|
|
25744
|
-
year = parseInt(filenameMatch[3], 10);
|
|
25745
|
-
} else if (filenameMatch[4]) {
|
|
25746
|
-
year = parseInt(filenameMatch[4], 10);
|
|
25747
|
-
}
|
|
25939
|
+
if (toDateMatch) {
|
|
25940
|
+
year = parseInt(toDateMatch[1], 10);
|
|
25748
25941
|
}
|
|
25749
25942
|
swissquoteContexts.push({
|
|
25750
25943
|
contextId,
|
|
@@ -25762,6 +25955,23 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
25762
25955
|
const config2 = context.configLoader(context.directory);
|
|
25763
25956
|
const swissquoteProvider = config2.providers?.["swissquote"];
|
|
25764
25957
|
const lotInventoryPath = swissquoteProvider?.lotInventoryPath ?? DEFAULT_LOT_INVENTORY_PATH;
|
|
25958
|
+
const symbolMapPath = swissquoteProvider?.symbolMapPath ?? "config/import/symbolMap.yaml";
|
|
25959
|
+
let symbolMap = {};
|
|
25960
|
+
const symbolMapFullPath = path14.join(context.directory, symbolMapPath);
|
|
25961
|
+
if (fs20.existsSync(symbolMapFullPath)) {
|
|
25962
|
+
try {
|
|
25963
|
+
const yaml = await Promise.resolve().then(() => (init_js_yaml(), exports_js_yaml));
|
|
25964
|
+
const content = fs20.readFileSync(symbolMapFullPath, "utf-8");
|
|
25965
|
+
const parsed = yaml.load(content);
|
|
25966
|
+
if (parsed && typeof parsed === "object") {
|
|
25967
|
+
symbolMap = parsed;
|
|
25968
|
+
logger?.info(`Loaded symbol map with ${Object.keys(symbolMap).length} entries`);
|
|
25969
|
+
}
|
|
25970
|
+
} catch (error45) {
|
|
25971
|
+
const message = error45 instanceof Error ? error45.message : String(error45);
|
|
25972
|
+
logger?.warn(`Failed to load symbol map from ${symbolMapPath}: ${message}`);
|
|
25973
|
+
}
|
|
25974
|
+
}
|
|
25765
25975
|
try {
|
|
25766
25976
|
let totalStats = {
|
|
25767
25977
|
totalRows: 0,
|
|
@@ -25774,7 +25984,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
25774
25984
|
let lastJournalFile = null;
|
|
25775
25985
|
for (const sqCtx of swissquoteContexts) {
|
|
25776
25986
|
logger?.logStep("Swissquote Preprocess", "start", `Processing ${path14.basename(sqCtx.csvPath)}`);
|
|
25777
|
-
const result = await preprocessSwissquote(sqCtx.csvPath, context.directory, sqCtx.currency, sqCtx.year, lotInventoryPath, logger);
|
|
25987
|
+
const result = await preprocessSwissquote(sqCtx.csvPath, context.directory, sqCtx.currency, sqCtx.year, lotInventoryPath, symbolMap, logger);
|
|
25778
25988
|
totalStats.totalRows += result.stats.totalRows;
|
|
25779
25989
|
totalStats.simpleTransactions += result.stats.simpleTransactions;
|
|
25780
25990
|
totalStats.trades += result.stats.trades;
|
|
@@ -25792,7 +26002,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
25792
26002
|
}
|
|
25793
26003
|
logger?.logStep("Swissquote Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.corporateActions} corporate actions`);
|
|
25794
26004
|
}
|
|
25795
|
-
const message = `Preprocessed ${totalStats.totalRows} rows:
|
|
26005
|
+
const message = `Preprocessed ${totalStats.totalRows} rows: ${totalStats.trades} trades, ${totalStats.dividends} dividends, ${totalStats.corporateActions} corporate actions, ${totalStats.simpleTransactions} simple`;
|
|
25796
26006
|
context.result.steps.swissquotePreprocess = buildStepResult(true, message, {
|
|
25797
26007
|
...totalStats,
|
|
25798
26008
|
journalFile: lastJournalFile
|
|
@@ -26203,7 +26413,7 @@ This tool orchestrates the full import workflow:
|
|
|
26203
26413
|
}
|
|
26204
26414
|
});
|
|
26205
26415
|
// src/tools/init-directories.ts
|
|
26206
|
-
import * as
|
|
26416
|
+
import * as fs21 from "fs";
|
|
26207
26417
|
import * as path15 from "path";
|
|
26208
26418
|
async function initDirectories(directory) {
|
|
26209
26419
|
try {
|
|
@@ -26211,8 +26421,8 @@ async function initDirectories(directory) {
|
|
|
26211
26421
|
const directoriesCreated = [];
|
|
26212
26422
|
const gitkeepFiles = [];
|
|
26213
26423
|
const importBase = path15.join(directory, "import");
|
|
26214
|
-
if (!
|
|
26215
|
-
|
|
26424
|
+
if (!fs21.existsSync(importBase)) {
|
|
26425
|
+
fs21.mkdirSync(importBase, { recursive: true });
|
|
26216
26426
|
directoriesCreated.push("import");
|
|
26217
26427
|
}
|
|
26218
26428
|
const pathsToCreate = [
|
|
@@ -26223,19 +26433,19 @@ async function initDirectories(directory) {
|
|
|
26223
26433
|
];
|
|
26224
26434
|
for (const { path: dirPath } of pathsToCreate) {
|
|
26225
26435
|
const fullPath = path15.join(directory, dirPath);
|
|
26226
|
-
if (!
|
|
26227
|
-
|
|
26436
|
+
if (!fs21.existsSync(fullPath)) {
|
|
26437
|
+
fs21.mkdirSync(fullPath, { recursive: true });
|
|
26228
26438
|
directoriesCreated.push(dirPath);
|
|
26229
26439
|
}
|
|
26230
26440
|
const gitkeepPath = path15.join(fullPath, ".gitkeep");
|
|
26231
|
-
if (!
|
|
26232
|
-
|
|
26441
|
+
if (!fs21.existsSync(gitkeepPath)) {
|
|
26442
|
+
fs21.writeFileSync(gitkeepPath, "");
|
|
26233
26443
|
gitkeepFiles.push(path15.join(dirPath, ".gitkeep"));
|
|
26234
26444
|
}
|
|
26235
26445
|
}
|
|
26236
26446
|
const gitignorePath = path15.join(importBase, ".gitignore");
|
|
26237
26447
|
let gitignoreCreated = false;
|
|
26238
|
-
if (!
|
|
26448
|
+
if (!fs21.existsSync(gitignorePath)) {
|
|
26239
26449
|
const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
|
|
26240
26450
|
/incoming/*.csv
|
|
26241
26451
|
/incoming/*.pdf
|
|
@@ -26253,7 +26463,7 @@ async function initDirectories(directory) {
|
|
|
26253
26463
|
.DS_Store
|
|
26254
26464
|
Thumbs.db
|
|
26255
26465
|
`;
|
|
26256
|
-
|
|
26466
|
+
fs21.writeFileSync(gitignorePath, gitignoreContent);
|
|
26257
26467
|
gitignoreCreated = true;
|
|
26258
26468
|
}
|
|
26259
26469
|
const parts = [];
|
|
@@ -26330,13 +26540,13 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
|
|
|
26330
26540
|
});
|
|
26331
26541
|
// src/tools/generate-btc-purchases.ts
|
|
26332
26542
|
import * as path16 from "path";
|
|
26333
|
-
import * as
|
|
26543
|
+
import * as fs22 from "fs";
|
|
26334
26544
|
function findFiatCsvPaths(directory, pendingDir, provider) {
|
|
26335
26545
|
const providerDir = path16.join(directory, pendingDir, provider);
|
|
26336
|
-
if (!
|
|
26546
|
+
if (!fs22.existsSync(providerDir))
|
|
26337
26547
|
return [];
|
|
26338
26548
|
const csvPaths = [];
|
|
26339
|
-
const entries =
|
|
26549
|
+
const entries = fs22.readdirSync(providerDir, { withFileTypes: true });
|
|
26340
26550
|
for (const entry of entries) {
|
|
26341
26551
|
if (!entry.isDirectory())
|
|
26342
26552
|
continue;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Symbol Map Configuration
|
|
2
|
+
|
|
3
|
+
The symbol map allows you to map Swissquote's internal symbol names to canonical ticker symbols, matching your preferred naming convention (e.g., wealthfolio-style with exchange suffixes).
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Swissquote CSV exports use various symbol formats:
|
|
8
|
+
- Long descriptive names for corporate actions: `GOLD ROYALTY RG`, `VIZSLA ROYAL RG`
|
|
9
|
+
- Short tickers without exchange suffixes: `FNV`, `FVI`, `VCU`
|
|
10
|
+
- Warrant/CVR names: `VIZSLA WT 12.25`, `KINROSS GLD CVR`
|
|
11
|
+
|
|
12
|
+
The symbol map normalizes these to canonical symbols used consistently across journal entries, lot inventory filenames, and account names.
|
|
13
|
+
|
|
14
|
+
## File Location
|
|
15
|
+
|
|
16
|
+
Default: `config/import/symbolMap.yaml` (relative to project root)
|
|
17
|
+
|
|
18
|
+
Configurable via `symbolMapPath` in `providers.yaml`:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
providers:
|
|
22
|
+
swissquote:
|
|
23
|
+
symbolMapPath: config/import/symbolMap.yaml
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Format
|
|
27
|
+
|
|
28
|
+
Simple YAML key-value pairs where keys are Swissquote symbols and values are canonical symbols:
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
# Swissquote symbol -> canonical symbol
|
|
32
|
+
FNV: FNV.TO
|
|
33
|
+
FVI: FVI.TO
|
|
34
|
+
GOLD ROYALTY RG: GROY
|
|
35
|
+
VIZSLA ROYAL RG: VROY.V
|
|
36
|
+
KINROSS GLD CVR: KGC-CVR
|
|
37
|
+
VIZSLA WT 12.25: VROY-WT
|
|
38
|
+
VIZSLA WT. 12.2: VROY-WT
|
|
39
|
+
COLOSSUS MIN: CSI.TO
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Both keys and values are plain strings. Keys must exactly match the `Symbol` column in the Swissquote CSV.
|
|
43
|
+
|
|
44
|
+
## When Needed
|
|
45
|
+
|
|
46
|
+
A symbol map is required when:
|
|
47
|
+
- Swissquote uses long descriptive names (common in merger/corporate action entries) that contain spaces, which break hledger commodity parsing
|
|
48
|
+
- You want exchange suffixes (`.TO`, `.V`) for TSX/TSXV-listed stocks
|
|
49
|
+
- Multiple Swissquote names refer to the same security (e.g., `VIZSLA WT 12.25` and `VIZSLA WT. 12.2`)
|
|
50
|
+
- You want consistency with another portfolio tool (e.g., wealthfolio)
|
|
51
|
+
|
|
52
|
+
## Effect
|
|
53
|
+
|
|
54
|
+
The symbol map is applied **early** in preprocessing, immediately after CSV parsing and before any trade, lot, or journal processing. This means:
|
|
55
|
+
|
|
56
|
+
- **Journal entries** use mapped symbols in descriptions and account names
|
|
57
|
+
- **Lot inventory files** use mapped symbols in filenames (e.g., `GROY-lot.json` instead of `GOLD ROYALTY RG-lot.json`)
|
|
58
|
+
- **Dividend income accounts** use mapped symbols (e.g., `income:dividends:GROY`)
|
|
59
|
+
- **Capital gains tracking** uses mapped symbols throughout
|
|
60
|
+
|
|
61
|
+
## Example
|
|
62
|
+
|
|
63
|
+
Given a Swissquote CSV with:
|
|
64
|
+
```
|
|
65
|
+
15-03-2022;12345678;Merger;GOLD ROYALTY RG;GOLD ROYALTY CORP;CA38071H1064;-1000;...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Without symbol map: creates `GOLD ROYALTY RG-lot.json` and journal entries with `GOLD ROYALTY RG` (breaks hledger).
|
|
69
|
+
|
|
70
|
+
With symbol map (`GOLD ROYALTY RG: GROY`): creates `GROY-lot.json` and journal entries with `GROY`.
|
|
@@ -208,13 +208,33 @@ See [classify-statements](classify-statements.md) for details.
|
|
|
208
208
|
**What happens**:
|
|
209
209
|
|
|
210
210
|
1. Finds Swissquote contexts from classification
|
|
211
|
-
2.
|
|
212
|
-
3.
|
|
213
|
-
4. For
|
|
214
|
-
5. For
|
|
215
|
-
6. For
|
|
216
|
-
7.
|
|
217
|
-
|
|
211
|
+
2. Loads **symbol map** from `config/import/symbolMap.yaml` (if present) and applies it to all transaction symbols before processing
|
|
212
|
+
3. Parses CSV to identify transaction types and routes them accordingly
|
|
213
|
+
4. For Buy transactions: adds lots to per-symbol inventory files (`ledger/investments/lot-inventory/{symbol}-lot.json`)
|
|
214
|
+
5. For Sell transactions: consumes lots using FIFO, calculates capital gains/losses
|
|
215
|
+
6. For Dividends: generates dividend income entries with withholding tax
|
|
216
|
+
7. For Corporate Actions:
|
|
217
|
+
- **Mergers** (including multi-way): groups by date+orderNum, partitions into outgoing/incoming, transfers cost basis proportionally
|
|
218
|
+
- **Reverse Splits**: adjusts lot quantities and cost basis
|
|
219
|
+
- **Worthless Liquidations**: removes lots and records capital loss
|
|
220
|
+
- **Rights Distributions**: adds shares at zero cost basis
|
|
221
|
+
- **Cross-currency mergers**: saves outgoing side to pending state, loads when incoming side is processed
|
|
222
|
+
8. Generates per-year investment journal entries (`ledger/investments/{year}-{currency}.journal`) based on the CSV filename's date range
|
|
223
|
+
9. Outputs filtered CSV (simple transactions only) for hledger rules import
|
|
224
|
+
|
|
225
|
+
**Symbol Map**: Maps Swissquote's internal symbol names (e.g., `GOLD ROYALTY RG`, `VIZSLA WT 12.25`) to canonical ticker symbols (e.g., `GROY`, `VROY-WT`). Applied before any processing, so journal entries, lot inventory filenames, and account names all use the mapped symbols. See [Symbol Map Configuration](../configuration/symbol-map.md).
|
|
226
|
+
|
|
227
|
+
**Per-Year Journals**: Each CSV writes to a journal file named `{year}-{currency}.journal` where the year is extracted from the `to-DDMMYYYY` portion of the filename (e.g., `swissquote-cad-transactions-01012013-to-31122013.csv` → `2013-cad.journal`).
|
|
228
|
+
|
|
229
|
+
**Multi-Way Mergers**: Mergers can involve multiple incoming/outgoing securities (1→N, N→1, N→M). The preprocessor groups all entries sharing the same date+orderNum, collects total cost basis from outgoing lots, and distributes it proportionally across incoming symbols.
|
|
230
|
+
|
|
231
|
+
**Cross-Currency Mergers**: When the outgoing and incoming sides of a merger appear in different currency CSV files, the outgoing side saves its state to `pending-mergers/` under the lot inventory directory. When the incoming side is processed later, it loads the pending state to create lots with the correct cost basis.
|
|
232
|
+
|
|
233
|
+
**Supported Transaction Types**:
|
|
234
|
+
- **Simple** (passed to rules import): Custody Fees, Forex credit/debit, Interest on debits/credits/deposits, Exchange fees, Payment, Debit, Fees Tax Statement, Dividend (Reversed), Reversal (Dividend), Redemption
|
|
235
|
+
- **Trades**: Buy, Sell
|
|
236
|
+
- **Dividends**: Dividend
|
|
237
|
+
- **Corporate Actions**: Merger, Internal exchange of securities, Exchange of securities, Corporate Action, Reverse Split, Worthless Liquidation, Rights Distribution
|
|
218
238
|
|
|
219
239
|
**Why**: hledger CSV rules can't perform FIFO lot matching, capital gains calculations, or handle complex corporate actions. The preprocessor handles these computations and generates proper journal entries, while passing through simple transactions (fees, forex, interest) for standard rules-based import.
|
|
220
240
|
|