@fuzzle/opencode-accountant 0.16.2 → 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 +108 -133
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16987,6 +16987,7 @@ function loadPricesConfig(directory) {
|
|
|
16987
16987
|
}
|
|
16988
16988
|
|
|
16989
16989
|
// src/utils/journalUtils.ts
|
|
16990
|
+
init_js_yaml();
|
|
16990
16991
|
import * as fs4 from "fs";
|
|
16991
16992
|
import * as path3 from "path";
|
|
16992
16993
|
|
|
@@ -17178,6 +17179,50 @@ function ensureInvestmentAccountDeclarations(accountJournalPath, accounts, logge
|
|
|
17178
17179
|
logger?.info(`Account declarations: added ${missing.length} (${missing.join(", ")})`);
|
|
17179
17180
|
return { added: missing.sort(), updated: true };
|
|
17180
17181
|
}
|
|
17182
|
+
function ensureStockPriceEntries(directory, symbols, logger) {
|
|
17183
|
+
const pricesPath = path3.join(directory, "config", "prices.yaml");
|
|
17184
|
+
if (!fs4.existsSync(pricesPath)) {
|
|
17185
|
+
return { added: [], updated: false };
|
|
17186
|
+
}
|
|
17187
|
+
const content = fs4.readFileSync(pricesPath, "utf-8");
|
|
17188
|
+
const parsed = jsYaml.load(content);
|
|
17189
|
+
const existingStocks = new Set;
|
|
17190
|
+
if (parsed?.stocks && typeof parsed.stocks === "object" && !Array.isArray(parsed.stocks)) {
|
|
17191
|
+
for (const key of Object.keys(parsed.stocks)) {
|
|
17192
|
+
existingStocks.add(key);
|
|
17193
|
+
}
|
|
17194
|
+
}
|
|
17195
|
+
const missing = [];
|
|
17196
|
+
for (const symbol2 of symbols) {
|
|
17197
|
+
const ticker = symbol2.toUpperCase();
|
|
17198
|
+
if (!existingStocks.has(ticker)) {
|
|
17199
|
+
missing.push(ticker);
|
|
17200
|
+
}
|
|
17201
|
+
}
|
|
17202
|
+
if (missing.length === 0) {
|
|
17203
|
+
return { added: [], updated: false };
|
|
17204
|
+
}
|
|
17205
|
+
let updatedContent = content;
|
|
17206
|
+
if (!parsed?.stocks) {
|
|
17207
|
+
updatedContent = updatedContent.trimEnd() + `
|
|
17208
|
+
|
|
17209
|
+
stocks:
|
|
17210
|
+
`;
|
|
17211
|
+
}
|
|
17212
|
+
if (!updatedContent.endsWith(`
|
|
17213
|
+
`)) {
|
|
17214
|
+
updatedContent += `
|
|
17215
|
+
`;
|
|
17216
|
+
}
|
|
17217
|
+
const sorted = missing.sort();
|
|
17218
|
+
for (const ticker of sorted) {
|
|
17219
|
+
updatedContent += ` ${ticker}:
|
|
17220
|
+
`;
|
|
17221
|
+
}
|
|
17222
|
+
fs4.writeFileSync(pricesPath, updatedContent);
|
|
17223
|
+
logger?.info(`Price config: added ${sorted.length} stock(s) (${sorted.join(", ")})`);
|
|
17224
|
+
return { added: sorted, updated: true };
|
|
17225
|
+
}
|
|
17181
17226
|
|
|
17182
17227
|
// src/utils/dateUtils.ts
|
|
17183
17228
|
function formatDateISO(date5) {
|
|
@@ -17506,7 +17551,9 @@ function validateProviderConfig(name, config2) {
|
|
|
17506
17551
|
return {
|
|
17507
17552
|
detect,
|
|
17508
17553
|
currencies,
|
|
17509
|
-
importOrder: configObj.importOrder
|
|
17554
|
+
importOrder: configObj.importOrder,
|
|
17555
|
+
lotInventoryPath: typeof configObj.lotInventoryPath === "string" ? configObj.lotInventoryPath : undefined,
|
|
17556
|
+
symbolMapPath: typeof configObj.symbolMapPath === "string" ? configObj.symbolMapPath : undefined
|
|
17510
17557
|
};
|
|
17511
17558
|
}
|
|
17512
17559
|
function loadImportConfig(directory) {
|
|
@@ -26428,6 +26475,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
|
|
|
26428
26475
|
investmentAccounts.add(`equity:conversion:${currency.toLowerCase()}`);
|
|
26429
26476
|
investmentAccounts.add("equity:rounding");
|
|
26430
26477
|
ensureInvestmentAccountDeclarations(accountJournalPath, investmentAccounts, logger);
|
|
26478
|
+
ensureStockPriceEntries(projectDir, stockSymbols, logger);
|
|
26431
26479
|
}
|
|
26432
26480
|
logger?.logResult({
|
|
26433
26481
|
totalRows: stats.totalRows,
|
|
@@ -26941,62 +26989,6 @@ function generateIbkrDividendEntry(dividend, logger) {
|
|
|
26941
26989
|
`;
|
|
26942
26990
|
return entry;
|
|
26943
26991
|
}
|
|
26944
|
-
function generateIbkrAdjustmentEntry(adjustment, logger) {
|
|
26945
|
-
const description = escapeDescription(adjustment.description);
|
|
26946
|
-
logger?.debug(`Generating IBKR Adjustment entry: ${adjustment.amount} ${adjustment.currency}`);
|
|
26947
|
-
const postings = [
|
|
26948
|
-
{
|
|
26949
|
-
account: `assets:broker:ibkr:${adjustment.currency.toLowerCase()}`,
|
|
26950
|
-
amount: formatAmount(adjustment.amount, adjustment.currency)
|
|
26951
|
-
}
|
|
26952
|
-
];
|
|
26953
|
-
if (adjustment.amount < 0) {
|
|
26954
|
-
postings.push({
|
|
26955
|
-
account: "expenses:fx-losses:ibkr",
|
|
26956
|
-
amount: formatAmount(Math.abs(adjustment.amount), adjustment.currency)
|
|
26957
|
-
});
|
|
26958
|
-
} else {
|
|
26959
|
-
postings.push({
|
|
26960
|
-
account: "income:fx-gains:ibkr",
|
|
26961
|
-
amount: formatAmount(-adjustment.amount, adjustment.currency)
|
|
26962
|
-
});
|
|
26963
|
-
}
|
|
26964
|
-
let entry = `${adjustment.date} ${description}
|
|
26965
|
-
`;
|
|
26966
|
-
entry += ` ; ibkr:account:${adjustment.account}
|
|
26967
|
-
`;
|
|
26968
|
-
entry += formatPostings(postings) + `
|
|
26969
|
-
`;
|
|
26970
|
-
return entry;
|
|
26971
|
-
}
|
|
26972
|
-
function generateIbkrForexEntry(entry, logger) {
|
|
26973
|
-
const description = escapeDescription(entry.description);
|
|
26974
|
-
logger?.debug(`Generating IBKR Forex entry: ${entry.amount} ${entry.currency}`);
|
|
26975
|
-
const postings = [
|
|
26976
|
-
{
|
|
26977
|
-
account: `assets:broker:ibkr:${entry.currency.toLowerCase()}`,
|
|
26978
|
-
amount: formatAmount(entry.amount, entry.currency)
|
|
26979
|
-
}
|
|
26980
|
-
];
|
|
26981
|
-
if (entry.amount < 0) {
|
|
26982
|
-
postings.push({
|
|
26983
|
-
account: "expenses:fees:forex:ibkr",
|
|
26984
|
-
amount: formatAmount(Math.abs(entry.amount), entry.currency)
|
|
26985
|
-
});
|
|
26986
|
-
} else {
|
|
26987
|
-
postings.push({
|
|
26988
|
-
account: "income:fx-gains:ibkr",
|
|
26989
|
-
amount: formatAmount(-entry.amount, entry.currency)
|
|
26990
|
-
});
|
|
26991
|
-
}
|
|
26992
|
-
let result = `${entry.date} ${description}
|
|
26993
|
-
`;
|
|
26994
|
-
result += ` ; ibkr:account:${entry.account}
|
|
26995
|
-
`;
|
|
26996
|
-
result += formatPostings(postings) + `
|
|
26997
|
-
`;
|
|
26998
|
-
return result;
|
|
26999
|
-
}
|
|
27000
26992
|
|
|
27001
26993
|
// src/utils/ibkrCsvPreprocessor.ts
|
|
27002
26994
|
var TRADE_TYPES2 = new Set(["Buy", "Sell"]);
|
|
@@ -27020,7 +27012,7 @@ function parseIbkrCsv(content) {
|
|
|
27020
27012
|
const account = values[3]?.trim() || "";
|
|
27021
27013
|
const description = values[4]?.trim() || "";
|
|
27022
27014
|
const transactionType = values[5]?.trim() || "";
|
|
27023
|
-
const symbol2 = values[6]?.trim() || "";
|
|
27015
|
+
const symbol2 = normalizeSymbol(values[6]?.trim() || "");
|
|
27024
27016
|
const quantity = parseFloat(values[7] || "0") || 0;
|
|
27025
27017
|
const price = parseFloat(values[8] || "0") || 0;
|
|
27026
27018
|
const priceCurrency = values[9]?.trim() || "";
|
|
@@ -27078,25 +27070,34 @@ function groupDividends(transactions) {
|
|
|
27078
27070
|
groups.sort((a, b) => a.date.localeCompare(b.date));
|
|
27079
27071
|
return groups;
|
|
27080
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
|
+
}
|
|
27081
27093
|
async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPath, logger) {
|
|
27082
27094
|
const csvDir = path17.dirname(csvPath);
|
|
27083
27095
|
const csvBasename = path17.basename(csvPath, ".csv");
|
|
27084
27096
|
const journalPath = path17.join(directory, "ledger", "investments", `${year}-ibkr-${currency}.journal`);
|
|
27097
|
+
let existingJournalContent = "";
|
|
27085
27098
|
if (fs22.existsSync(journalPath)) {
|
|
27086
|
-
|
|
27087
|
-
|
|
27088
|
-
simpleTransactionsCsv: null,
|
|
27089
|
-
journalFile: journalPath,
|
|
27090
|
-
stats: {
|
|
27091
|
-
totalRows: 0,
|
|
27092
|
-
deposits: 0,
|
|
27093
|
-
trades: 0,
|
|
27094
|
-
dividends: 0,
|
|
27095
|
-
adjustments: 0,
|
|
27096
|
-
forex: 0
|
|
27097
|
-
},
|
|
27098
|
-
alreadyPreprocessed: true
|
|
27099
|
-
};
|
|
27099
|
+
existingJournalContent = fs22.readFileSync(journalPath, "utf-8");
|
|
27100
|
+
logger?.info(`Found existing IBKR journal: ${journalPath}, will check for new entries`);
|
|
27100
27101
|
}
|
|
27101
27102
|
const content = fs22.readFileSync(csvPath, "utf-8");
|
|
27102
27103
|
const transactions = parseIbkrCsv(content);
|
|
@@ -27127,7 +27128,6 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27127
27128
|
const usedAccounts = new Set;
|
|
27128
27129
|
const journalEntries = [];
|
|
27129
27130
|
for (const trade of trades) {
|
|
27130
|
-
tradedSymbols.add(trade.symbol);
|
|
27131
27131
|
const totalCost = Math.abs(trade.grossAmount);
|
|
27132
27132
|
const unitPrice = trade.quantity !== 0 ? totalCost / Math.abs(trade.quantity) : 0;
|
|
27133
27133
|
const tradeEntry = {
|
|
@@ -27143,6 +27143,11 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27143
27143
|
currency: currency.toUpperCase()
|
|
27144
27144
|
};
|
|
27145
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
|
+
}
|
|
27146
27151
|
const tradeInfo = {
|
|
27147
27152
|
date: trade.date,
|
|
27148
27153
|
orderNum: "",
|
|
@@ -27153,13 +27158,19 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27153
27158
|
currency: currency.toUpperCase()
|
|
27154
27159
|
};
|
|
27155
27160
|
addLot(inventory, tradeInfo, logger);
|
|
27156
|
-
journalEntries.push(
|
|
27161
|
+
journalEntries.push(entry);
|
|
27162
|
+
tradedSymbols.add(trade.symbol);
|
|
27157
27163
|
usedAccounts.add(`assets:investments:stocks:${trade.symbol}`);
|
|
27158
27164
|
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27159
27165
|
usedAccounts.add("expenses:fees:trading:ibkr");
|
|
27160
27166
|
} else {
|
|
27167
|
+
if (existingJournalContent.includes(`${trade.date} Sell ${trade.symbol} - `)) {
|
|
27168
|
+
logger?.debug(`Skipping duplicate Sell: ${trade.date} ${trade.symbol}`);
|
|
27169
|
+
continue;
|
|
27170
|
+
}
|
|
27161
27171
|
const consumed = consumeLotsFIFO(inventory, trade.symbol, Math.abs(trade.quantity), logger);
|
|
27162
27172
|
journalEntries.push(generateIbkrSellEntry(tradeEntry, consumed, logger));
|
|
27173
|
+
tradedSymbols.add(trade.symbol);
|
|
27163
27174
|
usedAccounts.add(`assets:investments:stocks:${trade.symbol}`);
|
|
27164
27175
|
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27165
27176
|
usedAccounts.add("expenses:fees:trading:ibkr");
|
|
@@ -27178,74 +27189,36 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27178
27189
|
netAmount,
|
|
27179
27190
|
currency: currency.toUpperCase()
|
|
27180
27191
|
};
|
|
27181
|
-
|
|
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);
|
|
27182
27198
|
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27183
27199
|
usedAccounts.add(`income:dividends:${group.symbol}`);
|
|
27184
27200
|
if (group.withholdingTax > 0) {
|
|
27185
27201
|
usedAccounts.add("expenses:taxes:withholding");
|
|
27186
27202
|
}
|
|
27187
27203
|
}
|
|
27188
|
-
adjustments.sort((a, b) => a.date.localeCompare(b.date));
|
|
27189
|
-
for (const adj of adjustments) {
|
|
27190
|
-
const adjustmentEntry = {
|
|
27191
|
-
date: adj.date,
|
|
27192
|
-
account: adj.account,
|
|
27193
|
-
description: adj.description || "FX Translations P&L",
|
|
27194
|
-
amount: adj.netAmount,
|
|
27195
|
-
currency: currency.toUpperCase()
|
|
27196
|
-
};
|
|
27197
|
-
journalEntries.push(generateIbkrAdjustmentEntry(adjustmentEntry, logger));
|
|
27198
|
-
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27199
|
-
if (adj.netAmount < 0) {
|
|
27200
|
-
usedAccounts.add("expenses:fx-losses:ibkr");
|
|
27201
|
-
} else {
|
|
27202
|
-
usedAccounts.add("income:fx-gains:ibkr");
|
|
27203
|
-
}
|
|
27204
|
-
}
|
|
27205
|
-
if (forexTxns.length > 0) {
|
|
27206
|
-
const forexByDate = new Map;
|
|
27207
|
-
for (const txn of forexTxns) {
|
|
27208
|
-
const existing = forexByDate.get(txn.date);
|
|
27209
|
-
if (existing) {
|
|
27210
|
-
existing.total += txn.netAmount;
|
|
27211
|
-
} else {
|
|
27212
|
-
forexByDate.set(txn.date, { account: txn.account, total: txn.netAmount });
|
|
27213
|
-
}
|
|
27214
|
-
}
|
|
27215
|
-
const sortedDates = [...forexByDate.keys()].sort();
|
|
27216
|
-
for (const date5 of sortedDates) {
|
|
27217
|
-
const { account, total } = forexByDate.get(date5);
|
|
27218
|
-
const amount = Math.round(total * 100) / 100;
|
|
27219
|
-
if (amount === 0)
|
|
27220
|
-
continue;
|
|
27221
|
-
const forexEntry = {
|
|
27222
|
-
date: date5,
|
|
27223
|
-
account,
|
|
27224
|
-
description: "Forex conversion cost",
|
|
27225
|
-
amount,
|
|
27226
|
-
currency: currency.toUpperCase()
|
|
27227
|
-
};
|
|
27228
|
-
journalEntries.push(generateIbkrForexEntry(forexEntry, logger));
|
|
27229
|
-
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27230
|
-
if (amount < 0) {
|
|
27231
|
-
usedAccounts.add("expenses:fees:forex:ibkr");
|
|
27232
|
-
} else {
|
|
27233
|
-
usedAccounts.add("income:fx-gains:ibkr");
|
|
27234
|
-
}
|
|
27235
|
-
}
|
|
27236
|
-
}
|
|
27237
27204
|
saveLotInventory(directory, lotInventoryPath, inventory, logger);
|
|
27238
|
-
let journalFilePath = null;
|
|
27205
|
+
let journalFilePath = existingJournalContent ? journalPath : null;
|
|
27239
27206
|
if (journalEntries.length > 0) {
|
|
27240
|
-
const header = `; IBKR ${currency.toUpperCase()} investment transactions for ${year}
|
|
27241
|
-
; Generated by opencode-accountant
|
|
27242
|
-
; This file is auto-generated - do not edit manually`;
|
|
27243
|
-
const journalContent = formatJournalFile(journalEntries, header);
|
|
27244
27207
|
const investmentsDir = path17.join(directory, "ledger", "investments");
|
|
27245
27208
|
if (!fs22.existsSync(investmentsDir)) {
|
|
27246
27209
|
fs22.mkdirSync(investmentsDir, { recursive: true });
|
|
27247
27210
|
}
|
|
27248
|
-
|
|
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
|
+
}
|
|
27249
27222
|
journalFilePath = journalPath;
|
|
27250
27223
|
if (tradedSymbols.size > 0) {
|
|
27251
27224
|
const commodityJournalPath = path17.join(directory, "ledger", "investments", "commodities.journal");
|
|
@@ -27255,13 +27228,15 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27255
27228
|
const accountJournalPath = path17.join(directory, "ledger", "investments", "accounts.journal");
|
|
27256
27229
|
ensureInvestmentAccountDeclarations(accountJournalPath, usedAccounts, logger);
|
|
27257
27230
|
}
|
|
27231
|
+
ensureStockPriceEntries(directory, tradedSymbols, logger);
|
|
27258
27232
|
logger?.info(`Generated IBKR journal: ${journalPath} with ${journalEntries.length} entries`);
|
|
27259
27233
|
}
|
|
27234
|
+
const simpleTransactions = [...deposits, ...adjustments, ...forexTxns];
|
|
27260
27235
|
let simpleTransactionsCsvPath = null;
|
|
27261
|
-
if (
|
|
27236
|
+
if (simpleTransactions.length > 0) {
|
|
27262
27237
|
const filteredCsvPath = path17.join(csvDir, `${csvBasename}-filtered.csv`);
|
|
27263
27238
|
const csvHeader = "Date,Account,Description,Transaction Type,Symbol,Quantity,Price,Price Currency,Gross Amount,Commission,Net Amount";
|
|
27264
|
-
const csvRows =
|
|
27239
|
+
const csvRows = simpleTransactions.map((d) => [
|
|
27265
27240
|
d.date,
|
|
27266
27241
|
d.account,
|
|
27267
27242
|
`"${d.description.replace(/"/g, '""')}"`,
|
|
@@ -27279,7 +27254,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27279
27254
|
`) + `
|
|
27280
27255
|
`);
|
|
27281
27256
|
simpleTransactionsCsvPath = filteredCsvPath;
|
|
27282
|
-
logger?.info(`Generated filtered CSV
|
|
27257
|
+
logger?.info(`Generated filtered CSV: ${filteredCsvPath} (${simpleTransactions.length} rows)`);
|
|
27283
27258
|
}
|
|
27284
27259
|
const dividendCount = dividendGroups.length;
|
|
27285
27260
|
return {
|