@fuzzle/opencode-accountant 0.16.2-next.1 → 0.16.3-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 +57 -131
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26989,62 +26989,6 @@ function generateIbkrDividendEntry(dividend, logger) {
|
|
|
26989
26989
|
`;
|
|
26990
26990
|
return entry;
|
|
26991
26991
|
}
|
|
26992
|
-
function generateIbkrAdjustmentEntry(adjustment, logger) {
|
|
26993
|
-
const description = escapeDescription(adjustment.description);
|
|
26994
|
-
logger?.debug(`Generating IBKR Adjustment entry: ${adjustment.amount} ${adjustment.currency}`);
|
|
26995
|
-
const postings = [
|
|
26996
|
-
{
|
|
26997
|
-
account: `assets:broker:ibkr:${adjustment.currency.toLowerCase()}`,
|
|
26998
|
-
amount: formatAmount(adjustment.amount, adjustment.currency)
|
|
26999
|
-
}
|
|
27000
|
-
];
|
|
27001
|
-
if (adjustment.amount < 0) {
|
|
27002
|
-
postings.push({
|
|
27003
|
-
account: "expenses:fx-losses:ibkr",
|
|
27004
|
-
amount: formatAmount(Math.abs(adjustment.amount), adjustment.currency)
|
|
27005
|
-
});
|
|
27006
|
-
} else {
|
|
27007
|
-
postings.push({
|
|
27008
|
-
account: "income:fx-gains:ibkr",
|
|
27009
|
-
amount: formatAmount(-adjustment.amount, adjustment.currency)
|
|
27010
|
-
});
|
|
27011
|
-
}
|
|
27012
|
-
let entry = `${adjustment.date} ${description}
|
|
27013
|
-
`;
|
|
27014
|
-
entry += ` ; ibkr:account:${adjustment.account}
|
|
27015
|
-
`;
|
|
27016
|
-
entry += formatPostings(postings) + `
|
|
27017
|
-
`;
|
|
27018
|
-
return entry;
|
|
27019
|
-
}
|
|
27020
|
-
function generateIbkrForexEntry(entry, logger) {
|
|
27021
|
-
const description = escapeDescription(entry.description);
|
|
27022
|
-
logger?.debug(`Generating IBKR Forex entry: ${entry.amount} ${entry.currency}`);
|
|
27023
|
-
const postings = [
|
|
27024
|
-
{
|
|
27025
|
-
account: `assets:broker:ibkr:${entry.currency.toLowerCase()}`,
|
|
27026
|
-
amount: formatAmount(entry.amount, entry.currency)
|
|
27027
|
-
}
|
|
27028
|
-
];
|
|
27029
|
-
if (entry.amount < 0) {
|
|
27030
|
-
postings.push({
|
|
27031
|
-
account: "expenses:fees:forex:ibkr",
|
|
27032
|
-
amount: formatAmount(Math.abs(entry.amount), entry.currency)
|
|
27033
|
-
});
|
|
27034
|
-
} else {
|
|
27035
|
-
postings.push({
|
|
27036
|
-
account: "income:fx-gains:ibkr",
|
|
27037
|
-
amount: formatAmount(-entry.amount, entry.currency)
|
|
27038
|
-
});
|
|
27039
|
-
}
|
|
27040
|
-
let result = `${entry.date} ${description}
|
|
27041
|
-
`;
|
|
27042
|
-
result += ` ; ibkr:account:${entry.account}
|
|
27043
|
-
`;
|
|
27044
|
-
result += formatPostings(postings) + `
|
|
27045
|
-
`;
|
|
27046
|
-
return result;
|
|
27047
|
-
}
|
|
27048
26992
|
|
|
27049
26993
|
// src/utils/ibkrCsvPreprocessor.ts
|
|
27050
26994
|
var TRADE_TYPES2 = new Set(["Buy", "Sell"]);
|
|
@@ -27126,25 +27070,34 @@ function groupDividends(transactions) {
|
|
|
27126
27070
|
groups.sort((a, b) => a.date.localeCompare(b.date));
|
|
27127
27071
|
return groups;
|
|
27128
27072
|
}
|
|
27073
|
+
function isIbkrEntryDuplicate(entry, existingJournalContent) {
|
|
27074
|
+
if (!existingJournalContent)
|
|
27075
|
+
return false;
|
|
27076
|
+
const firstLine = entry.split(`
|
|
27077
|
+
`)[0].trim();
|
|
27078
|
+
if (!firstLine)
|
|
27079
|
+
return false;
|
|
27080
|
+
if (!existingJournalContent.includes(firstLine))
|
|
27081
|
+
return false;
|
|
27082
|
+
const brokerLine = entry.split(`
|
|
27083
|
+
`).find((l) => l.includes("assets:broker:ibkr:"));
|
|
27084
|
+
if (!brokerLine)
|
|
27085
|
+
return true;
|
|
27086
|
+
const amountMatch = brokerLine.match(/(-?\d+\.\d+\s+[A-Z]+)/);
|
|
27087
|
+
if (!amountMatch)
|
|
27088
|
+
return true;
|
|
27089
|
+
const idx = existingJournalContent.indexOf(firstLine);
|
|
27090
|
+
const chunk = existingJournalContent.slice(idx, idx + 500);
|
|
27091
|
+
return chunk.includes(amountMatch[1]);
|
|
27092
|
+
}
|
|
27129
27093
|
async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPath, logger) {
|
|
27130
27094
|
const csvDir = path17.dirname(csvPath);
|
|
27131
27095
|
const csvBasename = path17.basename(csvPath, ".csv");
|
|
27132
27096
|
const journalPath = path17.join(directory, "ledger", "investments", `${year}-ibkr-${currency}.journal`);
|
|
27097
|
+
let existingJournalContent = "";
|
|
27133
27098
|
if (fs22.existsSync(journalPath)) {
|
|
27134
|
-
|
|
27135
|
-
|
|
27136
|
-
simpleTransactionsCsv: null,
|
|
27137
|
-
journalFile: journalPath,
|
|
27138
|
-
stats: {
|
|
27139
|
-
totalRows: 0,
|
|
27140
|
-
deposits: 0,
|
|
27141
|
-
trades: 0,
|
|
27142
|
-
dividends: 0,
|
|
27143
|
-
adjustments: 0,
|
|
27144
|
-
forex: 0
|
|
27145
|
-
},
|
|
27146
|
-
alreadyPreprocessed: true
|
|
27147
|
-
};
|
|
27099
|
+
existingJournalContent = fs22.readFileSync(journalPath, "utf-8");
|
|
27100
|
+
logger?.info(`Found existing IBKR journal: ${journalPath}, will check for new entries`);
|
|
27148
27101
|
}
|
|
27149
27102
|
const content = fs22.readFileSync(csvPath, "utf-8");
|
|
27150
27103
|
const transactions = parseIbkrCsv(content);
|
|
@@ -27175,7 +27128,6 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27175
27128
|
const usedAccounts = new Set;
|
|
27176
27129
|
const journalEntries = [];
|
|
27177
27130
|
for (const trade of trades) {
|
|
27178
|
-
tradedSymbols.add(trade.symbol);
|
|
27179
27131
|
const totalCost = Math.abs(trade.grossAmount);
|
|
27180
27132
|
const unitPrice = trade.quantity !== 0 ? totalCost / Math.abs(trade.quantity) : 0;
|
|
27181
27133
|
const tradeEntry = {
|
|
@@ -27191,6 +27143,11 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27191
27143
|
currency: currency.toUpperCase()
|
|
27192
27144
|
};
|
|
27193
27145
|
if (trade.transactionType === "Buy") {
|
|
27146
|
+
const entry = generateIbkrBuyEntry(tradeEntry, logger);
|
|
27147
|
+
if (isIbkrEntryDuplicate(entry, existingJournalContent)) {
|
|
27148
|
+
logger?.debug(`Skipping duplicate Buy: ${trade.date} ${trade.symbol}`);
|
|
27149
|
+
continue;
|
|
27150
|
+
}
|
|
27194
27151
|
const tradeInfo = {
|
|
27195
27152
|
date: trade.date,
|
|
27196
27153
|
orderNum: "",
|
|
@@ -27201,13 +27158,19 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27201
27158
|
currency: currency.toUpperCase()
|
|
27202
27159
|
};
|
|
27203
27160
|
addLot(inventory, tradeInfo, logger);
|
|
27204
|
-
journalEntries.push(
|
|
27161
|
+
journalEntries.push(entry);
|
|
27162
|
+
tradedSymbols.add(trade.symbol);
|
|
27205
27163
|
usedAccounts.add(`assets:investments:stocks:${trade.symbol}`);
|
|
27206
27164
|
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27207
27165
|
usedAccounts.add("expenses:fees:trading:ibkr");
|
|
27208
27166
|
} else {
|
|
27167
|
+
if (existingJournalContent.includes(`${trade.date} Sell ${trade.symbol} - `)) {
|
|
27168
|
+
logger?.debug(`Skipping duplicate Sell: ${trade.date} ${trade.symbol}`);
|
|
27169
|
+
continue;
|
|
27170
|
+
}
|
|
27209
27171
|
const consumed = consumeLotsFIFO(inventory, trade.symbol, Math.abs(trade.quantity), logger);
|
|
27210
27172
|
journalEntries.push(generateIbkrSellEntry(tradeEntry, consumed, logger));
|
|
27173
|
+
tradedSymbols.add(trade.symbol);
|
|
27211
27174
|
usedAccounts.add(`assets:investments:stocks:${trade.symbol}`);
|
|
27212
27175
|
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27213
27176
|
usedAccounts.add("expenses:fees:trading:ibkr");
|
|
@@ -27226,74 +27189,36 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27226
27189
|
netAmount,
|
|
27227
27190
|
currency: currency.toUpperCase()
|
|
27228
27191
|
};
|
|
27229
|
-
|
|
27192
|
+
const entry = generateIbkrDividendEntry(dividendEntry, logger);
|
|
27193
|
+
if (isIbkrEntryDuplicate(entry, existingJournalContent)) {
|
|
27194
|
+
logger?.debug(`Skipping duplicate Dividend: ${group.date} ${group.symbol}`);
|
|
27195
|
+
continue;
|
|
27196
|
+
}
|
|
27197
|
+
journalEntries.push(entry);
|
|
27230
27198
|
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27231
27199
|
usedAccounts.add(`income:dividends:${group.symbol}`);
|
|
27232
27200
|
if (group.withholdingTax > 0) {
|
|
27233
27201
|
usedAccounts.add("expenses:taxes:withholding");
|
|
27234
27202
|
}
|
|
27235
27203
|
}
|
|
27236
|
-
adjustments.sort((a, b) => a.date.localeCompare(b.date));
|
|
27237
|
-
for (const adj of adjustments) {
|
|
27238
|
-
const adjustmentEntry = {
|
|
27239
|
-
date: adj.date,
|
|
27240
|
-
account: adj.account,
|
|
27241
|
-
description: adj.description || "FX Translations P&L",
|
|
27242
|
-
amount: adj.netAmount,
|
|
27243
|
-
currency: currency.toUpperCase()
|
|
27244
|
-
};
|
|
27245
|
-
journalEntries.push(generateIbkrAdjustmentEntry(adjustmentEntry, logger));
|
|
27246
|
-
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27247
|
-
if (adj.netAmount < 0) {
|
|
27248
|
-
usedAccounts.add("expenses:fx-losses:ibkr");
|
|
27249
|
-
} else {
|
|
27250
|
-
usedAccounts.add("income:fx-gains:ibkr");
|
|
27251
|
-
}
|
|
27252
|
-
}
|
|
27253
|
-
if (forexTxns.length > 0) {
|
|
27254
|
-
const forexByDate = new Map;
|
|
27255
|
-
for (const txn of forexTxns) {
|
|
27256
|
-
const existing = forexByDate.get(txn.date);
|
|
27257
|
-
if (existing) {
|
|
27258
|
-
existing.total += txn.netAmount;
|
|
27259
|
-
} else {
|
|
27260
|
-
forexByDate.set(txn.date, { account: txn.account, total: txn.netAmount });
|
|
27261
|
-
}
|
|
27262
|
-
}
|
|
27263
|
-
const sortedDates = [...forexByDate.keys()].sort();
|
|
27264
|
-
for (const date5 of sortedDates) {
|
|
27265
|
-
const { account, total } = forexByDate.get(date5);
|
|
27266
|
-
const amount = Math.round(total * 100) / 100;
|
|
27267
|
-
if (amount === 0)
|
|
27268
|
-
continue;
|
|
27269
|
-
const forexEntry = {
|
|
27270
|
-
date: date5,
|
|
27271
|
-
account,
|
|
27272
|
-
description: "Forex conversion cost",
|
|
27273
|
-
amount,
|
|
27274
|
-
currency: currency.toUpperCase()
|
|
27275
|
-
};
|
|
27276
|
-
journalEntries.push(generateIbkrForexEntry(forexEntry, logger));
|
|
27277
|
-
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27278
|
-
if (amount < 0) {
|
|
27279
|
-
usedAccounts.add("expenses:fees:forex:ibkr");
|
|
27280
|
-
} else {
|
|
27281
|
-
usedAccounts.add("income:fx-gains:ibkr");
|
|
27282
|
-
}
|
|
27283
|
-
}
|
|
27284
|
-
}
|
|
27285
27204
|
saveLotInventory(directory, lotInventoryPath, inventory, logger);
|
|
27286
|
-
let journalFilePath = null;
|
|
27205
|
+
let journalFilePath = existingJournalContent ? journalPath : null;
|
|
27287
27206
|
if (journalEntries.length > 0) {
|
|
27288
|
-
const header = `; IBKR ${currency.toUpperCase()} investment transactions for ${year}
|
|
27289
|
-
; Generated by opencode-accountant
|
|
27290
|
-
; This file is auto-generated - do not edit manually`;
|
|
27291
|
-
const journalContent = formatJournalFile(journalEntries, header);
|
|
27292
27207
|
const investmentsDir = path17.join(directory, "ledger", "investments");
|
|
27293
27208
|
if (!fs22.existsSync(investmentsDir)) {
|
|
27294
27209
|
fs22.mkdirSync(investmentsDir, { recursive: true });
|
|
27295
27210
|
}
|
|
27296
|
-
|
|
27211
|
+
if (existingJournalContent) {
|
|
27212
|
+
fs22.appendFileSync(journalPath, `
|
|
27213
|
+
` + journalEntries.join(`
|
|
27214
|
+
`));
|
|
27215
|
+
} else {
|
|
27216
|
+
const header = `; IBKR ${currency.toUpperCase()} investment transactions for ${year}
|
|
27217
|
+
; Generated by opencode-accountant
|
|
27218
|
+
; This file is auto-generated - do not edit manually`;
|
|
27219
|
+
const journalContent = formatJournalFile(journalEntries, header);
|
|
27220
|
+
fs22.writeFileSync(journalPath, journalContent);
|
|
27221
|
+
}
|
|
27297
27222
|
journalFilePath = journalPath;
|
|
27298
27223
|
if (tradedSymbols.size > 0) {
|
|
27299
27224
|
const commodityJournalPath = path17.join(directory, "ledger", "investments", "commodities.journal");
|
|
@@ -27306,11 +27231,12 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27306
27231
|
ensureStockPriceEntries(directory, tradedSymbols, logger);
|
|
27307
27232
|
logger?.info(`Generated IBKR journal: ${journalPath} with ${journalEntries.length} entries`);
|
|
27308
27233
|
}
|
|
27234
|
+
const simpleTransactions = [...deposits, ...adjustments, ...forexTxns];
|
|
27309
27235
|
let simpleTransactionsCsvPath = null;
|
|
27310
|
-
if (
|
|
27236
|
+
if (simpleTransactions.length > 0) {
|
|
27311
27237
|
const filteredCsvPath = path17.join(csvDir, `${csvBasename}-filtered.csv`);
|
|
27312
27238
|
const csvHeader = "Date,Account,Description,Transaction Type,Symbol,Quantity,Price,Price Currency,Gross Amount,Commission,Net Amount";
|
|
27313
|
-
const csvRows =
|
|
27239
|
+
const csvRows = simpleTransactions.map((d) => [
|
|
27314
27240
|
d.date,
|
|
27315
27241
|
d.account,
|
|
27316
27242
|
`"${d.description.replace(/"/g, '""')}"`,
|
|
@@ -27328,7 +27254,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27328
27254
|
`) + `
|
|
27329
27255
|
`);
|
|
27330
27256
|
simpleTransactionsCsvPath = filteredCsvPath;
|
|
27331
|
-
logger?.info(`Generated filtered CSV
|
|
27257
|
+
logger?.info(`Generated filtered CSV: ${filteredCsvPath} (${simpleTransactions.length} rows)`);
|
|
27332
27258
|
}
|
|
27333
27259
|
const dividendCount = dividendGroups.length;
|
|
27334
27260
|
return {
|
package/package.json
CHANGED