@fuzzle/opencode-accountant 0.10.6 → 0.10.7-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 +101 -27
- 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
|
@@ -4258,6 +4258,19 @@ var require_brace_expansion = __commonJS((exports, module) => {
|
|
|
4258
4258
|
}
|
|
4259
4259
|
});
|
|
4260
4260
|
|
|
4261
|
+
// src/utils/journalMatchers.ts
|
|
4262
|
+
function extractAccount(line, pattern) {
|
|
4263
|
+
const match2 = line.match(pattern);
|
|
4264
|
+
return match2 ? match2[1].trim() : null;
|
|
4265
|
+
}
|
|
4266
|
+
var JOURNAL_ACCOUNT_DECL, RULES_ACCOUNT_DIRECTIVE, RULES_ACCOUNT2_DIRECTIVE, TX_HEADER_PATTERN;
|
|
4267
|
+
var init_journalMatchers = __esm(() => {
|
|
4268
|
+
JOURNAL_ACCOUNT_DECL = /^account\s+(.+)$/;
|
|
4269
|
+
RULES_ACCOUNT_DIRECTIVE = /^account\d+\s+(.+)$/;
|
|
4270
|
+
RULES_ACCOUNT2_DIRECTIVE = /^\s*account2\s+(.+)$/;
|
|
4271
|
+
TX_HEADER_PATTERN = /^(\d{4})-(\d{2}-\d{2})(\s+(.+))?$/;
|
|
4272
|
+
});
|
|
4273
|
+
|
|
4261
4274
|
// src/utils/accountSuggester.ts
|
|
4262
4275
|
var exports_accountSuggester = {};
|
|
4263
4276
|
__export(exports_accountSuggester, {
|
|
@@ -4287,9 +4300,9 @@ function loadExistingAccounts(yearJournalPath) {
|
|
|
4287
4300
|
for (const line of lines) {
|
|
4288
4301
|
const trimmed = line.trim();
|
|
4289
4302
|
if (trimmed.startsWith("account ")) {
|
|
4290
|
-
const
|
|
4291
|
-
if (
|
|
4292
|
-
accounts.push(
|
|
4303
|
+
const accountName = extractAccount(trimmed, JOURNAL_ACCOUNT_DECL);
|
|
4304
|
+
if (accountName) {
|
|
4305
|
+
accounts.push(accountName);
|
|
4293
4306
|
}
|
|
4294
4307
|
}
|
|
4295
4308
|
}
|
|
@@ -4314,11 +4327,11 @@ function extractRulePatternsFromFile(rulesPath) {
|
|
|
4314
4327
|
currentCondition = ifMatch[1].trim();
|
|
4315
4328
|
continue;
|
|
4316
4329
|
}
|
|
4317
|
-
const
|
|
4318
|
-
if (
|
|
4330
|
+
const account2Name = extractAccount(trimmed, RULES_ACCOUNT2_DIRECTIVE);
|
|
4331
|
+
if (account2Name && currentCondition) {
|
|
4319
4332
|
patterns.push({
|
|
4320
4333
|
condition: currentCondition,
|
|
4321
|
-
account:
|
|
4334
|
+
account: account2Name
|
|
4322
4335
|
});
|
|
4323
4336
|
currentCondition = null;
|
|
4324
4337
|
continue;
|
|
@@ -4504,6 +4517,7 @@ function generateMockSuggestions(postings) {
|
|
|
4504
4517
|
var EXAMPLE_PATTERN_SAMPLE_SIZE = 10, suggestionCache, RESPONSE_FORMAT_SECTION;
|
|
4505
4518
|
var init_accountSuggester = __esm(() => {
|
|
4506
4519
|
init_agentLoader();
|
|
4520
|
+
init_journalMatchers();
|
|
4507
4521
|
suggestionCache = {};
|
|
4508
4522
|
RESPONSE_FORMAT_SECTION = [
|
|
4509
4523
|
`## Task
|
|
@@ -4533,7 +4547,7 @@ var init_accountSuggester = __esm(() => {
|
|
|
4533
4547
|
|
|
4534
4548
|
// src/index.ts
|
|
4535
4549
|
init_agentLoader();
|
|
4536
|
-
import { dirname as
|
|
4550
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
4537
4551
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4538
4552
|
|
|
4539
4553
|
// node_modules/zod/v4/classic/external.js
|
|
@@ -17051,6 +17065,38 @@ function ensureYearJournalExists(directory, year) {
|
|
|
17051
17065
|
}
|
|
17052
17066
|
return yearJournalPath;
|
|
17053
17067
|
}
|
|
17068
|
+
function ensureCommodityDeclarations(commodityJournalPath, symbols, logger) {
|
|
17069
|
+
const existing = new Set;
|
|
17070
|
+
if (fs4.existsSync(commodityJournalPath)) {
|
|
17071
|
+
const content2 = fs4.readFileSync(commodityJournalPath, "utf-8");
|
|
17072
|
+
for (const line of content2.split(`
|
|
17073
|
+
`)) {
|
|
17074
|
+
const match = line.trim().match(/^commodity\s+"(.+)"/);
|
|
17075
|
+
if (match) {
|
|
17076
|
+
existing.add(match[1]);
|
|
17077
|
+
}
|
|
17078
|
+
}
|
|
17079
|
+
}
|
|
17080
|
+
const missing = [];
|
|
17081
|
+
for (const symbol2 of symbols) {
|
|
17082
|
+
const upper = symbol2.toUpperCase();
|
|
17083
|
+
if (!existing.has(upper)) {
|
|
17084
|
+
missing.push(upper);
|
|
17085
|
+
existing.add(upper);
|
|
17086
|
+
}
|
|
17087
|
+
}
|
|
17088
|
+
if (missing.length === 0) {
|
|
17089
|
+
return { added: [], updated: false };
|
|
17090
|
+
}
|
|
17091
|
+
const sorted = Array.from(existing).sort((a, b) => a.localeCompare(b));
|
|
17092
|
+
const content = sorted.map((s) => `commodity "${s}"`).join(`
|
|
17093
|
+
`) + `
|
|
17094
|
+
`;
|
|
17095
|
+
ensureDirectory(path3.dirname(commodityJournalPath));
|
|
17096
|
+
fs4.writeFileSync(commodityJournalPath, content);
|
|
17097
|
+
logger?.info(`Commodity declarations: added ${missing.length} (${missing.join(", ")})`);
|
|
17098
|
+
return { added: missing.sort(), updated: true };
|
|
17099
|
+
}
|
|
17054
17100
|
|
|
17055
17101
|
// src/utils/dateUtils.ts
|
|
17056
17102
|
function formatDateISO(date5) {
|
|
@@ -23091,7 +23137,7 @@ function findRulesForCsv(csvPath, mapping) {
|
|
|
23091
23137
|
|
|
23092
23138
|
// src/utils/hledgerExecutor.ts
|
|
23093
23139
|
var {$: $2 } = globalThis.Bun;
|
|
23094
|
-
|
|
23140
|
+
init_journalMatchers();
|
|
23095
23141
|
async function defaultHledgerExecutor(cmdArgs) {
|
|
23096
23142
|
try {
|
|
23097
23143
|
const result = await $2`hledger ${cmdArgs}`.quiet().nothrow();
|
|
@@ -24118,6 +24164,7 @@ import * as fs20 from "fs";
|
|
|
24118
24164
|
import * as path14 from "path";
|
|
24119
24165
|
|
|
24120
24166
|
// src/utils/accountDeclarations.ts
|
|
24167
|
+
init_journalMatchers();
|
|
24121
24168
|
import * as fs12 from "fs";
|
|
24122
24169
|
function extractAccountsFromRulesFile(rulesPath) {
|
|
24123
24170
|
const accounts = new Set;
|
|
@@ -24132,9 +24179,9 @@ function extractAccountsFromRulesFile(rulesPath) {
|
|
|
24132
24179
|
if (trimmed.startsWith("#") || trimmed.startsWith(";") || trimmed === "") {
|
|
24133
24180
|
continue;
|
|
24134
24181
|
}
|
|
24135
|
-
const
|
|
24136
|
-
if (
|
|
24137
|
-
accounts.add(
|
|
24182
|
+
const accountName = extractAccount(trimmed, RULES_ACCOUNT_DIRECTIVE);
|
|
24183
|
+
if (accountName) {
|
|
24184
|
+
accounts.add(accountName);
|
|
24138
24185
|
continue;
|
|
24139
24186
|
}
|
|
24140
24187
|
}
|
|
@@ -24169,9 +24216,9 @@ function parseJournalSections(content) {
|
|
|
24169
24216
|
}
|
|
24170
24217
|
if (trimmed.startsWith("account ")) {
|
|
24171
24218
|
inAccountSection = true;
|
|
24172
|
-
const
|
|
24173
|
-
if (
|
|
24174
|
-
existingAccounts.add(
|
|
24219
|
+
const accountName = extractAccount(trimmed, JOURNAL_ACCOUNT_DECL);
|
|
24220
|
+
if (accountName) {
|
|
24221
|
+
existingAccounts.add(accountName);
|
|
24175
24222
|
}
|
|
24176
24223
|
continue;
|
|
24177
24224
|
}
|
|
@@ -25016,6 +25063,9 @@ function formatDate(dateStr) {
|
|
|
25016
25063
|
function escapeDescription(desc) {
|
|
25017
25064
|
return desc.replace(/[;|]/g, "-").trim();
|
|
25018
25065
|
}
|
|
25066
|
+
function formatCommodity(symbol2) {
|
|
25067
|
+
return `"${symbol2.toUpperCase()}"`;
|
|
25068
|
+
}
|
|
25019
25069
|
function formatQuantity(qty) {
|
|
25020
25070
|
return qty.toFixed(6).replace(/\.?0+$/, "");
|
|
25021
25071
|
}
|
|
@@ -25033,11 +25083,12 @@ function generateBuyEntry(trade, logger) {
|
|
|
25033
25083
|
const fees = trade.costs;
|
|
25034
25084
|
const cashOut = totalCost + fees;
|
|
25035
25085
|
logger?.debug(`Generating Buy entry: ${qty} ${trade.symbol} @ ${price} ${trade.currency}`);
|
|
25086
|
+
const commodity = formatCommodity(trade.symbol);
|
|
25036
25087
|
let entry = `${date5} ${description}
|
|
25037
25088
|
`;
|
|
25038
25089
|
entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
|
|
25039
25090
|
`;
|
|
25040
|
-
entry += ` assets:investments:stocks:${trade.symbol} ${qty} @ ${price} ${trade.currency}
|
|
25091
|
+
entry += ` assets:investments:stocks:${trade.symbol} ${qty} ${commodity} @ ${price} ${trade.currency}
|
|
25041
25092
|
`;
|
|
25042
25093
|
if (fees > 0) {
|
|
25043
25094
|
entry += ` expenses:fees:trading:swissquote ${formatAmount2(fees, trade.currency)}
|
|
@@ -25057,6 +25108,7 @@ function generateSellEntry(trade, consumed, logger) {
|
|
|
25057
25108
|
const cashIn = saleProceeds - fees;
|
|
25058
25109
|
const gain = calculateCapitalGain(consumed, salePrice, trade.quantity);
|
|
25059
25110
|
logger?.debug(`Generating Sell entry: ${qty} ${trade.symbol} @ ${salePrice} ${trade.currency}, gain: ${gain.toFixed(2)}`);
|
|
25111
|
+
const commodity = formatCommodity(trade.symbol);
|
|
25060
25112
|
let entry = `${date5} ${description}
|
|
25061
25113
|
`;
|
|
25062
25114
|
entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
|
|
@@ -25067,7 +25119,7 @@ function generateSellEntry(trade, consumed, logger) {
|
|
|
25067
25119
|
for (const c of consumed) {
|
|
25068
25120
|
const lotQty = formatQuantity(c.quantity);
|
|
25069
25121
|
const lotPrice = formatPrice(c.lot.costBasis);
|
|
25070
|
-
entry += ` assets:investments:stocks:${trade.symbol} -${lotQty} @ ${lotPrice} ${trade.currency}
|
|
25122
|
+
entry += ` assets:investments:stocks:${trade.symbol} -${lotQty} ${commodity} @ ${lotPrice} ${trade.currency}
|
|
25071
25123
|
`;
|
|
25072
25124
|
}
|
|
25073
25125
|
entry += ` assets:broker:swissquote:${trade.currency.toLowerCase()} ${formatAmount2(cashIn, trade.currency)}
|
|
@@ -25115,13 +25167,14 @@ function generateSplitEntry(action, oldQuantity, newQuantity, logger) {
|
|
|
25115
25167
|
`;
|
|
25116
25168
|
entry += ` ; Ratio: ${ratio.toFixed(4)} (${oldQuantity} -> ${newQuantity})
|
|
25117
25169
|
`;
|
|
25118
|
-
|
|
25170
|
+
const commodity = formatCommodity(action.symbol);
|
|
25171
|
+
entry += ` assets:investments:stocks:${action.symbol} -${formatQuantity(oldQuantity)} ${commodity}
|
|
25119
25172
|
`;
|
|
25120
|
-
entry += ` equity:conversion ${formatQuantity(oldQuantity)}
|
|
25173
|
+
entry += ` equity:conversion ${formatQuantity(oldQuantity)} ${commodity}
|
|
25121
25174
|
`;
|
|
25122
|
-
entry += ` equity:conversion -${formatQuantity(newQuantity)}
|
|
25175
|
+
entry += ` equity:conversion -${formatQuantity(newQuantity)} ${commodity}
|
|
25123
25176
|
`;
|
|
25124
|
-
entry += ` assets:investments:stocks:${action.symbol} ${formatQuantity(newQuantity)}
|
|
25177
|
+
entry += ` assets:investments:stocks:${action.symbol} ${formatQuantity(newQuantity)} ${commodity}
|
|
25125
25178
|
`;
|
|
25126
25179
|
return entry;
|
|
25127
25180
|
}
|
|
@@ -25138,10 +25191,11 @@ function generateWorthlessEntry(action, removedLots, logger) {
|
|
|
25138
25191
|
`;
|
|
25139
25192
|
entry += ` ; Total loss: ${totalCost.toFixed(2)} ${currency}
|
|
25140
25193
|
`;
|
|
25194
|
+
const commodity = formatCommodity(action.symbol);
|
|
25141
25195
|
for (const lot of removedLots) {
|
|
25142
25196
|
const qty = formatQuantity(lot.quantity);
|
|
25143
25197
|
const price = formatPrice(lot.costBasis);
|
|
25144
|
-
entry += ` assets:investments:stocks:${action.symbol} -${qty} @ ${price} ${currency}
|
|
25198
|
+
entry += ` assets:investments:stocks:${action.symbol} -${qty} ${commodity} @ ${price} ${currency}
|
|
25145
25199
|
`;
|
|
25146
25200
|
}
|
|
25147
25201
|
entry += ` expenses:losses:capital ${formatAmount2(totalCost, currency)}
|
|
@@ -25166,16 +25220,18 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, logger
|
|
|
25166
25220
|
}
|
|
25167
25221
|
for (const out of group.outgoing) {
|
|
25168
25222
|
const qty = formatQuantity(Math.abs(out.quantity));
|
|
25169
|
-
|
|
25223
|
+
const commodity = formatCommodity(out.symbol);
|
|
25224
|
+
entry += ` assets:investments:stocks:${out.symbol} -${qty} ${commodity}
|
|
25170
25225
|
`;
|
|
25171
|
-
entry += ` equity:conversion ${qty}
|
|
25226
|
+
entry += ` equity:conversion ${qty} ${commodity}
|
|
25172
25227
|
`;
|
|
25173
25228
|
}
|
|
25174
25229
|
for (const inc of group.incoming) {
|
|
25175
25230
|
const qty = formatQuantity(Math.abs(inc.quantity));
|
|
25176
|
-
|
|
25231
|
+
const commodity = formatCommodity(inc.symbol);
|
|
25232
|
+
entry += ` equity:conversion -${qty} ${commodity}
|
|
25177
25233
|
`;
|
|
25178
|
-
entry += ` assets:investments:stocks:${inc.symbol} ${qty}
|
|
25234
|
+
entry += ` assets:investments:stocks:${inc.symbol} ${qty} ${commodity}
|
|
25179
25235
|
`;
|
|
25180
25236
|
}
|
|
25181
25237
|
return entry;
|
|
@@ -25185,11 +25241,12 @@ function generateRightsDistributionEntry(action, logger) {
|
|
|
25185
25241
|
const qty = formatQuantity(Math.abs(action.quantity));
|
|
25186
25242
|
const description = escapeDescription(`Rights Distribution: ${action.symbol} - ${action.name}`);
|
|
25187
25243
|
logger?.debug(`Generating Rights Distribution entry: ${qty} ${action.symbol}`);
|
|
25244
|
+
const commodity = formatCommodity(action.symbol);
|
|
25188
25245
|
let entry = `${date5} ${description}
|
|
25189
25246
|
`;
|
|
25190
25247
|
entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
|
|
25191
25248
|
`;
|
|
25192
|
-
entry += ` assets:investments:stocks:${action.symbol} ${qty} @ 0.00 CAD
|
|
25249
|
+
entry += ` assets:investments:stocks:${action.symbol} ${qty} ${commodity} @ 0.00 CAD
|
|
25193
25250
|
`;
|
|
25194
25251
|
entry += ` income:capital-gains:rights-distribution 0.00 CAD
|
|
25195
25252
|
`;
|
|
@@ -25757,6 +25814,23 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
|
|
|
25757
25814
|
logger?.logStep("write-journal", "success", `Created ${path13.basename(journalFile)} with ${journalEntries.length} entries`);
|
|
25758
25815
|
}
|
|
25759
25816
|
}
|
|
25817
|
+
const stockSymbols = new Set;
|
|
25818
|
+
for (const trade of trades) {
|
|
25819
|
+
stockSymbols.add(trade.symbol);
|
|
25820
|
+
}
|
|
25821
|
+
for (const dividend of dividends) {
|
|
25822
|
+
stockSymbols.add(dividend.symbol);
|
|
25823
|
+
}
|
|
25824
|
+
for (const action of corporateActions) {
|
|
25825
|
+
stockSymbols.add(action.symbol);
|
|
25826
|
+
if (action.newSymbol) {
|
|
25827
|
+
stockSymbols.add(action.newSymbol);
|
|
25828
|
+
}
|
|
25829
|
+
}
|
|
25830
|
+
if (stockSymbols.size > 0) {
|
|
25831
|
+
const commodityJournalPath = path13.join(projectDir, "ledger", "investments", "commodities.journal");
|
|
25832
|
+
ensureCommodityDeclarations(commodityJournalPath, stockSymbols, logger);
|
|
25833
|
+
}
|
|
25760
25834
|
logger?.logResult({
|
|
25761
25835
|
totalRows: stats.totalRows,
|
|
25762
25836
|
simpleTransactions: stats.simpleTransactions,
|
|
@@ -26685,7 +26759,7 @@ to produce equity conversion entries for Bitcoin purchases.
|
|
|
26685
26759
|
}
|
|
26686
26760
|
});
|
|
26687
26761
|
// src/index.ts
|
|
26688
|
-
var __dirname2 =
|
|
26762
|
+
var __dirname2 = dirname7(fileURLToPath3(import.meta.url));
|
|
26689
26763
|
var AGENT_FILE = join15(__dirname2, "..", "agent", "accountant.md");
|
|
26690
26764
|
var AccountantPlugin = async () => {
|
|
26691
26765
|
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
|
|