@quantbrasil/cli 0.1.0-beta.13 → 0.1.0-beta.15
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 +5 -0
- package/dist/cli/client.d.ts.map +1 -1
- package/dist/cli/client.js +27 -1
- package/dist/cli/errors.d.ts +3 -0
- package/dist/cli/errors.d.ts.map +1 -1
- package/dist/cli/errors.js +8 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +16 -0
- package/dist/commands/backtests.d.ts +183 -0
- package/dist/commands/backtests.d.ts.map +1 -0
- package/dist/commands/backtests.js +525 -0
- package/dist/commands/news.d.ts +48 -0
- package/dist/commands/news.d.ts.map +1 -0
- package/dist/commands/news.js +85 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/vendor/core/capabilities/backtests.d.ts +207 -0
- package/dist/vendor/core/capabilities/backtests.d.ts.map +1 -0
- package/dist/vendor/core/capabilities/backtests.js +239 -0
- package/dist/vendor/core/capabilities/index.d.ts +2 -0
- package/dist/vendor/core/capabilities/index.d.ts.map +1 -1
- package/dist/vendor/core/capabilities/index.js +2 -0
- package/dist/vendor/core/capabilities/news.d.ts +54 -0
- package/dist/vendor/core/capabilities/news.d.ts.map +1 -0
- package/dist/vendor/core/capabilities/news.js +65 -0
- package/dist/vendor/core/capabilities/registry.d.ts +512 -0
- package/dist/vendor/core/capabilities/registry.d.ts.map +1 -1
- package/dist/vendor/core/capabilities/registry.js +4 -0
- package/dist/vendor/core/capabilities/types.d.ts +1 -1
- package/dist/vendor/core/capabilities/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/quantbrasil/SKILL.md +8 -4
- package/skills/quantbrasil/references/backtests.md +173 -0
- package/skills/quantbrasil/references/cli.md +45 -0
- package/skills/quantbrasil/references/costs.md +6 -1
- package/skills/quantbrasil/references/quality-eval-queries.json +30 -0
- package/skills/quantbrasil/references/unsupported.md +5 -0
- package/skills/quantbrasil/references/workflows.md +46 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { invokeCliCapability } from "../cli/client.js";
|
|
2
|
+
import { createCliValidationError } from "../cli/errors.js";
|
|
3
|
+
import { createTerminalTheme } from "../cli/terminal.js";
|
|
4
|
+
export function registerBacktestsCommands(program, context = {}) {
|
|
5
|
+
const backtestsCommand = program
|
|
6
|
+
.command("backtests")
|
|
7
|
+
.description("Descobre estratégias e executa backtests salvos");
|
|
8
|
+
backtestsCommand
|
|
9
|
+
.command("strategies")
|
|
10
|
+
.description("Lista estratégias de backtest disponíveis")
|
|
11
|
+
.option("--json", "Exibe saída JSON")
|
|
12
|
+
.action(async (options) => {
|
|
13
|
+
await runBacktestsStrategiesCommand(options, context);
|
|
14
|
+
});
|
|
15
|
+
backtestsCommand
|
|
16
|
+
.command("info")
|
|
17
|
+
.description("Mostra a motivação, defaults e parâmetros de uma estratégia")
|
|
18
|
+
.argument("<strategy-id>", "ID da estratégia")
|
|
19
|
+
.option("--json", "Exibe saída JSON")
|
|
20
|
+
.action(async (strategyId, options) => {
|
|
21
|
+
await runBacktestsInfoCommand(strategyId, options, context);
|
|
22
|
+
});
|
|
23
|
+
backtestsCommand
|
|
24
|
+
.command("get")
|
|
25
|
+
.description("Mostra um backtest salvo por ID")
|
|
26
|
+
.argument("<backtest-id>", "ID do backtest")
|
|
27
|
+
.option("--json", "Exibe saída JSON")
|
|
28
|
+
.action(async (backtestId, options) => {
|
|
29
|
+
await runBacktestsGetCommand(backtestId, options, context);
|
|
30
|
+
});
|
|
31
|
+
backtestsCommand
|
|
32
|
+
.command("run")
|
|
33
|
+
.description("Executa e salva um backtest para o usuário autenticado")
|
|
34
|
+
.argument("<strategy-id>", "ID da estratégia")
|
|
35
|
+
.argument("<ticker>", "Ticker do ativo")
|
|
36
|
+
.option("--timeframe <timeframe>", "Timeframe do backtest")
|
|
37
|
+
.option("--from <date>", "Data inicial em AAAA-MM-DD")
|
|
38
|
+
.option("--to <date>", "Data final em AAAA-MM-DD")
|
|
39
|
+
.option("--capital <number>", "Capital inicial")
|
|
40
|
+
.option("--max-risk <number>", "Risco máximo")
|
|
41
|
+
.option("--contracts <number>", "Número de contratos", "5")
|
|
42
|
+
.option("--param <key=value>", "Parâmetro da estratégia; pode ser usado várias vezes", collectRepeatedOption, [])
|
|
43
|
+
.option("--params-json <json>", "Objeto JSON com parâmetros da estratégia")
|
|
44
|
+
.option("--json", "Exibe saída JSON")
|
|
45
|
+
.action(async (strategyId, ticker, options) => {
|
|
46
|
+
await runBacktestsRunCommand(strategyId, ticker, options, context);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export async function runBacktestsStrategiesCommand(options, context = {}) {
|
|
50
|
+
const stdout = context.io?.stdout ?? process.stdout;
|
|
51
|
+
const theme = createTerminalTheme(stdout, context.env ?? process.env);
|
|
52
|
+
const response = await invokeCliCapability({
|
|
53
|
+
capability: "backtests.strategies",
|
|
54
|
+
env: context.env,
|
|
55
|
+
fetch: context.fetch,
|
|
56
|
+
});
|
|
57
|
+
if (options.json) {
|
|
58
|
+
stdout.write(`${JSON.stringify(response.data, null, 2)}\n`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
stdout.write(`${formatBacktestStrategiesHuman(response.data, theme)}\n`);
|
|
62
|
+
}
|
|
63
|
+
export async function runBacktestsInfoCommand(strategyId, options, context = {}) {
|
|
64
|
+
const stdout = context.io?.stdout ?? process.stdout;
|
|
65
|
+
const theme = createTerminalTheme(stdout, context.env ?? process.env);
|
|
66
|
+
const response = await invokeCliCapability({
|
|
67
|
+
capability: "backtests.info",
|
|
68
|
+
input: buildBacktestsInfoInput(strategyId),
|
|
69
|
+
env: context.env,
|
|
70
|
+
fetch: context.fetch,
|
|
71
|
+
});
|
|
72
|
+
if (options.json) {
|
|
73
|
+
stdout.write(`${JSON.stringify(response.data, null, 2)}\n`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
stdout.write(`${formatBacktestStrategyHuman(response.data, theme)}\n`);
|
|
77
|
+
}
|
|
78
|
+
export async function runBacktestsGetCommand(backtestId, options, context = {}) {
|
|
79
|
+
const stdout = context.io?.stdout ?? process.stdout;
|
|
80
|
+
const theme = createTerminalTheme(stdout, context.env ?? process.env);
|
|
81
|
+
const response = await invokeCliCapability({
|
|
82
|
+
capability: "backtests.get",
|
|
83
|
+
input: buildBacktestsGetInput(backtestId),
|
|
84
|
+
env: context.env,
|
|
85
|
+
fetch: context.fetch,
|
|
86
|
+
});
|
|
87
|
+
if (options.json) {
|
|
88
|
+
stdout.write(`${JSON.stringify(response.data, null, 2)}\n`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
stdout.write(`${formatBacktestGetHuman(response.data, theme)}\n`);
|
|
92
|
+
}
|
|
93
|
+
export async function runBacktestsRunCommand(strategyId, ticker, options, context = {}) {
|
|
94
|
+
const stdout = context.io?.stdout ?? process.stdout;
|
|
95
|
+
const theme = createTerminalTheme(stdout, context.env ?? process.env);
|
|
96
|
+
const response = await invokeCliCapability({
|
|
97
|
+
capability: "backtests.run",
|
|
98
|
+
input: buildBacktestsRunInput(strategyId, ticker, options),
|
|
99
|
+
env: context.env,
|
|
100
|
+
fetch: context.fetch,
|
|
101
|
+
});
|
|
102
|
+
if (options.json) {
|
|
103
|
+
stdout.write(`${JSON.stringify(response.data, null, 2)}\n`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
stdout.write(`${formatBacktestRunHuman(response.data, theme)}\n`);
|
|
107
|
+
}
|
|
108
|
+
export function buildBacktestsInfoInput(strategyId) {
|
|
109
|
+
return {
|
|
110
|
+
strategy_id: normalizeStrategyId(strategyId),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function buildBacktestsGetInput(backtestId) {
|
|
114
|
+
return {
|
|
115
|
+
backtest_id: parsePositiveIntegerArgument(backtestId, "ID do backtest"),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function buildBacktestsRunInput(strategyId, ticker, options) {
|
|
119
|
+
const input = {
|
|
120
|
+
strategy_id: normalizeStrategyId(strategyId),
|
|
121
|
+
ticker: normalizeTicker(ticker),
|
|
122
|
+
};
|
|
123
|
+
if (options.timeframe !== undefined) {
|
|
124
|
+
input.timeframe_id = options.timeframe.trim().toUpperCase();
|
|
125
|
+
}
|
|
126
|
+
if (options.from !== undefined) {
|
|
127
|
+
input.start_date = options.from.trim();
|
|
128
|
+
}
|
|
129
|
+
if (options.to !== undefined) {
|
|
130
|
+
input.end_date = options.to.trim();
|
|
131
|
+
}
|
|
132
|
+
const parameters = parseBacktestParameters(options);
|
|
133
|
+
if (parameters !== undefined) {
|
|
134
|
+
input.parameters = parameters;
|
|
135
|
+
}
|
|
136
|
+
if (options.capital !== undefined) {
|
|
137
|
+
input.capital = parseNumberLikeOption(options.capital);
|
|
138
|
+
}
|
|
139
|
+
if (options.maxRisk !== undefined) {
|
|
140
|
+
input.max_risk = parseNumberLikeOption(options.maxRisk);
|
|
141
|
+
}
|
|
142
|
+
if (options.contracts !== undefined) {
|
|
143
|
+
input.num_contracts = parseNumberLikeOption(options.contracts);
|
|
144
|
+
}
|
|
145
|
+
return input;
|
|
146
|
+
}
|
|
147
|
+
export function formatBacktestStrategiesHuman(data, theme = createTerminalTheme(process.stdout)) {
|
|
148
|
+
const lines = [
|
|
149
|
+
theme.label("Estratégias de backtest"),
|
|
150
|
+
"",
|
|
151
|
+
`${theme.label("Total:")} ${formatInteger(data.total)}`,
|
|
152
|
+
`${theme.label("Ajuda:")} quantbrasil backtests info <strategy-id>`,
|
|
153
|
+
];
|
|
154
|
+
appendStrategyGroup(lines, theme, "Compra", data.strategies.filter(strategy => strategy.side === "long"));
|
|
155
|
+
appendStrategyGroup(lines, theme, "Venda", data.strategies.filter(strategy => strategy.side === "short"));
|
|
156
|
+
return lines.join("\n");
|
|
157
|
+
}
|
|
158
|
+
export function formatBacktestStrategyHuman(data, theme = createTerminalTheme(process.stdout)) {
|
|
159
|
+
const strategy = data.strategy;
|
|
160
|
+
const lines = [
|
|
161
|
+
theme.label("Estratégia de backtest"),
|
|
162
|
+
"",
|
|
163
|
+
`${theme.bold(strategy.label)} ${theme.dim(`(${strategy.id})`)}`,
|
|
164
|
+
strategy.description,
|
|
165
|
+
"",
|
|
166
|
+
`${theme.label("Quando usar:")} ${strategy.selection_guidance}`,
|
|
167
|
+
`${theme.label("Guia de timeframe:")} ${strategy.timeframe_guidance}`,
|
|
168
|
+
"",
|
|
169
|
+
`${theme.label("Família:")} ${strategy.family}`,
|
|
170
|
+
`${theme.label("Lado:")} ${formatStrategySide(strategy.side)}`,
|
|
171
|
+
`${theme.label("Timeframes:")} ${formatStrategyTimeframes(strategy)}`,
|
|
172
|
+
];
|
|
173
|
+
const flags = formatStrategyFlags(strategy);
|
|
174
|
+
if (flags.length > 0) {
|
|
175
|
+
lines.push(`${theme.label("Observações:")} ${flags.join("; ")}`);
|
|
176
|
+
}
|
|
177
|
+
lines.push("");
|
|
178
|
+
lines.push(theme.label("Parâmetros"));
|
|
179
|
+
if (strategy.parameters.length === 0) {
|
|
180
|
+
lines.push(" nenhum parâmetro configurável");
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
for (const parameter of strategy.parameters) {
|
|
184
|
+
lines.push(` ${formatBacktestParameter(parameter)}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push(theme.label("Exemplo"));
|
|
189
|
+
lines.push(` ${formatStrategyRunUsage(strategy)}`);
|
|
190
|
+
return lines.join("\n");
|
|
191
|
+
}
|
|
192
|
+
export function formatBacktestRunHuman(data, theme = createTerminalTheme(process.stdout)) {
|
|
193
|
+
const backtest = data.backtest;
|
|
194
|
+
const lines = [
|
|
195
|
+
theme.label("Backtest"),
|
|
196
|
+
"",
|
|
197
|
+
`${theme.bold(backtest.ticker)} ${theme.dim(`- ${backtest.strategy_id}`)}`,
|
|
198
|
+
`${theme.label("ID:")} ${backtest.id ?? "não salvo"}`,
|
|
199
|
+
`${theme.label("Setup:")} ${backtest.setup_id}`,
|
|
200
|
+
`${theme.label("Timeframe:")} ${backtest.timeframe_id}`,
|
|
201
|
+
`${theme.label("Período:")} ${formatDatePtBr(backtest.start_date)} a ${formatDatePtBr(backtest.end_date)}`,
|
|
202
|
+
`${theme.label("Capital inicial:")} ${formatCurrency(backtest.initial_capital)}`,
|
|
203
|
+
"",
|
|
204
|
+
`${theme.label("Retorno total:")} ${formatPercentage(backtest.pct_profit)}`,
|
|
205
|
+
`${theme.label("Drawdown:")} ${formatPercentage(backtest.drawdown)}`,
|
|
206
|
+
`${theme.label("EV:")} ${formatPercentage(backtest.ev)}`,
|
|
207
|
+
`${theme.label("Taxa de acerto:")} ${formatPercentage(backtest.pct_gains)}`,
|
|
208
|
+
`${theme.label("Operações:")} ${formatInteger(backtest.num_operations)}`,
|
|
209
|
+
`${theme.label("Profit factor:")} ${formatNullableNumber(backtest.profit_factor)}`,
|
|
210
|
+
];
|
|
211
|
+
if (backtest.score !== null) {
|
|
212
|
+
lines.push(`${theme.label("Score:")} ${formatNumber(backtest.score)}`);
|
|
213
|
+
}
|
|
214
|
+
if (Object.keys(backtest.parameters).length > 0) {
|
|
215
|
+
lines.push(`${theme.label("Parâmetros:")} ${formatParameterRecord(backtest.parameters)}`);
|
|
216
|
+
}
|
|
217
|
+
if (backtest.suspicious) {
|
|
218
|
+
lines.push("");
|
|
219
|
+
lines.push(theme.warning("Resultado marcado como suspeito pelo backend."));
|
|
220
|
+
}
|
|
221
|
+
if (data.summary_markdown.trim()) {
|
|
222
|
+
lines.push("");
|
|
223
|
+
lines.push(data.summary_markdown);
|
|
224
|
+
}
|
|
225
|
+
if (backtest.trade_history.length > 0) {
|
|
226
|
+
lines.push("");
|
|
227
|
+
lines.push("Use --json para inspecionar o histórico completo de operações.");
|
|
228
|
+
}
|
|
229
|
+
return lines.join("\n");
|
|
230
|
+
}
|
|
231
|
+
export function formatBacktestGetHuman(data, theme = createTerminalTheme(process.stdout)) {
|
|
232
|
+
const backtest = data.backtest;
|
|
233
|
+
const lines = [
|
|
234
|
+
theme.label("Backtest"),
|
|
235
|
+
"",
|
|
236
|
+
`${theme.bold(backtest.ticker)} ${theme.dim(`- ${backtest.strategy_id}`)}`,
|
|
237
|
+
`${theme.label("ID:")} ${backtest.id}`,
|
|
238
|
+
`${theme.label("Origem:")} ${formatBacktestOrigin(backtest.origin)}`,
|
|
239
|
+
`${theme.label("Setup:")} ${backtest.setup_id}`,
|
|
240
|
+
`${theme.label("Timeframe:")} ${backtest.timeframe_id}`,
|
|
241
|
+
`${theme.label("Período:")} ${formatDatePtBr(backtest.start_date)} a ${formatDatePtBr(backtest.end_date)}`,
|
|
242
|
+
`${theme.label("Capital inicial:")} ${formatCurrency(backtest.initial_capital)}`,
|
|
243
|
+
"",
|
|
244
|
+
`${theme.label("Retorno total:")} ${formatPercentage(backtest.pct_profit)}`,
|
|
245
|
+
`${theme.label("Drawdown:")} ${formatPercentage(backtest.drawdown)}`,
|
|
246
|
+
`${theme.label("EV:")} ${formatPercentage(backtest.ev)}`,
|
|
247
|
+
`${theme.label("Taxa de acerto:")} ${formatPercentage(backtest.pct_gains)}`,
|
|
248
|
+
`${theme.label("Operações:")} ${formatInteger(backtest.num_operations)}`,
|
|
249
|
+
`${theme.label("Profit factor:")} ${formatNullableNumber(backtest.profit_factor)}`,
|
|
250
|
+
];
|
|
251
|
+
if (backtest.score !== null) {
|
|
252
|
+
lines.push(`${theme.label("Score:")} ${formatNumber(backtest.score)}`);
|
|
253
|
+
}
|
|
254
|
+
if (Object.keys(backtest.parameters).length > 0) {
|
|
255
|
+
lines.push(`${theme.label("Parâmetros:")} ${formatParameterRecord(backtest.parameters)}`);
|
|
256
|
+
}
|
|
257
|
+
if (backtest.suspicious) {
|
|
258
|
+
lines.push("");
|
|
259
|
+
lines.push(theme.warning("Resultado marcado como suspeito pelo backend."));
|
|
260
|
+
}
|
|
261
|
+
if (backtest.trade_history.length > 0) {
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push("Use --json para inspecionar o histórico completo de operações.");
|
|
264
|
+
}
|
|
265
|
+
return lines.join("\n");
|
|
266
|
+
}
|
|
267
|
+
function collectRepeatedOption(value, previous) {
|
|
268
|
+
return [...previous, value];
|
|
269
|
+
}
|
|
270
|
+
function parseBacktestParameters(options) {
|
|
271
|
+
let parameters;
|
|
272
|
+
if (options.paramsJson !== undefined) {
|
|
273
|
+
parameters = parseParamsJsonOption(options.paramsJson);
|
|
274
|
+
}
|
|
275
|
+
for (const rawParam of options.param ?? []) {
|
|
276
|
+
if (parameters !== undefined && !isPlainRecord(parameters)) {
|
|
277
|
+
throw createCliValidationError("--params-json deve ser um objeto JSON para combinar com --param.");
|
|
278
|
+
}
|
|
279
|
+
const parameterRecord = parameters === undefined ? {} : parameters;
|
|
280
|
+
const [rawKey, ...rawValueParts] = rawParam.split("=");
|
|
281
|
+
const rawValue = rawValueParts.join("=");
|
|
282
|
+
const key = normalizeParameterKey(rawKey ?? "");
|
|
283
|
+
if (!key || rawValueParts.length === 0) {
|
|
284
|
+
throw createCliValidationError("Cada --param deve usar o formato nome=valor.");
|
|
285
|
+
}
|
|
286
|
+
parameterRecord[key] = parseParameterValue(rawValue);
|
|
287
|
+
parameters = parameterRecord;
|
|
288
|
+
}
|
|
289
|
+
return parameters;
|
|
290
|
+
}
|
|
291
|
+
function parseParamsJsonOption(value) {
|
|
292
|
+
let parsed;
|
|
293
|
+
try {
|
|
294
|
+
parsed = JSON.parse(value);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
298
|
+
throw createCliValidationError(`Não foi possível interpretar --params-json: ${message}`);
|
|
299
|
+
}
|
|
300
|
+
if (!isPlainRecord(parsed)) {
|
|
301
|
+
return parsed;
|
|
302
|
+
}
|
|
303
|
+
const parameters = {};
|
|
304
|
+
for (const [rawKey, rawValue] of Object.entries(parsed)) {
|
|
305
|
+
const key = normalizeParameterKey(rawKey);
|
|
306
|
+
parameters[key] = rawValue;
|
|
307
|
+
}
|
|
308
|
+
return parameters;
|
|
309
|
+
}
|
|
310
|
+
function parseParameterValue(value) {
|
|
311
|
+
const normalized = value.trim();
|
|
312
|
+
if (normalized === "true") {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (normalized === "false") {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
if (/^-?(?:\d+|\d+\.\d+|\.\d+)(?:e[+-]?\d+)?$/i.test(normalized)) {
|
|
319
|
+
const parsed = Number(normalized);
|
|
320
|
+
if (Number.isFinite(parsed)) {
|
|
321
|
+
return parsed;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return normalized;
|
|
325
|
+
}
|
|
326
|
+
function parseNumberLikeOption(value) {
|
|
327
|
+
const normalized = value.trim();
|
|
328
|
+
if (/^-?(?:\d+|\d+\.\d+|\.\d+)(?:e[+-]?\d+)?$/i.test(normalized)) {
|
|
329
|
+
const parsed = Number(normalized);
|
|
330
|
+
if (Number.isFinite(parsed)) {
|
|
331
|
+
return parsed;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return normalized;
|
|
335
|
+
}
|
|
336
|
+
function normalizeStrategyId(strategyId) {
|
|
337
|
+
const normalized = strategyId.trim().toLowerCase().replaceAll("-", "_");
|
|
338
|
+
if (!normalized) {
|
|
339
|
+
throw createCliValidationError("O ID da estratégia é obrigatório.");
|
|
340
|
+
}
|
|
341
|
+
return normalized;
|
|
342
|
+
}
|
|
343
|
+
function normalizeParameterKey(parameterKey) {
|
|
344
|
+
return parameterKey
|
|
345
|
+
.trim()
|
|
346
|
+
.replaceAll("-", "_")
|
|
347
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
348
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
349
|
+
.toLowerCase();
|
|
350
|
+
}
|
|
351
|
+
function normalizeTicker(ticker) {
|
|
352
|
+
const normalized = ticker.trim().toUpperCase();
|
|
353
|
+
if (!normalized) {
|
|
354
|
+
throw createCliValidationError("O ticker é obrigatório.");
|
|
355
|
+
}
|
|
356
|
+
return normalized;
|
|
357
|
+
}
|
|
358
|
+
function parsePositiveIntegerArgument(value, label) {
|
|
359
|
+
const parsed = Number(value);
|
|
360
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
361
|
+
throw createCliValidationError(`${label} deve ser um inteiro positivo.`);
|
|
362
|
+
}
|
|
363
|
+
return parsed;
|
|
364
|
+
}
|
|
365
|
+
function appendStrategyGroup(lines, theme, title, strategies) {
|
|
366
|
+
lines.push("");
|
|
367
|
+
lines.push(`${theme.label(title)} ${theme.dim(`(${formatInteger(strategies.length)})`)}`);
|
|
368
|
+
if (strategies.length === 0) {
|
|
369
|
+
lines.push(" nenhuma estratégia");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
for (const strategy of strategies) {
|
|
373
|
+
lines.push(` ${theme.bold(strategy.id)} - ${strategy.label}`);
|
|
374
|
+
lines.push(` ${theme.label("Família:")} ${strategy.family}; ${theme.label("timeframes:")} ${formatStrategyTimeframes(strategy)}`);
|
|
375
|
+
lines.push(` ${theme.label("Quando usar:")} ${formatShortGuidance(strategy.selection_guidance)}`);
|
|
376
|
+
lines.push(` ${theme.label("Parâmetros:")} ${formatInteger(strategy.parameters.length)}; ${theme.label("uso:")} quantbrasil backtests info ${strategy.id}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function formatBacktestParameter(parameter) {
|
|
380
|
+
const details = [parameter.type];
|
|
381
|
+
if (parameter.required) {
|
|
382
|
+
details.push("obrigatório");
|
|
383
|
+
}
|
|
384
|
+
if (parameter.default !== null) {
|
|
385
|
+
details.push(`padrão ${formatScalar(parameter.default)}`);
|
|
386
|
+
}
|
|
387
|
+
if (parameter.options !== null && parameter.options.length > 0) {
|
|
388
|
+
details.push(`opções ${parameter.options.map(option => option.value).join("|")}`);
|
|
389
|
+
}
|
|
390
|
+
const lines = [
|
|
391
|
+
`${parameter.name}: ${details.join(", ")} - ${parameter.description}`,
|
|
392
|
+
];
|
|
393
|
+
if (parameter.value_hint) {
|
|
394
|
+
lines.push(` Valores: ${parameter.value_hint}`);
|
|
395
|
+
}
|
|
396
|
+
if (parameter.agent_guidance) {
|
|
397
|
+
lines.push(` Guia: ${parameter.agent_guidance}`);
|
|
398
|
+
}
|
|
399
|
+
return lines.join("\n");
|
|
400
|
+
}
|
|
401
|
+
function formatStrategyRunUsage(strategy) {
|
|
402
|
+
const timeframeOption = strategy.requires_timeframe
|
|
403
|
+
? ` --timeframe ${strategy.allowed_timeframes.includes("D1") ? "D1" : strategy.allowed_timeframes[0]}`
|
|
404
|
+
: "";
|
|
405
|
+
const requiredParameterOptions = strategy.parameters
|
|
406
|
+
.filter(parameter => parameter.required)
|
|
407
|
+
.map(parameter => ` --param ${parameter.name}=${formatStrategyExampleParameterValue(parameter)}`)
|
|
408
|
+
.join("");
|
|
409
|
+
return `quantbrasil backtests run ${strategy.id} PETR4${timeframeOption} --from 2025-01-01 --to 2026-01-01${requiredParameterOptions}`;
|
|
410
|
+
}
|
|
411
|
+
function formatStrategyExampleParameterValue(parameter) {
|
|
412
|
+
if (parameter.default !== null) {
|
|
413
|
+
return String(parameter.default);
|
|
414
|
+
}
|
|
415
|
+
if (parameter.options !== null && parameter.options.length > 0) {
|
|
416
|
+
return parameter.options[0]?.value ?? "valor";
|
|
417
|
+
}
|
|
418
|
+
if (parameter.format === "time") {
|
|
419
|
+
return "10:30";
|
|
420
|
+
}
|
|
421
|
+
if (parameter.type === "boolean") {
|
|
422
|
+
return "true";
|
|
423
|
+
}
|
|
424
|
+
if (parameter.type === "integer" || parameter.type === "number") {
|
|
425
|
+
return "1";
|
|
426
|
+
}
|
|
427
|
+
return "valor";
|
|
428
|
+
}
|
|
429
|
+
function formatStrategyFlags(strategy) {
|
|
430
|
+
const flags = [];
|
|
431
|
+
if (!strategy.requires_timeframe) {
|
|
432
|
+
flags.push("timeframe definido internamente pela estratégia");
|
|
433
|
+
}
|
|
434
|
+
if (strategy.fixed_risk) {
|
|
435
|
+
flags.push("usa risco técnico/fixo no setup");
|
|
436
|
+
}
|
|
437
|
+
if (strategy.day_trade_only) {
|
|
438
|
+
flags.push("estratégia intradiária");
|
|
439
|
+
}
|
|
440
|
+
if (strategy.blocked_in_crypto) {
|
|
441
|
+
flags.push("não suporta criptoativos");
|
|
442
|
+
}
|
|
443
|
+
return flags;
|
|
444
|
+
}
|
|
445
|
+
function formatStrategySide(side) {
|
|
446
|
+
return side === "long" ? "compra" : "venda";
|
|
447
|
+
}
|
|
448
|
+
function formatStrategyTimeframes(strategy) {
|
|
449
|
+
if (!strategy.requires_timeframe) {
|
|
450
|
+
return "interno";
|
|
451
|
+
}
|
|
452
|
+
return strategy.allowed_timeframes.join(", ");
|
|
453
|
+
}
|
|
454
|
+
function formatShortGuidance(value) {
|
|
455
|
+
const maxLength = 180;
|
|
456
|
+
if (value.length <= maxLength) {
|
|
457
|
+
return value;
|
|
458
|
+
}
|
|
459
|
+
return `${value.slice(0, maxLength - 3).trimEnd()}...`;
|
|
460
|
+
}
|
|
461
|
+
function formatParameterRecord(parameters) {
|
|
462
|
+
return Object.entries(parameters)
|
|
463
|
+
.map(([key, value]) => `${key}=${formatJsonScalar(value)}`)
|
|
464
|
+
.join(", ");
|
|
465
|
+
}
|
|
466
|
+
function formatJsonScalar(value) {
|
|
467
|
+
if (value === null) {
|
|
468
|
+
return "null";
|
|
469
|
+
}
|
|
470
|
+
if (typeof value === "string") {
|
|
471
|
+
return JSON.stringify(value);
|
|
472
|
+
}
|
|
473
|
+
if (typeof value === "number") {
|
|
474
|
+
return formatNumber(value);
|
|
475
|
+
}
|
|
476
|
+
if (typeof value === "boolean") {
|
|
477
|
+
return String(value);
|
|
478
|
+
}
|
|
479
|
+
return JSON.stringify(value);
|
|
480
|
+
}
|
|
481
|
+
function formatScalar(value) {
|
|
482
|
+
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
483
|
+
}
|
|
484
|
+
function formatBacktestOrigin(origin) {
|
|
485
|
+
if (!origin) {
|
|
486
|
+
return "não informado";
|
|
487
|
+
}
|
|
488
|
+
if (origin === "tool") {
|
|
489
|
+
return "ferramenta";
|
|
490
|
+
}
|
|
491
|
+
return origin;
|
|
492
|
+
}
|
|
493
|
+
function formatDatePtBr(value) {
|
|
494
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
|
|
495
|
+
if (!match) {
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
498
|
+
return `${match[3]}/${match[2]}/${match[1]}`;
|
|
499
|
+
}
|
|
500
|
+
function formatCurrency(value) {
|
|
501
|
+
return new Intl.NumberFormat("pt-BR", {
|
|
502
|
+
style: "currency",
|
|
503
|
+
currency: "BRL",
|
|
504
|
+
maximumFractionDigits: 2,
|
|
505
|
+
}).format(value);
|
|
506
|
+
}
|
|
507
|
+
function formatPercentage(value) {
|
|
508
|
+
return `${formatNumber(value * 100)}%`;
|
|
509
|
+
}
|
|
510
|
+
function formatNullableNumber(value) {
|
|
511
|
+
return value === null ? "n/d" : formatNumber(value);
|
|
512
|
+
}
|
|
513
|
+
function formatInteger(value) {
|
|
514
|
+
return new Intl.NumberFormat("pt-BR", {
|
|
515
|
+
maximumFractionDigits: 0,
|
|
516
|
+
}).format(value);
|
|
517
|
+
}
|
|
518
|
+
function formatNumber(value) {
|
|
519
|
+
return new Intl.NumberFormat("pt-BR", {
|
|
520
|
+
maximumFractionDigits: 2,
|
|
521
|
+
}).format(value);
|
|
522
|
+
}
|
|
523
|
+
function isPlainRecord(value) {
|
|
524
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
525
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { JsonValue } from "../vendor/core/index.js";
|
|
3
|
+
import { type CliInvokeContext } from "../cli/client.js";
|
|
4
|
+
import { type TerminalWriter } from "../cli/terminal.js";
|
|
5
|
+
export interface NewsCommandIO {
|
|
6
|
+
stdout: TerminalWriter;
|
|
7
|
+
}
|
|
8
|
+
export interface AssetNewsCommandOptions {
|
|
9
|
+
window?: string;
|
|
10
|
+
limit?: string;
|
|
11
|
+
json?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface AssetNewsAsset {
|
|
14
|
+
[key: string]: JsonValue;
|
|
15
|
+
id: number;
|
|
16
|
+
ticker: string;
|
|
17
|
+
name: string | null;
|
|
18
|
+
type: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface AssetNewsArticle {
|
|
21
|
+
[key: string]: JsonValue;
|
|
22
|
+
id: number;
|
|
23
|
+
source_slug: string;
|
|
24
|
+
source_name: string;
|
|
25
|
+
title: string;
|
|
26
|
+
summary: string | null;
|
|
27
|
+
url: string;
|
|
28
|
+
published_at: string;
|
|
29
|
+
last_seen_at: string;
|
|
30
|
+
content_updated_at: string | null;
|
|
31
|
+
mentioned_tickers: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface AssetNewsResponse {
|
|
34
|
+
[key: string]: JsonValue;
|
|
35
|
+
ok: boolean;
|
|
36
|
+
asset: AssetNewsAsset;
|
|
37
|
+
window_days: number;
|
|
38
|
+
limit: number;
|
|
39
|
+
total: number;
|
|
40
|
+
articles: AssetNewsArticle[];
|
|
41
|
+
}
|
|
42
|
+
export interface NewsCommandContext extends CliInvokeContext {
|
|
43
|
+
io?: NewsCommandIO;
|
|
44
|
+
}
|
|
45
|
+
export declare function registerNewsCommands(program: Command, context?: NewsCommandContext): void;
|
|
46
|
+
export declare function runAssetNewsCommand(ticker: string, options: AssetNewsCommandOptions, context?: NewsCommandContext): Promise<void>;
|
|
47
|
+
export declare function formatAssetNewsHuman(data: AssetNewsResponse, theme?: import("../cli/terminal.js").TerminalTheme): string;
|
|
48
|
+
//# sourceMappingURL=news.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"news.d.ts","sourceRoot":"","sources":["../../src/commands/news.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAE9E,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAK9E,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,cAAc,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAmB,SAAQ,gBAAgB;IAC1D,EAAE,CAAC,EAAE,aAAa,CAAC;CACpB;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,kBAAuB,GAC/B,IAAI,CAeN;AAED,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,uBAAuB,EAChC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAgBf;AA+BD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,iBAAiB,EACvB,KAAK,6CAAsC,GAC1C,MAAM,CAoCR"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { invokeCliCapability } from "../cli/client.js";
|
|
2
|
+
import { createCliValidationError } from "../cli/errors.js";
|
|
3
|
+
import { createTerminalTheme } from "../cli/terminal.js";
|
|
4
|
+
const DEFAULT_WINDOW_DAYS = "30";
|
|
5
|
+
const DEFAULT_NEWS_LIMIT = "20";
|
|
6
|
+
export function registerNewsCommands(program, context = {}) {
|
|
7
|
+
const newsCommand = program
|
|
8
|
+
.command("news")
|
|
9
|
+
.description("Lê notícias recentes vinculadas a ativos da QuantBrasil");
|
|
10
|
+
newsCommand
|
|
11
|
+
.command("asset")
|
|
12
|
+
.description("Busca notícias recentes para um ticker")
|
|
13
|
+
.argument("<ticker>", "Ticker do ativo, por exemplo PETR4")
|
|
14
|
+
.option("--window <days>", "Janela de busca em dias", DEFAULT_WINDOW_DAYS)
|
|
15
|
+
.option("--limit <count>", "Número máximo de notícias", DEFAULT_NEWS_LIMIT)
|
|
16
|
+
.option("--json", "Mostra a resposta em JSON")
|
|
17
|
+
.action(async (ticker, options) => {
|
|
18
|
+
await runAssetNewsCommand(ticker, options, context);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export async function runAssetNewsCommand(ticker, options, context = {}) {
|
|
22
|
+
const stdout = context.io?.stdout ?? process.stdout;
|
|
23
|
+
const theme = createTerminalTheme(stdout, context.env ?? process.env);
|
|
24
|
+
const response = await invokeCliCapability({
|
|
25
|
+
capability: "news.asset",
|
|
26
|
+
input: buildAssetNewsInput(ticker, options),
|
|
27
|
+
env: context.env,
|
|
28
|
+
fetch: context.fetch,
|
|
29
|
+
});
|
|
30
|
+
if (options.json) {
|
|
31
|
+
stdout.write(`${JSON.stringify(response.data, null, 2)}\n`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
stdout.write(`${formatAssetNewsHuman(response.data, theme)}\n`);
|
|
35
|
+
}
|
|
36
|
+
function buildAssetNewsInput(ticker, options) {
|
|
37
|
+
return {
|
|
38
|
+
ticker,
|
|
39
|
+
window_days: parsePositiveIntegerOption(options.window ?? DEFAULT_WINDOW_DAYS, "--window"),
|
|
40
|
+
limit: parsePositiveIntegerOption(options.limit ?? DEFAULT_NEWS_LIMIT, "--limit"),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function parsePositiveIntegerOption(raw, flag) {
|
|
44
|
+
const normalized = (raw ?? "").trim();
|
|
45
|
+
const value = Number.parseInt(normalized, 10);
|
|
46
|
+
if (!Number.isInteger(value) || value <= 0 || String(value) !== normalized) {
|
|
47
|
+
throw createCliValidationError(`${flag} deve ser um inteiro positivo.`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
export function formatAssetNewsHuman(data, theme = createTerminalTheme(process.stdout)) {
|
|
52
|
+
const identity = data.asset.name && data.asset.name.trim()
|
|
53
|
+
? `${theme.bold(data.asset.ticker)} ${theme.dim(`· ${data.asset.name}`)}`
|
|
54
|
+
: theme.bold(data.asset.ticker);
|
|
55
|
+
const lines = [
|
|
56
|
+
theme.label("Notícias do ativo"),
|
|
57
|
+
"",
|
|
58
|
+
identity,
|
|
59
|
+
`${theme.label("Janela:")} ${data.window_days} dias`,
|
|
60
|
+
`${theme.label("Total:")} ${formatInteger(data.total)}`,
|
|
61
|
+
];
|
|
62
|
+
if (data.articles.length === 0) {
|
|
63
|
+
lines.push("", "Nenhuma notícia recente encontrada.");
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
for (const article of data.articles) {
|
|
67
|
+
lines.push("");
|
|
68
|
+
lines.push(`${theme.bold(article.title)}`);
|
|
69
|
+
lines.push(`${theme.label("Fonte:")} ${article.source_name} ${theme.dim(formatDate(article.published_at))}`);
|
|
70
|
+
lines.push(`${theme.label("Menções:")} ${article.mentioned_tickers.join(", ")}`);
|
|
71
|
+
if (article.summary) {
|
|
72
|
+
lines.push(`${theme.label("Resumo:")} ${article.summary}`);
|
|
73
|
+
}
|
|
74
|
+
lines.push(`${theme.label("URL:")} ${article.url}`);
|
|
75
|
+
}
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
78
|
+
function formatDate(value) {
|
|
79
|
+
return value.split("T")[0] ?? value;
|
|
80
|
+
}
|
|
81
|
+
function formatInteger(value) {
|
|
82
|
+
return new Intl.NumberFormat("pt-BR", {
|
|
83
|
+
maximumFractionDigits: 0,
|
|
84
|
+
}).format(value);
|
|
85
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,9 +4,11 @@ export * from "./cli/errors.js";
|
|
|
4
4
|
export * from "./cli/index.js";
|
|
5
5
|
export * from "./commands/assets.js";
|
|
6
6
|
export * from "./commands/auth.js";
|
|
7
|
+
export * from "./commands/backtests.js";
|
|
7
8
|
export * from "./commands/capabilities.js";
|
|
8
9
|
export * from "./commands/init.js";
|
|
9
10
|
export * from "./commands/market.js";
|
|
11
|
+
export * from "./commands/news.js";
|
|
10
12
|
export * from "./commands/portfolios.js";
|
|
11
13
|
export * from "./commands/rankings.js";
|
|
12
14
|
export * from "./commands/screening.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
|