@fuzzle/opencode-accountant 0.16.1 → 0.16.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 +121 -11
- 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) {
|
|
@@ -23515,7 +23562,8 @@ function balancesMatch(balance1, balance2) {
|
|
|
23515
23562
|
return false;
|
|
23516
23563
|
}
|
|
23517
23564
|
validateCurrencies(parsed1, parsed2);
|
|
23518
|
-
|
|
23565
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
23566
|
+
return round2(parsed1.amount) === round2(parsed2.amount);
|
|
23519
23567
|
}
|
|
23520
23568
|
|
|
23521
23569
|
// src/utils/csvParser.ts
|
|
@@ -26427,6 +26475,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
|
|
|
26427
26475
|
investmentAccounts.add(`equity:conversion:${currency.toLowerCase()}`);
|
|
26428
26476
|
investmentAccounts.add("equity:rounding");
|
|
26429
26477
|
ensureInvestmentAccountDeclarations(accountJournalPath, investmentAccounts, logger);
|
|
26478
|
+
ensureStockPriceEntries(projectDir, stockSymbols, logger);
|
|
26430
26479
|
}
|
|
26431
26480
|
logger?.logResult({
|
|
26432
26481
|
totalRows: stats.totalRows,
|
|
@@ -26968,6 +27017,34 @@ function generateIbkrAdjustmentEntry(adjustment, logger) {
|
|
|
26968
27017
|
`;
|
|
26969
27018
|
return entry;
|
|
26970
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
|
+
}
|
|
26971
27048
|
|
|
26972
27049
|
// src/utils/ibkrCsvPreprocessor.ts
|
|
26973
27050
|
var TRADE_TYPES2 = new Set(["Buy", "Sell"]);
|
|
@@ -26991,7 +27068,7 @@ function parseIbkrCsv(content) {
|
|
|
26991
27068
|
const account = values[3]?.trim() || "";
|
|
26992
27069
|
const description = values[4]?.trim() || "";
|
|
26993
27070
|
const transactionType = values[5]?.trim() || "";
|
|
26994
|
-
const symbol2 = values[6]?.trim() || "";
|
|
27071
|
+
const symbol2 = normalizeSymbol(values[6]?.trim() || "");
|
|
26995
27072
|
const quantity = parseFloat(values[7] || "0") || 0;
|
|
26996
27073
|
const price = parseFloat(values[8] || "0") || 0;
|
|
26997
27074
|
const priceCurrency = values[9]?.trim() || "";
|
|
@@ -27064,7 +27141,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27064
27141
|
trades: 0,
|
|
27065
27142
|
dividends: 0,
|
|
27066
27143
|
adjustments: 0,
|
|
27067
|
-
|
|
27144
|
+
forex: 0
|
|
27068
27145
|
},
|
|
27069
27146
|
alreadyPreprocessed: true
|
|
27070
27147
|
};
|
|
@@ -27076,7 +27153,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27076
27153
|
const trades = [];
|
|
27077
27154
|
const dividendTxns = [];
|
|
27078
27155
|
const adjustments = [];
|
|
27079
|
-
|
|
27156
|
+
const forexTxns = [];
|
|
27080
27157
|
for (const txn of transactions) {
|
|
27081
27158
|
if (TRADE_TYPES2.has(txn.transactionType)) {
|
|
27082
27159
|
trades.push(txn);
|
|
@@ -27087,7 +27164,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27087
27164
|
} else if (txn.transactionType === DEPOSIT_TYPE) {
|
|
27088
27165
|
deposits.push(txn);
|
|
27089
27166
|
} else if (txn.transactionType === FOREX_TYPE) {
|
|
27090
|
-
|
|
27167
|
+
forexTxns.push(txn);
|
|
27091
27168
|
} else {
|
|
27092
27169
|
logger?.warn(`Unknown IBKR transaction type: ${txn.transactionType}`);
|
|
27093
27170
|
}
|
|
@@ -27173,6 +27250,38 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27173
27250
|
usedAccounts.add("income:fx-gains:ibkr");
|
|
27174
27251
|
}
|
|
27175
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
|
+
}
|
|
27176
27285
|
saveLotInventory(directory, lotInventoryPath, inventory, logger);
|
|
27177
27286
|
let journalFilePath = null;
|
|
27178
27287
|
if (journalEntries.length > 0) {
|
|
@@ -27194,6 +27303,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27194
27303
|
const accountJournalPath = path17.join(directory, "ledger", "investments", "accounts.journal");
|
|
27195
27304
|
ensureInvestmentAccountDeclarations(accountJournalPath, usedAccounts, logger);
|
|
27196
27305
|
}
|
|
27306
|
+
ensureStockPriceEntries(directory, tradedSymbols, logger);
|
|
27197
27307
|
logger?.info(`Generated IBKR journal: ${journalPath} with ${journalEntries.length} entries`);
|
|
27198
27308
|
}
|
|
27199
27309
|
let simpleTransactionsCsvPath = null;
|
|
@@ -27230,7 +27340,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
|
|
|
27230
27340
|
trades: trades.length,
|
|
27231
27341
|
dividends: dividendCount,
|
|
27232
27342
|
adjustments: adjustments.length,
|
|
27233
|
-
|
|
27343
|
+
forex: forexTxns.length
|
|
27234
27344
|
},
|
|
27235
27345
|
alreadyPreprocessed: false
|
|
27236
27346
|
};
|
|
@@ -27609,7 +27719,7 @@ async function executeIbkrPreprocessStep(context, contextIds, logger) {
|
|
|
27609
27719
|
trades: 0,
|
|
27610
27720
|
dividends: 0,
|
|
27611
27721
|
adjustments: 0,
|
|
27612
|
-
|
|
27722
|
+
forex: 0
|
|
27613
27723
|
};
|
|
27614
27724
|
let lastJournalFile = null;
|
|
27615
27725
|
ibkrContexts.sort((a, b) => {
|
|
@@ -27625,7 +27735,7 @@ async function executeIbkrPreprocessStep(context, contextIds, logger) {
|
|
|
27625
27735
|
totalStats.trades += result.stats.trades;
|
|
27626
27736
|
totalStats.dividends += result.stats.dividends;
|
|
27627
27737
|
totalStats.adjustments += result.stats.adjustments;
|
|
27628
|
-
totalStats.
|
|
27738
|
+
totalStats.forex += result.stats.forex;
|
|
27629
27739
|
if (result.journalFile) {
|
|
27630
27740
|
lastJournalFile = result.journalFile;
|
|
27631
27741
|
}
|
|
@@ -27635,9 +27745,9 @@ async function executeIbkrPreprocessStep(context, contextIds, logger) {
|
|
|
27635
27745
|
});
|
|
27636
27746
|
logger?.info(`Updated context ${ibkrCtx.contextId} to use filtered CSV: ${path18.basename(result.simpleTransactionsCsv)}`);
|
|
27637
27747
|
}
|
|
27638
|
-
logger?.logStep("IBKR Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.adjustments} adjustments, ${result.stats.
|
|
27748
|
+
logger?.logStep("IBKR Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.adjustments} adjustments, ${result.stats.forex} forex`);
|
|
27639
27749
|
}
|
|
27640
|
-
const message = `Preprocessed ${totalStats.totalRows} rows: ${totalStats.trades} trades, ${totalStats.dividends} dividends, ${totalStats.adjustments} adjustments, ${totalStats.deposits} deposits, ${totalStats.
|
|
27750
|
+
const message = `Preprocessed ${totalStats.totalRows} rows: ${totalStats.trades} trades, ${totalStats.dividends} dividends, ${totalStats.adjustments} adjustments, ${totalStats.deposits} deposits, ${totalStats.forex} forex`;
|
|
27641
27751
|
context.result.steps.ibkrPreprocess = buildStepResult(true, message, {
|
|
27642
27752
|
...totalStats,
|
|
27643
27753
|
journalFile: lastJournalFile
|