@quantbrasil/cli 0.1.0-beta.3 → 0.1.0-beta.5

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.
Files changed (44) hide show
  1. package/README.md +18 -11
  2. package/dist/cli/index.d.ts +4 -4
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +15 -12
  5. package/dist/commands/auth.d.ts +18 -0
  6. package/dist/commands/auth.d.ts.map +1 -1
  7. package/dist/commands/auth.js +49 -0
  8. package/dist/commands/market.d.ts +1 -0
  9. package/dist/commands/market.d.ts.map +1 -1
  10. package/dist/commands/market.js +17 -1
  11. package/dist/commands/portfolios.d.ts +148 -8
  12. package/dist/commands/portfolios.d.ts.map +1 -1
  13. package/dist/commands/portfolios.js +557 -55
  14. package/dist/index.d.ts +0 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +0 -1
  17. package/dist/vendor/core/capabilities/index.d.ts +0 -1
  18. package/dist/vendor/core/capabilities/index.d.ts.map +1 -1
  19. package/dist/vendor/core/capabilities/index.js +0 -1
  20. package/dist/vendor/core/capabilities/market.d.ts +9 -1
  21. package/dist/vendor/core/capabilities/market.d.ts.map +1 -1
  22. package/dist/vendor/core/capabilities/market.js +10 -0
  23. package/dist/vendor/core/capabilities/portfolios.d.ts +452 -56
  24. package/dist/vendor/core/capabilities/portfolios.d.ts.map +1 -1
  25. package/dist/vendor/core/capabilities/portfolios.js +434 -116
  26. package/dist/vendor/core/capabilities/registry.d.ts +714 -276
  27. package/dist/vendor/core/capabilities/registry.d.ts.map +1 -1
  28. package/dist/vendor/core/capabilities/registry.js +0 -2
  29. package/dist/vendor/core/capabilities/types.d.ts +1 -1
  30. package/dist/vendor/core/capabilities/types.d.ts.map +1 -1
  31. package/package.json +3 -3
  32. package/skills/quantbrasil/SKILL.md +9 -6
  33. package/skills/quantbrasil/references/cli.md +25 -25
  34. package/skills/quantbrasil/references/costs.md +4 -4
  35. package/skills/quantbrasil/references/errors.md +6 -5
  36. package/skills/quantbrasil/references/portfolios.md +103 -0
  37. package/skills/quantbrasil/references/unsupported.md +5 -2
  38. package/skills/quantbrasil/references/workflows.md +38 -26
  39. package/dist/commands/analytics.d.ts +0 -131
  40. package/dist/commands/analytics.d.ts.map +0 -1
  41. package/dist/commands/analytics.js +0 -291
  42. package/dist/vendor/core/capabilities/analytics.d.ts +0 -187
  43. package/dist/vendor/core/capabilities/analytics.d.ts.map +0 -1
  44. package/dist/vendor/core/capabilities/analytics.js +0 -214
@@ -2,65 +2,156 @@ import { invokeCliCapability } from "../cli/client.js";
2
2
  import { createCliMutationFailedError, createCliValidationError, } from "../cli/errors.js";
3
3
  import { createTerminalTheme } from "../cli/terminal.js";
4
4
  export function registerPortfoliosCommands(program, context = {}) {
5
- const portfoliosCommand = program
6
- .command("portfolios")
7
- .description("Manage saved portfolios");
8
- portfoliosCommand
5
+ const watchlistsCommand = program
6
+ .command("watchlists")
7
+ .description("Manage watchlists for tracking ideas and filters");
8
+ watchlistsCommand
9
9
  .command("list")
10
- .description("List saved portfolios for authenticated user")
10
+ .description("List watchlists for authenticated user")
11
11
  .option("--json", "Show JSON output")
12
12
  .action(async (options) => {
13
- await runPortfolioListCommand(options, context);
13
+ await runWatchlistListCommand(options, context);
14
14
  });
15
- portfoliosCommand
15
+ watchlistsCommand
16
16
  .command("get")
17
- .description("Get one saved portfolio by id")
18
- .argument("<portfolio-id>", "Saved portfolio id", parsePortfolioIdArgument)
17
+ .description("Get one watchlist by id")
18
+ .argument("<watchlist-id>", "Watchlist id", parsePortfolioIdArgument)
19
19
  .option("--json", "Show JSON output")
20
20
  .action(async (portfolioId, options) => {
21
- await runPortfolioGetCommand(portfolioId, options, context);
21
+ await runWatchlistGetCommand(portfolioId, options, context);
22
22
  });
23
- portfoliosCommand
23
+ watchlistsCommand
24
24
  .command("create")
25
- .description("Create a new portfolio")
26
- .argument("<name>", "Portfolio name")
25
+ .description("Create a new watchlist")
26
+ .argument("<name>", "Watchlist name")
27
27
  .option("--json", "Show JSON output")
28
28
  .action(async (name, options) => {
29
- await runPortfolioCreateCommand(name, options, context);
29
+ await runWatchlistCreateCommand(name, options, context);
30
30
  });
31
- portfoliosCommand
31
+ watchlistsCommand
32
32
  .command("rename")
33
- .description("Rename an existing portfolio")
34
- .argument("<portfolio-id>", "Saved portfolio id", parsePortfolioIdArgument)
35
- .argument("<name>", "New portfolio name")
33
+ .description("Rename an existing watchlist")
34
+ .argument("<watchlist-id>", "Watchlist id", parsePortfolioIdArgument)
35
+ .argument("<name>", "New watchlist name")
36
36
  .option("--json", "Show JSON output")
37
37
  .action(async (portfolioId, name, options) => {
38
- await runPortfolioRenameCommand(portfolioId, name, options, context);
38
+ await runWatchlistRenameCommand(portfolioId, name, options, context);
39
39
  });
40
- portfoliosCommand
40
+ watchlistsCommand
41
41
  .command("add-assets")
42
- .description("Add one or more monitored tickers to a portfolio")
43
- .argument("<portfolio-id>", "Saved portfolio id", parsePortfolioIdArgument)
42
+ .description("Add one or more monitored tickers to a watchlist")
43
+ .argument("<watchlist-id>", "Watchlist id", parsePortfolioIdArgument)
44
44
  .argument("<ticker...>", "One or more tickers to add")
45
45
  .option("--json", "Show JSON output")
46
46
  .action(async (portfolioId, symbols, options) => {
47
- await runPortfolioAddAssetsCommand(portfolioId, symbols, options, context);
47
+ await runWatchlistAddAssetsCommand(portfolioId, symbols, options, context);
48
48
  });
49
- portfoliosCommand
49
+ watchlistsCommand
50
50
  .command("remove-assets")
51
- .description("Remove one or more monitored tickers from a portfolio")
52
- .argument("<portfolio-id>", "Saved portfolio id", parsePortfolioIdArgument)
51
+ .description("Remove one or more monitored tickers from a watchlist")
52
+ .argument("<watchlist-id>", "Watchlist id", parsePortfolioIdArgument)
53
53
  .argument("<ticker...>", "One or more tickers to remove")
54
54
  .option("--json", "Show JSON output")
55
55
  .action(async (portfolioId, symbols, options) => {
56
- await runPortfolioRemoveAssetsCommand(portfolioId, symbols, options, context);
56
+ await runWatchlistRemoveAssetsCommand(portfolioId, symbols, options, context);
57
57
  });
58
+ const holdingsCommand = program
59
+ .command("holdings")
60
+ .description("Manage holdings for investable metrics");
61
+ holdingsCommand
62
+ .command("list")
63
+ .description("List holdings for authenticated user")
64
+ .option("--json", "Show JSON output")
65
+ .action(async (options) => {
66
+ await runHoldingListCommand(options, context);
67
+ });
68
+ holdingsCommand
69
+ .command("get")
70
+ .description("Get one holding by id")
71
+ .argument("<holding-id>", "Holding id", parsePortfolioIdArgument)
72
+ .option("--json", "Show JSON output")
73
+ .action(async (portfolioId, options) => {
74
+ await runHoldingGetCommand(portfolioId, options, context);
75
+ });
76
+ holdingsCommand
77
+ .command("create")
78
+ .description("Create a new holding")
79
+ .argument("<name>", "Holding name")
80
+ .option("--mode <mode>", "Initial mode: target or position", "target")
81
+ .option("--target <target>", "Initial target weight in TICKER:WEIGHT_PCT form", collectStringOption, [])
82
+ .option("--json", "Show JSON output")
83
+ .action(async (name, options) => {
84
+ await runHoldingCreateCommand(name, options, context);
85
+ });
86
+ holdingsCommand
87
+ .command("rename")
88
+ .description("Rename an existing holding")
89
+ .argument("<holding-id>", "Holding id", parsePortfolioIdArgument)
90
+ .argument("<name>", "New holding name")
91
+ .option("--json", "Show JSON output")
92
+ .action(async (portfolioId, name, options) => {
93
+ await runHoldingRenameCommand(portfolioId, name, options, context);
94
+ });
95
+ holdingsCommand
96
+ .command("set-targets")
97
+ .description("Set target weights for a holding")
98
+ .argument("<holding-id>", "Holding id", parsePortfolioIdArgument)
99
+ .argument("<target...>", "One or more target weights in TICKER:WEIGHT_PCT form")
100
+ .option("--json", "Show JSON output")
101
+ .action(async (portfolioId, targets, options) => {
102
+ await runHoldingSetTargetsCommand(portfolioId, targets, options, context);
103
+ });
104
+ holdingsCommand
105
+ .command("set-positions")
106
+ .description("Set position quantities for a holding")
107
+ .argument("<holding-id>", "Holding id", parsePortfolioIdArgument)
108
+ .argument("<position...>", "One or more position quantities in TICKER:QUANTITY form")
109
+ .option("--json", "Show JSON output")
110
+ .action(async (portfolioId, positions, options) => {
111
+ await runHoldingSetPositionsCommand(portfolioId, positions, options, context);
112
+ });
113
+ holdingsCommand
114
+ .command("historical-return")
115
+ .description("Calculate historical return for a saved holding")
116
+ .argument("<holding-id>", "Holding id", parsePortfolioIdArgument)
117
+ .option("--from <date>", "Inclusive start date in ISO format")
118
+ .option("--to <date>", "Inclusive end date in ISO format")
119
+ .option("--period <period>", "Lookback period such as 1y, 3y, 5y, or 6m")
120
+ .option("--json", "Show JSON output")
121
+ .action(async (portfolioId, options) => {
122
+ await runHoldingHistoricalReturnCommand(portfolioId, options, context);
123
+ });
124
+ holdingsCommand
125
+ .command("beta")
126
+ .description("Calculate holding beta against IBOV")
127
+ .argument("<holding-id>", "Holding id", parsePortfolioIdArgument)
128
+ .option("--years <years>", "Lookback window in years", "1")
129
+ .option("--json", "Show JSON output")
130
+ .action(async (portfolioId, options) => {
131
+ await runHoldingBetaCommand(portfolioId, options, context);
132
+ });
133
+ holdingsCommand
134
+ .command("var")
135
+ .description("Calculate one-day Value-at-Risk for a holding")
136
+ .argument("<holding-id>", "Holding id", parsePortfolioIdArgument)
137
+ .option("--years <years>", "Lookback window in years", "1")
138
+ .option("--confidence <confidence>", "Confidence level in percent", "95")
139
+ .option("--json", "Show JSON output")
140
+ .action(async (portfolioId, options) => {
141
+ await runHoldingVarCommand(portfolioId, options, context);
142
+ });
143
+ }
144
+ export async function runWatchlistListCommand(options, context = {}) {
145
+ await runPortfolioListLikeCommand("watchlists.list", "Watchlists", options, context);
58
146
  }
59
- export async function runPortfolioListCommand(options, context = {}) {
147
+ export async function runHoldingListCommand(options, context = {}) {
148
+ await runPortfolioListLikeCommand("holdings.list", "Holdings", options, context);
149
+ }
150
+ async function runPortfolioListLikeCommand(capability, title, options, context) {
60
151
  const stdout = context.io?.stdout ?? process.stdout;
61
152
  const theme = createTerminalTheme(stdout, context.env ?? process.env);
62
153
  const response = await invokeCliCapability({
63
- capability: "portfolios.list",
154
+ capability,
64
155
  env: context.env,
65
156
  fetch: context.fetch,
66
157
  });
@@ -68,13 +159,19 @@ export async function runPortfolioListCommand(options, context = {}) {
68
159
  stdout.write(`${JSON.stringify(response.data, null, 2)}\n`);
69
160
  return;
70
161
  }
71
- stdout.write(`${formatPortfolioListHuman(response.data, theme)}\n`);
162
+ stdout.write(`${formatPortfolioListHuman(response.data, theme, title)}\n`);
163
+ }
164
+ export async function runWatchlistGetCommand(portfolioId, options, context = {}) {
165
+ await runPortfolioGetLikeCommand("watchlists.get", "Watchlist", portfolioId, options, context);
166
+ }
167
+ export async function runHoldingGetCommand(portfolioId, options, context = {}) {
168
+ await runPortfolioGetLikeCommand("holdings.get", "Holding", portfolioId, options, context);
72
169
  }
73
- export async function runPortfolioGetCommand(portfolioId, options, context = {}) {
170
+ async function runPortfolioGetLikeCommand(capability, title, portfolioId, options, context) {
74
171
  const stdout = context.io?.stdout ?? process.stdout;
75
172
  const theme = createTerminalTheme(stdout, context.env ?? process.env);
76
173
  const response = await invokeCliCapability({
77
- capability: "portfolios.get",
174
+ capability,
78
175
  input: {
79
176
  portfolio_id: portfolioId,
80
177
  },
@@ -85,26 +182,74 @@ export async function runPortfolioGetCommand(portfolioId, options, context = {})
85
182
  stdout.write(`${JSON.stringify(response.data, null, 2)}\n`);
86
183
  return;
87
184
  }
88
- stdout.write(`${formatPortfolioGetHuman(response.data, theme)}\n`);
185
+ stdout.write(`${formatPortfolioGetHuman(response.data, theme, title)}\n`);
186
+ }
187
+ export async function runWatchlistCreateCommand(name, options, context = {}) {
188
+ const stdout = context.io?.stdout ?? process.stdout;
189
+ const theme = createTerminalTheme(stdout, context.env ?? process.env);
190
+ const response = await invokeCliCapability({
191
+ capability: "watchlists.create",
192
+ input: {
193
+ name: parsePortfolioNameArgument(name),
194
+ },
195
+ env: context.env,
196
+ fetch: context.fetch,
197
+ });
198
+ writeMutationResponse(response.data, options.json, stdout, theme);
89
199
  }
90
- export async function runPortfolioCreateCommand(name, options, context = {}) {
200
+ export async function runHoldingCreateCommand(name, options, context = {}) {
91
201
  const stdout = context.io?.stdout ?? process.stdout;
92
202
  const theme = createTerminalTheme(stdout, context.env ?? process.env);
203
+ const defaultAllocationInput = parseHoldingModeOption(options.mode ?? "target");
204
+ const targets = parseTargetArguments(options.target ?? []);
205
+ if (targets.length > 0 && defaultAllocationInput === "POSITION") {
206
+ throw createCliValidationError("Initial targets require target mode.");
207
+ }
93
208
  const response = await invokeCliCapability({
94
- capability: "portfolios.create",
209
+ capability: "holdings.create",
95
210
  input: {
96
211
  name: parsePortfolioNameArgument(name),
212
+ default_allocation_input: defaultAllocationInput,
213
+ ...(targets.length > 0 ? { targets } : {}),
97
214
  },
98
215
  env: context.env,
99
216
  fetch: context.fetch,
100
217
  });
101
218
  writeMutationResponse(response.data, options.json, stdout, theme);
102
219
  }
103
- export async function runPortfolioRenameCommand(portfolioId, name, options, context = {}) {
220
+ export async function runHoldingSetTargetsCommand(portfolioId, targets, options, context = {}) {
104
221
  const stdout = context.io?.stdout ?? process.stdout;
105
222
  const theme = createTerminalTheme(stdout, context.env ?? process.env);
106
223
  const response = await invokeCliCapability({
107
- capability: "portfolios.rename",
224
+ capability: "holdings.set-targets",
225
+ input: {
226
+ portfolio_id: portfolioId,
227
+ targets: parseTargetArguments(targets),
228
+ },
229
+ env: context.env,
230
+ fetch: context.fetch,
231
+ });
232
+ writeMutationResponse(response.data, options.json, stdout, theme);
233
+ }
234
+ export async function runHoldingSetPositionsCommand(portfolioId, positions, options, context = {}) {
235
+ const stdout = context.io?.stdout ?? process.stdout;
236
+ const theme = createTerminalTheme(stdout, context.env ?? process.env);
237
+ const response = await invokeCliCapability({
238
+ capability: "holdings.set-positions",
239
+ input: {
240
+ portfolio_id: portfolioId,
241
+ positions: parsePositionArguments(positions),
242
+ },
243
+ env: context.env,
244
+ fetch: context.fetch,
245
+ });
246
+ writeMutationResponse(response.data, options.json, stdout, theme);
247
+ }
248
+ export async function runWatchlistRenameCommand(portfolioId, name, options, context = {}) {
249
+ const stdout = context.io?.stdout ?? process.stdout;
250
+ const theme = createTerminalTheme(stdout, context.env ?? process.env);
251
+ const response = await invokeCliCapability({
252
+ capability: "watchlists.rename",
108
253
  input: {
109
254
  portfolio_id: portfolioId,
110
255
  name: parsePortfolioNameArgument(name),
@@ -114,11 +259,25 @@ export async function runPortfolioRenameCommand(portfolioId, name, options, cont
114
259
  });
115
260
  writeMutationResponse(response.data, options.json, stdout, theme);
116
261
  }
117
- export async function runPortfolioAddAssetsCommand(portfolioId, symbols, options, context = {}) {
262
+ export async function runHoldingRenameCommand(portfolioId, name, options, context = {}) {
118
263
  const stdout = context.io?.stdout ?? process.stdout;
119
264
  const theme = createTerminalTheme(stdout, context.env ?? process.env);
120
265
  const response = await invokeCliCapability({
121
- capability: "portfolios.add-assets",
266
+ capability: "holdings.rename",
267
+ input: {
268
+ portfolio_id: portfolioId,
269
+ name: parsePortfolioNameArgument(name),
270
+ },
271
+ env: context.env,
272
+ fetch: context.fetch,
273
+ });
274
+ writeMutationResponse(response.data, options.json, stdout, theme);
275
+ }
276
+ export async function runWatchlistAddAssetsCommand(portfolioId, symbols, options, context = {}) {
277
+ const stdout = context.io?.stdout ?? process.stdout;
278
+ const theme = createTerminalTheme(stdout, context.env ?? process.env);
279
+ const response = await invokeCliCapability({
280
+ capability: "watchlists.add-assets",
122
281
  input: {
123
282
  portfolio_id: portfolioId,
124
283
  symbols: parseSymbolArguments(symbols),
@@ -128,11 +287,11 @@ export async function runPortfolioAddAssetsCommand(portfolioId, symbols, options
128
287
  });
129
288
  writeMutationResponse(response.data, options.json, stdout, theme);
130
289
  }
131
- export async function runPortfolioRemoveAssetsCommand(portfolioId, symbols, options, context = {}) {
290
+ export async function runWatchlistRemoveAssetsCommand(portfolioId, symbols, options, context = {}) {
132
291
  const stdout = context.io?.stdout ?? process.stdout;
133
292
  const theme = createTerminalTheme(stdout, context.env ?? process.env);
134
293
  const response = await invokeCliCapability({
135
- capability: "portfolios.remove-assets",
294
+ capability: "watchlists.remove-assets",
136
295
  input: {
137
296
  portfolio_id: portfolioId,
138
297
  symbols: parseSymbolArguments(symbols),
@@ -142,15 +301,72 @@ export async function runPortfolioRemoveAssetsCommand(portfolioId, symbols, opti
142
301
  });
143
302
  writeMutationResponse(response.data, options.json, stdout, theme);
144
303
  }
145
- export function formatPortfolioListHuman(data, theme = createTerminalTheme(process.stdout)) {
304
+ export async function runHoldingHistoricalReturnCommand(portfolioId, options, context = {}) {
305
+ const stdout = context.io?.stdout ?? process.stdout;
306
+ const theme = createTerminalTheme(stdout, context.env ?? process.env);
307
+ const dateRange = resolveHistoricalReturnDateRange(options, context.now ?? new Date());
308
+ const response = await invokeCliCapability({
309
+ capability: "holdings.historical-return",
310
+ input: {
311
+ portfolio_id: portfolioId,
312
+ start_date: dateRange.start_date,
313
+ end_date: dateRange.end_date,
314
+ },
315
+ env: context.env,
316
+ fetch: context.fetch,
317
+ });
318
+ if (options.json) {
319
+ stdout.write(`${JSON.stringify(response.data, null, 2)}\n`);
320
+ return;
321
+ }
322
+ stdout.write(`${formatHistoricalReturnHuman(response.data, theme)}\n`);
323
+ }
324
+ export async function runHoldingBetaCommand(portfolioId, options, context = {}) {
325
+ const stdout = context.io?.stdout ?? process.stdout;
326
+ const theme = createTerminalTheme(stdout, context.env ?? process.env);
327
+ const response = await invokeCliCapability({
328
+ capability: "holdings.beta",
329
+ input: {
330
+ portfolio_id: portfolioId,
331
+ years: parseYearsOption(options.years ?? "1"),
332
+ },
333
+ env: context.env,
334
+ fetch: context.fetch,
335
+ });
336
+ if (options.json) {
337
+ stdout.write(`${JSON.stringify(response.data, null, 2)}\n`);
338
+ return;
339
+ }
340
+ stdout.write(`${formatBetaHuman(response.data, theme)}\n`);
341
+ }
342
+ export async function runHoldingVarCommand(portfolioId, options, context = {}) {
343
+ const stdout = context.io?.stdout ?? process.stdout;
344
+ const theme = createTerminalTheme(stdout, context.env ?? process.env);
345
+ const response = await invokeCliCapability({
346
+ capability: "holdings.var",
347
+ input: {
348
+ portfolio_id: portfolioId,
349
+ years: parsePositiveIntegerOption(options.years ?? "1"),
350
+ confidence_pct: parseNumberOption(options.confidence ?? "95", "confidence"),
351
+ },
352
+ env: context.env,
353
+ fetch: context.fetch,
354
+ });
355
+ if (options.json) {
356
+ stdout.write(`${JSON.stringify(response.data, null, 2)}\n`);
357
+ return;
358
+ }
359
+ stdout.write(`${formatVarHuman(response.data, theme)}\n`);
360
+ }
361
+ export function formatPortfolioListHuman(data, theme = createTerminalTheme(process.stdout), title = "Saved items") {
146
362
  const lines = [
147
- theme.label("Portfolios"),
363
+ theme.label(title),
148
364
  "",
149
365
  `${theme.label("Total:")} ${formatInteger(data.total)}`,
150
366
  ];
151
367
  if (data.portfolios.length === 0) {
152
368
  lines.push("");
153
- lines.push("No saved portfolios.");
369
+ lines.push(`No ${title.toLowerCase()} saved.`);
154
370
  return lines.join("\n");
155
371
  }
156
372
  for (const portfolio of data.portfolios) {
@@ -163,17 +379,17 @@ export function formatPortfolioListHuman(data, theme = createTerminalTheme(proce
163
379
  }
164
380
  return lines.join("\n");
165
381
  }
166
- export function formatPortfolioGetHuman(data, theme = createTerminalTheme(process.stdout)) {
382
+ export function formatPortfolioGetHuman(data, theme = createTerminalTheme(process.stdout), title = "Saved item") {
167
383
  const { portfolio } = data;
168
384
  const lines = [
169
- theme.label("Portfolio"),
385
+ theme.label(title),
170
386
  "",
171
387
  `${theme.bold(String(portfolio.id))} ${theme.dim("·")} ${portfolio.name}`,
172
388
  `${theme.label("Assets:")} ${formatInteger(portfolio.symbols.length)}`,
173
389
  ];
174
390
  if (portfolio.symbols.length === 0) {
175
391
  lines.push("");
176
- lines.push("No assets in this portfolio.");
392
+ lines.push("No assets saved.");
177
393
  return lines.join("\n");
178
394
  }
179
395
  lines.push("");
@@ -193,21 +409,24 @@ export function formatPortfolioGetHuman(data, theme = createTerminalTheme(proces
193
409
  return lines.join("\n");
194
410
  }
195
411
  export function formatPortfolioMutationHuman(data, theme = createTerminalTheme(process.stdout)) {
412
+ const label = formatPortfolioKind(data.portfolio);
196
413
  const lines = [
197
414
  data.ok
198
- ? theme.success(theme.bold("Portfolio"))
199
- : theme.danger(theme.bold("Portfolio mutation failed")),
415
+ ? theme.success(theme.bold(label))
416
+ : theme.danger(theme.bold("Mutation failed")),
200
417
  "",
201
418
  data.summary_markdown,
202
419
  "",
203
- `${theme.bold(String(data.portfolio.id))} ${theme.dim("·")} ${data.portfolio.name || "Portfolio"}`,
420
+ `${theme.bold(String(data.portfolio.id))} ${theme.dim("·")} ${data.portfolio.name || label}`,
204
421
  `${theme.label("Assets:")} ${formatInteger(data.portfolio.symbols.length)}`,
205
422
  ];
206
423
  if (data.portfolio.symbols.length > 0) {
207
424
  lines.push("");
208
425
  lines.push(theme.label("Holdings"));
209
426
  for (const symbol of data.portfolio.symbols) {
210
- lines.push(` - ${symbol}`);
427
+ const weight = data.portfolio.weights?.[symbol];
428
+ const suffix = weight === undefined ? "" : `: ${formatNumber(weight)}%`;
429
+ lines.push(` - ${symbol}${suffix}`);
211
430
  }
212
431
  }
213
432
  return lines.join("\n");
@@ -215,24 +434,113 @@ export function formatPortfolioMutationHuman(data, theme = createTerminalTheme(p
215
434
  function parsePortfolioIdArgument(value) {
216
435
  const normalized = value.trim();
217
436
  if (!/^\d+$/.test(normalized)) {
218
- throw createCliValidationError("Portfolio id must be a positive integer.");
437
+ throw createCliValidationError("Id must be a positive integer.");
219
438
  }
220
439
  const portfolioId = Number(normalized);
221
440
  if (!Number.isSafeInteger(portfolioId) || portfolioId <= 0) {
222
- throw createCliValidationError("Portfolio id must be a positive integer.");
441
+ throw createCliValidationError("Id must be a positive integer.");
223
442
  }
224
443
  return portfolioId;
225
444
  }
226
445
  function parsePortfolioNameArgument(value) {
227
446
  const normalized = value.trim();
228
447
  if (!normalized) {
229
- throw createCliValidationError("Portfolio name cannot be empty.");
448
+ throw createCliValidationError("Name cannot be empty.");
230
449
  }
231
450
  if (normalized.length > 100) {
232
- throw createCliValidationError("Portfolio name cannot exceed 100 characters.");
451
+ throw createCliValidationError("Name cannot exceed 100 characters.");
233
452
  }
234
453
  return normalized;
235
454
  }
455
+ function parseHoldingModeOption(value) {
456
+ const normalized = value.trim().toLowerCase();
457
+ if (normalized === "target" || normalized === "target-weight") {
458
+ return "TARGET_WEIGHT";
459
+ }
460
+ if (normalized === "position" || normalized === "positions") {
461
+ return "POSITION";
462
+ }
463
+ throw createCliValidationError("Holding mode must be target or position.");
464
+ }
465
+ function collectStringOption(value, previous) {
466
+ previous.push(value);
467
+ return previous;
468
+ }
469
+ function parseTargetArguments(values) {
470
+ if (values.length === 0) {
471
+ return [];
472
+ }
473
+ const targets = values.map(parseTargetArgument);
474
+ const tickers = targets.map(target => target.ticker);
475
+ if (new Set(tickers).size !== tickers.length) {
476
+ throw createCliValidationError("Do not send duplicated tickers.");
477
+ }
478
+ return targets;
479
+ }
480
+ function parseTargetArgument(value) {
481
+ const normalized = value.trim();
482
+ if (!normalized) {
483
+ throw createCliValidationError("Target input cannot be empty.");
484
+ }
485
+ const parts = normalized.split(":");
486
+ if (parts.length !== 2) {
487
+ throw createCliValidationError(`Invalid target input "${value}". Use TICKER:WEIGHT_PCT.`);
488
+ }
489
+ const [rawTicker, rawWeight] = parts;
490
+ const ticker = rawTicker?.trim().toUpperCase();
491
+ if (!ticker) {
492
+ throw createCliValidationError(`Invalid target input "${value}". Use TICKER:WEIGHT_PCT.`);
493
+ }
494
+ const weight_pct = Number(rawWeight?.trim());
495
+ if (!Number.isFinite(weight_pct) || weight_pct < 0 || weight_pct > 100) {
496
+ throw createCliValidationError(`Invalid target weight in "${value}". Use a number from 0 to 100.`);
497
+ }
498
+ return { ticker, weight_pct };
499
+ }
500
+ function parsePositionArguments(values) {
501
+ if (values.length === 0) {
502
+ return [];
503
+ }
504
+ const positions = values.map(parsePositionArgument);
505
+ const tickers = positions.map(position => position.ticker);
506
+ if (new Set(tickers).size !== tickers.length) {
507
+ throw createCliValidationError("Do not send duplicated tickers.");
508
+ }
509
+ return positions;
510
+ }
511
+ function parsePositionArgument(value) {
512
+ const normalized = value.trim();
513
+ if (!normalized) {
514
+ throw createCliValidationError("Position input cannot be empty.");
515
+ }
516
+ const parts = normalized.split(":");
517
+ if (parts.length !== 2 && parts.length !== 3) {
518
+ throw createCliValidationError(`Invalid position input "${value}". Use TICKER:QUANTITY.`);
519
+ }
520
+ const rawTicker = parts[0];
521
+ const rawQuantity = parts.length === 2
522
+ ? parts[1]
523
+ : parsePositionMode(value, parts[1], parts[2]);
524
+ const ticker = rawTicker?.trim().toUpperCase();
525
+ if (!ticker) {
526
+ throw createCliValidationError(`Invalid position input "${value}". Use TICKER:QUANTITY.`);
527
+ }
528
+ const quantity = Number(rawQuantity?.trim());
529
+ if (!Number.isFinite(quantity) || quantity <= 0) {
530
+ throw createCliValidationError(`Invalid position quantity in "${value}". Use a number greater than 0.`);
531
+ }
532
+ return { ticker, quantity };
533
+ }
534
+ function parsePositionMode(originalValue, rawMode, rawQuantity) {
535
+ const mode = rawMode?.trim().toLowerCase();
536
+ if (mode === "qty" || mode === "quantity") {
537
+ return rawQuantity;
538
+ }
539
+ if (mode === "value" || mode === "amount") {
540
+ throw createCliValidationError("Position values are not supported in the public CLI. Use TICKER:QUANTITY.");
541
+ }
542
+ throw createCliValidationError(`Invalid position input "${originalValue}". Use TICKER:QUANTITY.`);
543
+ }
236
544
  function parseSymbolArguments(values) {
237
545
  const symbols = values.map(symbol => symbol.trim().toUpperCase());
238
546
  if (symbols.length === 0 || symbols.some(symbol => symbol.length === 0)) {
@@ -252,6 +560,85 @@ function dedupeSymbols(symbols) {
252
560
  }
253
561
  return deduped;
254
562
  }
563
+ function parsePositiveIntegerOption(value) {
564
+ const normalized = value.trim();
565
+ if (!/^\d+$/.test(normalized)) {
566
+ throw createCliValidationError("Value must be a positive integer.");
567
+ }
568
+ const parsed = Number(normalized);
569
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
570
+ throw createCliValidationError("Value must be a positive integer.");
571
+ }
572
+ return parsed;
573
+ }
574
+ function parseYearsOption(value) {
575
+ const parsed = parsePositiveIntegerOption(value);
576
+ if (![1, 3, 5].includes(parsed)) {
577
+ throw createCliValidationError("Years must be one of: 1, 3, 5.");
578
+ }
579
+ return parsed;
580
+ }
581
+ export function resolveHistoricalReturnDateRange(options, now = new Date()) {
582
+ const from = options.from?.trim();
583
+ const to = options.to?.trim();
584
+ const period = options.period?.trim().toLowerCase();
585
+ const hasExplicitRange = Boolean(from || to);
586
+ if (period && hasExplicitRange) {
587
+ throw createCliValidationError("Use either --period or both --from and --to, not both.");
588
+ }
589
+ if (period) {
590
+ return resolvePeriodDateRange(period, now);
591
+ }
592
+ if (from && to) {
593
+ return {
594
+ start_date: from,
595
+ end_date: to,
596
+ };
597
+ }
598
+ throw createCliValidationError("Use --period, or provide both --from and --to.");
599
+ }
600
+ function resolvePeriodDateRange(period, now) {
601
+ const match = /^(\d+)([my])$/.exec(period);
602
+ if (!match) {
603
+ throw createCliValidationError("Period must use the form <number>m or <number>y, for example 6m or 1y.");
604
+ }
605
+ const amount = Number(match[1]);
606
+ const unit = match[2];
607
+ if (!Number.isSafeInteger(amount) || amount <= 0) {
608
+ throw createCliValidationError("Period amount must be a positive integer.");
609
+ }
610
+ const endDate = toLocalCalendarDate(now);
611
+ const startDate = subtractUtcCalendarPeriod(endDate, amount, unit);
612
+ return {
613
+ start_date: formatIsoDate(startDate),
614
+ end_date: formatIsoDate(endDate),
615
+ };
616
+ }
617
+ function subtractUtcCalendarPeriod(endDate, amount, unit) {
618
+ const targetYear = unit === "y" ? endDate.getUTCFullYear() - amount : endDate.getUTCFullYear();
619
+ const targetMonth = unit === "m" ? endDate.getUTCMonth() - amount : endDate.getUTCMonth();
620
+ return createClampedUtcDate(targetYear, targetMonth, endDate.getUTCDate());
621
+ }
622
+ function createClampedUtcDate(year, monthIndex, day) {
623
+ const firstOfTargetMonth = new Date(Date.UTC(year, monthIndex, 1));
624
+ const normalizedYear = firstOfTargetMonth.getUTCFullYear();
625
+ const normalizedMonth = firstOfTargetMonth.getUTCMonth();
626
+ const lastDayOfTargetMonth = new Date(Date.UTC(normalizedYear, normalizedMonth + 1, 0)).getUTCDate();
627
+ return new Date(Date.UTC(normalizedYear, normalizedMonth, Math.min(day, lastDayOfTargetMonth)));
628
+ }
629
+ function toLocalCalendarDate(value) {
630
+ return new Date(Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()));
631
+ }
632
+ function formatIsoDate(value) {
633
+ return value.toISOString().slice(0, 10);
634
+ }
635
+ function parseNumberOption(value, fieldName) {
636
+ const parsed = Number(value.trim());
637
+ if (!Number.isFinite(parsed)) {
638
+ throw createCliValidationError(`${fieldName} must be a number.`);
639
+ }
640
+ return parsed;
641
+ }
255
642
  function writeMutationResponse(data, json, stdout, theme) {
256
643
  if (!data.ok) {
257
644
  if (!json) {
@@ -283,9 +670,124 @@ function formatWeightsInline(portfolio) {
283
670
  })
284
671
  .join(", ");
285
672
  }
673
+ function formatPortfolioKind(portfolio) {
674
+ if (portfolio.kind === "WATCHLIST") {
675
+ return "Watchlist";
676
+ }
677
+ if (portfolio.kind === "HOLDING") {
678
+ return "Holding";
679
+ }
680
+ return "Saved item";
681
+ }
286
682
  function createCountSuffix(count) {
287
683
  return count === 1 ? "(1 asset)" : `(${count} assets)`;
288
684
  }
685
+ export function formatHistoricalReturnHuman(data, theme = createTerminalTheme(process.stdout)) {
686
+ const lines = [
687
+ theme.label("Historical return"),
688
+ "",
689
+ formatSourceLabel(data, theme),
690
+ `${theme.label("Period:")} ${data.effective_start_date} -> ${data.effective_end_date}`,
691
+ `${theme.label("Requested:")} ${data.requested_start_date} -> ${data.requested_end_date}`,
692
+ "",
693
+ `${theme.label("Holding return:")} ${formatPercentValue(data.total_return)}`,
694
+ `${theme.label("Annualized return:")} ${formatPercentValue(data.annualized_return)}`,
695
+ `${theme.label("Max drawdown:")} ${formatPercentValue(data.max_drawdown)}`,
696
+ `${theme.label("Daily volatility:")} ${formatPercentValue(data.daily_volatility)}`,
697
+ `${theme.label("Annualized volatility:")} ${formatPercentValue(data.annualized_volatility)}`,
698
+ `${theme.label("Sharpe ratio:")} ${formatNumber(data.sharpe_ratio)}`,
699
+ "",
700
+ theme.label("Benchmarks"),
701
+ ` IBOV: ${formatPercentValue(data.ibov_return)}`,
702
+ ` CDI: ${formatPercentValue(data.cdi_return)}`,
703
+ ` IPCA: ${formatPercentValue(data.ipca_return)}`,
704
+ "",
705
+ theme.label("Holdings"),
706
+ ...data.holdings.map(item => ` - ${item.ticker}: weight=${formatPercentDirect(item.weight_pct)}, return=${formatPercentValue(item.total_return)}, contribution=${formatPercentValue(item.contribution)}`),
707
+ ];
708
+ pushNotes(lines, "Assumptions", data.assumptions, theme);
709
+ pushNotes(lines, "Warnings", data.warnings, theme);
710
+ return lines.join("\n");
711
+ }
712
+ export function formatBetaHuman(data, theme = createTerminalTheme(process.stdout)) {
713
+ const lines = [
714
+ theme.label("Holding beta"),
715
+ "",
716
+ formatSourceLabel(data, theme),
717
+ `${theme.label("Benchmark:")} ${data.benchmark}`,
718
+ `${theme.label("Lookback:")} ${data.lookback_years}Y`,
719
+ "",
720
+ `${theme.label("Beta:")} ${formatNumber(data.beta)}`,
721
+ `${theme.label("Correlation:")} ${formatNumber(data.correlation)}`,
722
+ `${theme.label("Daily volatility:")} ${formatPercentValue(data.daily_volatility)}`,
723
+ `${theme.label("Annualized volatility:")} ${formatPercentValue(data.annualized_volatility)}`,
724
+ `${theme.label("Long exposure:")} ${formatPercentDirect(data.long_exposure_pct)}`,
725
+ `${theme.label("Short exposure:")} ${formatPercentDirect(data.short_exposure_pct)}`,
726
+ `${theme.label("Net exposure:")} ${formatPercentDirect(data.total_weight_pct)}`,
727
+ "",
728
+ theme.label("Holdings"),
729
+ ...data.holdings.map(item => ` - ${item.ticker}: weight=${formatPercentDirect(item.weight_pct)}, beta=${formatNullableNumber(item.beta)}, weighted=${formatNullableNumber(item.weighted_beta)}, corr=${formatNullableNumber(item.correlation)}`),
730
+ ];
731
+ pushNotes(lines, "Assumptions", data.assumptions, theme);
732
+ pushNotes(lines, "Warnings", data.warnings, theme);
733
+ return lines.join("\n");
734
+ }
735
+ export function formatVarHuman(data, theme = createTerminalTheme(process.stdout)) {
736
+ const lines = [
737
+ theme.label("Holding VaR"),
738
+ "",
739
+ formatSourceLabel(data, theme),
740
+ `${theme.label("Lookback:")} ${data.lookback_years}Y`,
741
+ `${theme.label("Confidence:")} ${formatPercentDirect(data.confidence_pct)}`,
742
+ `${theme.label("Horizon:")} ${data.time_horizon}`,
743
+ "",
744
+ `${theme.label("VaR:")} ${formatPercentValue(data.var)}`,
745
+ `${theme.label("Long exposure:")} ${formatPercentDirect(data.long_exposure_pct)}`,
746
+ `${theme.label("Short exposure:")} ${formatPercentDirect(data.short_exposure_pct)}`,
747
+ `${theme.label("Net exposure:")} ${formatPercentDirect(data.total_weight_pct)}`,
748
+ "",
749
+ theme.label("Holdings"),
750
+ ...data.holdings.map(item => ` - ${item.ticker}: weight=${formatPercentDirect(item.weight_pct)}`),
751
+ "",
752
+ `${theme.label("Histogram bins:")} ${formatInteger(data.histogram_data.length)}`,
753
+ ];
754
+ pushNotes(lines, "Assumptions", data.assumptions, theme);
755
+ pushNotes(lines, "Warnings", data.warnings, theme);
756
+ return lines.join("\n");
757
+ }
758
+ function formatSourceLabel(data, theme) {
759
+ const id = data.portfolio_id === null ? "n/a" : String(data.portfolio_id);
760
+ const name = data.portfolio_name ? ` · ${data.portfolio_name}` : "";
761
+ return `${theme.bold(id)}${theme.dim(name)}`;
762
+ }
763
+ function pushNotes(lines, title, items, theme) {
764
+ if (items.length === 0) {
765
+ return;
766
+ }
767
+ lines.push("");
768
+ lines.push(theme.label(title));
769
+ for (const item of items) {
770
+ lines.push(` - ${item}`);
771
+ }
772
+ }
773
+ function formatNullableNumber(value) {
774
+ if (value === null) {
775
+ return "n/a";
776
+ }
777
+ return formatNumber(value);
778
+ }
779
+ function formatPercentValue(value) {
780
+ return `${new Intl.NumberFormat("en-US", {
781
+ maximumFractionDigits: 2,
782
+ minimumFractionDigits: 2,
783
+ }).format(value * 100)}%`;
784
+ }
785
+ function formatPercentDirect(value) {
786
+ return `${new Intl.NumberFormat("en-US", {
787
+ maximumFractionDigits: 2,
788
+ minimumFractionDigits: 2,
789
+ }).format(value)}%`;
790
+ }
289
791
  function formatInteger(value) {
290
792
  return new Intl.NumberFormat("en-US", {
291
793
  maximumFractionDigits: 0,