@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.
- package/README.md +18 -11
- package/dist/cli/index.d.ts +4 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +15 -12
- package/dist/commands/auth.d.ts +18 -0
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +49 -0
- package/dist/commands/market.d.ts +1 -0
- package/dist/commands/market.d.ts.map +1 -1
- package/dist/commands/market.js +17 -1
- package/dist/commands/portfolios.d.ts +148 -8
- package/dist/commands/portfolios.d.ts.map +1 -1
- package/dist/commands/portfolios.js +557 -55
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/vendor/core/capabilities/index.d.ts +0 -1
- package/dist/vendor/core/capabilities/index.d.ts.map +1 -1
- package/dist/vendor/core/capabilities/index.js +0 -1
- package/dist/vendor/core/capabilities/market.d.ts +9 -1
- package/dist/vendor/core/capabilities/market.d.ts.map +1 -1
- package/dist/vendor/core/capabilities/market.js +10 -0
- package/dist/vendor/core/capabilities/portfolios.d.ts +452 -56
- package/dist/vendor/core/capabilities/portfolios.d.ts.map +1 -1
- package/dist/vendor/core/capabilities/portfolios.js +434 -116
- package/dist/vendor/core/capabilities/registry.d.ts +714 -276
- package/dist/vendor/core/capabilities/registry.d.ts.map +1 -1
- package/dist/vendor/core/capabilities/registry.js +0 -2
- package/dist/vendor/core/capabilities/types.d.ts +1 -1
- package/dist/vendor/core/capabilities/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/skills/quantbrasil/SKILL.md +9 -6
- package/skills/quantbrasil/references/cli.md +25 -25
- package/skills/quantbrasil/references/costs.md +4 -4
- package/skills/quantbrasil/references/errors.md +6 -5
- package/skills/quantbrasil/references/portfolios.md +103 -0
- package/skills/quantbrasil/references/unsupported.md +5 -2
- package/skills/quantbrasil/references/workflows.md +38 -26
- package/dist/commands/analytics.d.ts +0 -131
- package/dist/commands/analytics.d.ts.map +0 -1
- package/dist/commands/analytics.js +0 -291
- package/dist/vendor/core/capabilities/analytics.d.ts +0 -187
- package/dist/vendor/core/capabilities/analytics.d.ts.map +0 -1
- 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
|
|
6
|
-
.command("
|
|
7
|
-
.description("Manage
|
|
8
|
-
|
|
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
|
|
10
|
+
.description("List watchlists for authenticated user")
|
|
11
11
|
.option("--json", "Show JSON output")
|
|
12
12
|
.action(async (options) => {
|
|
13
|
-
await
|
|
13
|
+
await runWatchlistListCommand(options, context);
|
|
14
14
|
});
|
|
15
|
-
|
|
15
|
+
watchlistsCommand
|
|
16
16
|
.command("get")
|
|
17
|
-
.description("Get one
|
|
18
|
-
.argument("<
|
|
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
|
|
21
|
+
await runWatchlistGetCommand(portfolioId, options, context);
|
|
22
22
|
});
|
|
23
|
-
|
|
23
|
+
watchlistsCommand
|
|
24
24
|
.command("create")
|
|
25
|
-
.description("Create a new
|
|
26
|
-
.argument("<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
|
|
29
|
+
await runWatchlistCreateCommand(name, options, context);
|
|
30
30
|
});
|
|
31
|
-
|
|
31
|
+
watchlistsCommand
|
|
32
32
|
.command("rename")
|
|
33
|
-
.description("Rename an existing
|
|
34
|
-
.argument("<
|
|
35
|
-
.argument("<name>", "New
|
|
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
|
|
38
|
+
await runWatchlistRenameCommand(portfolioId, name, options, context);
|
|
39
39
|
});
|
|
40
|
-
|
|
40
|
+
watchlistsCommand
|
|
41
41
|
.command("add-assets")
|
|
42
|
-
.description("Add one or more monitored tickers to a
|
|
43
|
-
.argument("<
|
|
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
|
|
47
|
+
await runWatchlistAddAssetsCommand(portfolioId, symbols, options, context);
|
|
48
48
|
});
|
|
49
|
-
|
|
49
|
+
watchlistsCommand
|
|
50
50
|
.command("remove-assets")
|
|
51
|
-
.description("Remove one or more monitored tickers from a
|
|
52
|
-
.argument("<
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
199
|
-
: theme.danger(theme.bold("
|
|
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 ||
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
448
|
+
throw createCliValidationError("Name cannot be empty.");
|
|
230
449
|
}
|
|
231
450
|
if (normalized.length > 100) {
|
|
232
|
-
throw createCliValidationError("
|
|
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,
|