@fuzzle/opencode-accountant 0.10.5 → 0.10.6-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/README.md +9 -0
- package/agent/accountant.md +9 -0
- package/dist/index.js +110 -33
- package/docs/tools/import-pipeline.md +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -270,6 +270,15 @@ The pipeline automatically manages account declarations:
|
|
|
270
270
|
|
|
271
271
|
**No manual account setup required.** Account declarations are created proactively before import attempts.
|
|
272
272
|
|
|
273
|
+
#### Commodity Declarations
|
|
274
|
+
|
|
275
|
+
For Swissquote investment imports, the pipeline automatically manages commodity declarations:
|
|
276
|
+
|
|
277
|
+
- Collects all stock ticker symbols from trades, dividends, and corporate actions
|
|
278
|
+
- Writes sorted `commodity "SYMBOL"` declarations to `ledger/investments/commodities.journal`
|
|
279
|
+
- Merges with existing declarations (idempotent)
|
|
280
|
+
- Ensures `hledger check --strict` validation passes for quoted commodity symbols
|
|
281
|
+
|
|
273
282
|
#### Unknown Postings
|
|
274
283
|
|
|
275
284
|
When a transaction doesn't match any `if` pattern in the rules file, hledger assigns it to `income:unknown` or `expenses:unknown` depending on the transaction direction. The pipeline will fail at the validation step, reporting the unknown postings so you can add appropriate rules before retrying.
|
package/agent/accountant.md
CHANGED
|
@@ -164,6 +164,15 @@ The import pipeline automatically:
|
|
|
164
164
|
- Prevents `hledger check --strict` failures due to missing account declarations
|
|
165
165
|
- No manual account setup required
|
|
166
166
|
|
|
167
|
+
### Automatic Commodity Declarations
|
|
168
|
+
|
|
169
|
+
For Swissquote investment imports, the pipeline automatically:
|
|
170
|
+
|
|
171
|
+
- Collects all stock ticker symbols from trades, dividends, and corporate actions
|
|
172
|
+
- Writes sorted `commodity "SYMBOL"` declarations to `ledger/investments/commodities.journal`
|
|
173
|
+
- Merges with existing declarations (idempotent)
|
|
174
|
+
- Prevents `hledger check --strict` failures due to missing commodity declarations
|
|
175
|
+
|
|
167
176
|
### Automatic Balance Detection
|
|
168
177
|
|
|
169
178
|
The reconciliation step attempts to extract closing balance from:
|
package/dist/index.js
CHANGED
|
@@ -4533,7 +4533,7 @@ var init_accountSuggester = __esm(() => {
|
|
|
4533
4533
|
|
|
4534
4534
|
// src/index.ts
|
|
4535
4535
|
init_agentLoader();
|
|
4536
|
-
import { dirname as
|
|
4536
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
4537
4537
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4538
4538
|
|
|
4539
4539
|
// node_modules/zod/v4/classic/external.js
|
|
@@ -17051,6 +17051,38 @@ function ensureYearJournalExists(directory, year) {
|
|
|
17051
17051
|
}
|
|
17052
17052
|
return yearJournalPath;
|
|
17053
17053
|
}
|
|
17054
|
+
function ensureCommodityDeclarations(commodityJournalPath, symbols, logger) {
|
|
17055
|
+
const existing = new Set;
|
|
17056
|
+
if (fs4.existsSync(commodityJournalPath)) {
|
|
17057
|
+
const content2 = fs4.readFileSync(commodityJournalPath, "utf-8");
|
|
17058
|
+
for (const line of content2.split(`
|
|
17059
|
+
`)) {
|
|
17060
|
+
const match = line.trim().match(/^commodity\s+"(.+)"/);
|
|
17061
|
+
if (match) {
|
|
17062
|
+
existing.add(match[1]);
|
|
17063
|
+
}
|
|
17064
|
+
}
|
|
17065
|
+
}
|
|
17066
|
+
const missing = [];
|
|
17067
|
+
for (const symbol2 of symbols) {
|
|
17068
|
+
const upper = symbol2.toUpperCase();
|
|
17069
|
+
if (!existing.has(upper)) {
|
|
17070
|
+
missing.push(upper);
|
|
17071
|
+
existing.add(upper);
|
|
17072
|
+
}
|
|
17073
|
+
}
|
|
17074
|
+
if (missing.length === 0) {
|
|
17075
|
+
return { added: [], updated: false };
|
|
17076
|
+
}
|
|
17077
|
+
const sorted = Array.from(existing).sort((a, b) => a.localeCompare(b));
|
|
17078
|
+
const content = sorted.map((s) => `commodity "${s}"`).join(`
|
|
17079
|
+
`) + `
|
|
17080
|
+
`;
|
|
17081
|
+
ensureDirectory(path3.dirname(commodityJournalPath));
|
|
17082
|
+
fs4.writeFileSync(commodityJournalPath, content);
|
|
17083
|
+
logger?.info(`Commodity declarations: added ${missing.length} (${missing.join(", ")})`);
|
|
17084
|
+
return { added: missing.sort(), updated: true };
|
|
17085
|
+
}
|
|
17054
17086
|
|
|
17055
17087
|
// src/utils/dateUtils.ts
|
|
17056
17088
|
function formatDateISO(date5) {
|
|
@@ -17408,16 +17440,22 @@ function parseCSVPreview(content, skipRows = 0, delimiter = ",") {
|
|
|
17408
17440
|
csvContent = lines.slice(skipRows).join(`
|
|
17409
17441
|
`);
|
|
17410
17442
|
}
|
|
17411
|
-
const
|
|
17412
|
-
|
|
17413
|
-
|
|
17414
|
-
|
|
17415
|
-
|
|
17416
|
-
|
|
17417
|
-
|
|
17418
|
-
|
|
17419
|
-
|
|
17420
|
-
|
|
17443
|
+
const origWarn = console.warn;
|
|
17444
|
+
console.warn = () => {};
|
|
17445
|
+
try {
|
|
17446
|
+
const result = import_papaparse.default.parse(csvContent, {
|
|
17447
|
+
header: true,
|
|
17448
|
+
preview: 1,
|
|
17449
|
+
skipEmptyLines: true,
|
|
17450
|
+
delimiter
|
|
17451
|
+
});
|
|
17452
|
+
return {
|
|
17453
|
+
fields: result.meta.fields,
|
|
17454
|
+
firstRow: result.data[0]
|
|
17455
|
+
};
|
|
17456
|
+
} finally {
|
|
17457
|
+
console.warn = origWarn;
|
|
17458
|
+
}
|
|
17421
17459
|
}
|
|
17422
17460
|
function extractLastRowField(content, skipRows, delimiter, fieldName) {
|
|
17423
17461
|
let csvContent = content;
|
|
@@ -17427,11 +17465,18 @@ function extractLastRowField(content, skipRows, delimiter, fieldName) {
|
|
|
17427
17465
|
csvContent = lines.slice(skipRows).join(`
|
|
17428
17466
|
`);
|
|
17429
17467
|
}
|
|
17430
|
-
const
|
|
17431
|
-
|
|
17432
|
-
|
|
17433
|
-
|
|
17434
|
-
|
|
17468
|
+
const origWarn = console.warn;
|
|
17469
|
+
console.warn = () => {};
|
|
17470
|
+
let result;
|
|
17471
|
+
try {
|
|
17472
|
+
result = import_papaparse.default.parse(csvContent, {
|
|
17473
|
+
header: true,
|
|
17474
|
+
skipEmptyLines: true,
|
|
17475
|
+
delimiter
|
|
17476
|
+
});
|
|
17477
|
+
} finally {
|
|
17478
|
+
console.warn = origWarn;
|
|
17479
|
+
}
|
|
17435
17480
|
if (result.data.length === 0)
|
|
17436
17481
|
return;
|
|
17437
17482
|
const lastRow = result.data[result.data.length - 1];
|
|
@@ -25003,23 +25048,32 @@ function formatDate(dateStr) {
|
|
|
25003
25048
|
function escapeDescription(desc) {
|
|
25004
25049
|
return desc.replace(/[;|]/g, "-").trim();
|
|
25005
25050
|
}
|
|
25051
|
+
function formatCommodity(symbol2) {
|
|
25052
|
+
return `"${symbol2.toUpperCase()}"`;
|
|
25053
|
+
}
|
|
25006
25054
|
function formatQuantity(qty) {
|
|
25007
25055
|
return qty.toFixed(6).replace(/\.?0+$/, "");
|
|
25008
25056
|
}
|
|
25057
|
+
function formatPrice(price) {
|
|
25058
|
+
const full = price.toFixed(6).replace(/0+$/, "");
|
|
25059
|
+
const [integer2, decimal = ""] = full.split(".");
|
|
25060
|
+
return `${integer2}.${decimal.padEnd(2, "0")}`;
|
|
25061
|
+
}
|
|
25009
25062
|
function generateBuyEntry(trade, logger) {
|
|
25010
25063
|
const date5 = formatDate(trade.date);
|
|
25011
25064
|
const description = escapeDescription(`Buy ${trade.symbol} - ${trade.name}`);
|
|
25012
25065
|
const qty = formatQuantity(trade.quantity);
|
|
25013
|
-
const price = trade.unitPrice
|
|
25066
|
+
const price = formatPrice(trade.unitPrice);
|
|
25014
25067
|
const totalCost = trade.quantity * trade.unitPrice;
|
|
25015
25068
|
const fees = trade.costs;
|
|
25016
25069
|
const cashOut = totalCost + fees;
|
|
25017
25070
|
logger?.debug(`Generating Buy entry: ${qty} ${trade.symbol} @ ${price} ${trade.currency}`);
|
|
25071
|
+
const commodity = formatCommodity(trade.symbol);
|
|
25018
25072
|
let entry = `${date5} ${description}
|
|
25019
25073
|
`;
|
|
25020
25074
|
entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
|
|
25021
25075
|
`;
|
|
25022
|
-
entry += ` assets:investments:stocks:${trade.symbol} ${qty} @ ${price} ${trade.currency}
|
|
25076
|
+
entry += ` assets:investments:stocks:${trade.symbol} ${qty} ${commodity} @ ${price} ${trade.currency}
|
|
25023
25077
|
`;
|
|
25024
25078
|
if (fees > 0) {
|
|
25025
25079
|
entry += ` expenses:fees:trading:swissquote ${formatAmount2(fees, trade.currency)}
|
|
@@ -25039,17 +25093,18 @@ function generateSellEntry(trade, consumed, logger) {
|
|
|
25039
25093
|
const cashIn = saleProceeds - fees;
|
|
25040
25094
|
const gain = calculateCapitalGain(consumed, salePrice, trade.quantity);
|
|
25041
25095
|
logger?.debug(`Generating Sell entry: ${qty} ${trade.symbol} @ ${salePrice} ${trade.currency}, gain: ${gain.toFixed(2)}`);
|
|
25096
|
+
const commodity = formatCommodity(trade.symbol);
|
|
25042
25097
|
let entry = `${date5} ${description}
|
|
25043
25098
|
`;
|
|
25044
25099
|
entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
|
|
25045
25100
|
`;
|
|
25046
|
-
const lotDetails = consumed.map((c) => `${c.lot.date}: ${formatQuantity(c.quantity)}@${c.lot.costBasis
|
|
25101
|
+
const lotDetails = consumed.map((c) => `${c.lot.date}: ${formatQuantity(c.quantity)}@${formatPrice(c.lot.costBasis)}`).join(", ");
|
|
25047
25102
|
entry += ` ; FIFO lots: ${lotDetails}
|
|
25048
25103
|
`;
|
|
25049
25104
|
for (const c of consumed) {
|
|
25050
25105
|
const lotQty = formatQuantity(c.quantity);
|
|
25051
|
-
const lotPrice = c.lot.costBasis
|
|
25052
|
-
entry += ` assets:investments:stocks:${trade.symbol} -${lotQty} @ ${lotPrice} ${trade.currency}
|
|
25106
|
+
const lotPrice = formatPrice(c.lot.costBasis);
|
|
25107
|
+
entry += ` assets:investments:stocks:${trade.symbol} -${lotQty} ${commodity} @ ${lotPrice} ${trade.currency}
|
|
25053
25108
|
`;
|
|
25054
25109
|
}
|
|
25055
25110
|
entry += ` assets:broker:swissquote:${trade.currency.toLowerCase()} ${formatAmount2(cashIn, trade.currency)}
|
|
@@ -25097,13 +25152,14 @@ function generateSplitEntry(action, oldQuantity, newQuantity, logger) {
|
|
|
25097
25152
|
`;
|
|
25098
25153
|
entry += ` ; Ratio: ${ratio.toFixed(4)} (${oldQuantity} -> ${newQuantity})
|
|
25099
25154
|
`;
|
|
25100
|
-
|
|
25155
|
+
const commodity = formatCommodity(action.symbol);
|
|
25156
|
+
entry += ` assets:investments:stocks:${action.symbol} -${formatQuantity(oldQuantity)} ${commodity}
|
|
25101
25157
|
`;
|
|
25102
|
-
entry += ` equity:conversion ${formatQuantity(oldQuantity)}
|
|
25158
|
+
entry += ` equity:conversion ${formatQuantity(oldQuantity)} ${commodity}
|
|
25103
25159
|
`;
|
|
25104
|
-
entry += ` equity:conversion -${formatQuantity(newQuantity)}
|
|
25160
|
+
entry += ` equity:conversion -${formatQuantity(newQuantity)} ${commodity}
|
|
25105
25161
|
`;
|
|
25106
|
-
entry += ` assets:investments:stocks:${action.symbol} ${formatQuantity(newQuantity)}
|
|
25162
|
+
entry += ` assets:investments:stocks:${action.symbol} ${formatQuantity(newQuantity)} ${commodity}
|
|
25107
25163
|
`;
|
|
25108
25164
|
return entry;
|
|
25109
25165
|
}
|
|
@@ -25120,10 +25176,11 @@ function generateWorthlessEntry(action, removedLots, logger) {
|
|
|
25120
25176
|
`;
|
|
25121
25177
|
entry += ` ; Total loss: ${totalCost.toFixed(2)} ${currency}
|
|
25122
25178
|
`;
|
|
25179
|
+
const commodity = formatCommodity(action.symbol);
|
|
25123
25180
|
for (const lot of removedLots) {
|
|
25124
25181
|
const qty = formatQuantity(lot.quantity);
|
|
25125
|
-
const price = lot.costBasis
|
|
25126
|
-
entry += ` assets:investments:stocks:${action.symbol} -${qty} @ ${price} ${currency}
|
|
25182
|
+
const price = formatPrice(lot.costBasis);
|
|
25183
|
+
entry += ` assets:investments:stocks:${action.symbol} -${qty} ${commodity} @ ${price} ${currency}
|
|
25127
25184
|
`;
|
|
25128
25185
|
}
|
|
25129
25186
|
entry += ` expenses:losses:capital ${formatAmount2(totalCost, currency)}
|
|
@@ -25148,16 +25205,18 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, logger
|
|
|
25148
25205
|
}
|
|
25149
25206
|
for (const out of group.outgoing) {
|
|
25150
25207
|
const qty = formatQuantity(Math.abs(out.quantity));
|
|
25151
|
-
|
|
25208
|
+
const commodity = formatCommodity(out.symbol);
|
|
25209
|
+
entry += ` assets:investments:stocks:${out.symbol} -${qty} ${commodity}
|
|
25152
25210
|
`;
|
|
25153
|
-
entry += ` equity:conversion ${qty}
|
|
25211
|
+
entry += ` equity:conversion ${qty} ${commodity}
|
|
25154
25212
|
`;
|
|
25155
25213
|
}
|
|
25156
25214
|
for (const inc of group.incoming) {
|
|
25157
25215
|
const qty = formatQuantity(Math.abs(inc.quantity));
|
|
25158
|
-
|
|
25216
|
+
const commodity = formatCommodity(inc.symbol);
|
|
25217
|
+
entry += ` equity:conversion -${qty} ${commodity}
|
|
25159
25218
|
`;
|
|
25160
|
-
entry += ` assets:investments:stocks:${inc.symbol} ${qty}
|
|
25219
|
+
entry += ` assets:investments:stocks:${inc.symbol} ${qty} ${commodity}
|
|
25161
25220
|
`;
|
|
25162
25221
|
}
|
|
25163
25222
|
return entry;
|
|
@@ -25167,11 +25226,12 @@ function generateRightsDistributionEntry(action, logger) {
|
|
|
25167
25226
|
const qty = formatQuantity(Math.abs(action.quantity));
|
|
25168
25227
|
const description = escapeDescription(`Rights Distribution: ${action.symbol} - ${action.name}`);
|
|
25169
25228
|
logger?.debug(`Generating Rights Distribution entry: ${qty} ${action.symbol}`);
|
|
25229
|
+
const commodity = formatCommodity(action.symbol);
|
|
25170
25230
|
let entry = `${date5} ${description}
|
|
25171
25231
|
`;
|
|
25172
25232
|
entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
|
|
25173
25233
|
`;
|
|
25174
|
-
entry += ` assets:investments:stocks:${action.symbol} ${qty} @ 0.00 CAD
|
|
25234
|
+
entry += ` assets:investments:stocks:${action.symbol} ${qty} ${commodity} @ 0.00 CAD
|
|
25175
25235
|
`;
|
|
25176
25236
|
entry += ` income:capital-gains:rights-distribution 0.00 CAD
|
|
25177
25237
|
`;
|
|
@@ -25739,6 +25799,23 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
|
|
|
25739
25799
|
logger?.logStep("write-journal", "success", `Created ${path13.basename(journalFile)} with ${journalEntries.length} entries`);
|
|
25740
25800
|
}
|
|
25741
25801
|
}
|
|
25802
|
+
const stockSymbols = new Set;
|
|
25803
|
+
for (const trade of trades) {
|
|
25804
|
+
stockSymbols.add(trade.symbol);
|
|
25805
|
+
}
|
|
25806
|
+
for (const dividend of dividends) {
|
|
25807
|
+
stockSymbols.add(dividend.symbol);
|
|
25808
|
+
}
|
|
25809
|
+
for (const action of corporateActions) {
|
|
25810
|
+
stockSymbols.add(action.symbol);
|
|
25811
|
+
if (action.newSymbol) {
|
|
25812
|
+
stockSymbols.add(action.newSymbol);
|
|
25813
|
+
}
|
|
25814
|
+
}
|
|
25815
|
+
if (stockSymbols.size > 0) {
|
|
25816
|
+
const commodityJournalPath = path13.join(projectDir, "ledger", "investments", "commodities.journal");
|
|
25817
|
+
ensureCommodityDeclarations(commodityJournalPath, stockSymbols, logger);
|
|
25818
|
+
}
|
|
25742
25819
|
logger?.logResult({
|
|
25743
25820
|
totalRows: stats.totalRows,
|
|
25744
25821
|
simpleTransactions: stats.simpleTransactions,
|
|
@@ -26667,7 +26744,7 @@ to produce equity conversion entries for Bitcoin purchases.
|
|
|
26667
26744
|
}
|
|
26668
26745
|
});
|
|
26669
26746
|
// src/index.ts
|
|
26670
|
-
var __dirname2 =
|
|
26747
|
+
var __dirname2 = dirname7(fileURLToPath3(import.meta.url));
|
|
26671
26748
|
var AGENT_FILE = join15(__dirname2, "..", "agent", "accountant.md");
|
|
26672
26749
|
var AccountantPlugin = async () => {
|
|
26673
26750
|
const agent = loadAgent(AGENT_FILE);
|
|
@@ -220,7 +220,8 @@ See [classify-statements](classify-statements.md) for details.
|
|
|
220
220
|
- **Rights Distributions**: adds shares at zero cost basis
|
|
221
221
|
- **Cross-currency mergers**: saves outgoing side to pending state, loads when incoming side is processed
|
|
222
222
|
8. Generates per-year investment journal entries (`ledger/investments/{year}-{currency}.journal`) based on the CSV filename's date range
|
|
223
|
-
9.
|
|
223
|
+
9. Collects all stock ticker symbols and writes commodity declarations to `ledger/investments/commodities.journal` (merged with existing, sorted, idempotent)
|
|
224
|
+
10. Outputs filtered CSV (simple transactions only) for hledger rules import
|
|
224
225
|
|
|
225
226
|
**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
|
|