echopai 2.3.0 → 2.4.0
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 +63 -348
- package/dist/bin.js +8298 -190
- package/package.json +11 -13
- package/dist/_generated/commands.js +0 -378
- package/dist/_generated/help.js +0 -295
- package/dist/_generated/operations.js +0 -2385
- package/dist/runtime/auth.js +0 -95
- package/dist/runtime/envelope.js +0 -52
- package/dist/runtime/errors.js +0 -186
- package/dist/runtime/filters.js +0 -153
- package/dist/runtime/format.js +0 -143
- package/dist/runtime/http.js +0 -65
- package/dist/runtime/idempotency.js +0 -18
- package/dist/runtime/invoker.js +0 -391
- package/dist/runtime/io.js +0 -16
- package/dist/runtime/paginator.js +0 -146
- package/dist/runtime/trace.js +0 -99
- package/dist/runtime/tty.js +0 -51
- package/dist/runtime/update_check.js +0 -120
- package/dist/runtime/update_worker.js +0 -63
- package/dist/runtime/verb_cmd.js +0 -72
- package/dist/runtime/verb_runner.js +0 -152
- package/dist/runtime/whoami_cache.js +0 -109
- package/dist/tools/api.js +0 -81
- package/dist/tools/completion.js +0 -116
- package/dist/tools/config.js +0 -123
- package/dist/tools/doctor.js +0 -183
- package/dist/tools/login.js +0 -99
- package/dist/tools/mcp.js +0 -141
- package/dist/tools/raw.js +0 -96
- package/dist/tools/schema.js +0 -58
- package/dist/tools/trace.js +0 -54
- package/dist/tools/upgrade.js +0 -103
- package/dist/tools/welcome.js +0 -225
- package/dist/tools/whoami.js +0 -132
- package/dist/verbs/_spec.js +0 -15
- package/dist/verbs/announcements.js +0 -195
- package/dist/verbs/bars_batch.js +0 -66
- package/dist/verbs/chart.js +0 -110
- package/dist/verbs/concepts.js +0 -393
- package/dist/verbs/digest.js +0 -351
- package/dist/verbs/financials.js +0 -212
- package/dist/verbs/hot.js +0 -29
- package/dist/verbs/index.js +0 -88
- package/dist/verbs/limit_up.js +0 -156
- package/dist/verbs/lookup.js +0 -72
- package/dist/verbs/market.js +0 -185
- package/dist/verbs/news.js +0 -81
- package/dist/verbs/quote.js +0 -53
- package/dist/verbs/scan.js +0 -42
- package/dist/verbs/search.js +0 -105
- package/dist/verbs/sentiment.js +0 -231
- package/dist/verbs/views.js +0 -85
- package/dist/version.js +0 -5
package/dist/verbs/_spec.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VerbSpec —— curated verb 的源声明,CLI 与 MCP 两条入口共享。
|
|
3
|
-
*
|
|
4
|
-
* - CLI (commander): build*Command() 内部把 spec.handler 接到 commander.action
|
|
5
|
-
* 外加 executeVerb (process.exit / stderr / 错误 envelope) 包装。
|
|
6
|
-
* - MCP: tools/mcp.ts 启动时枚举所有 spec,把 inputSchema 直接喂 SDK
|
|
7
|
-
* registerTool,handler 包成 MCP tool callback。
|
|
8
|
-
*
|
|
9
|
-
* 单源声明保证两个入口的 verb 名称 / 参数语义 / 输出形态完全同步。
|
|
10
|
-
*
|
|
11
|
-
* inputSchema 用 Zod raw shape (Record<string, ZodType>) —— MCP SDK 原生
|
|
12
|
-
* 接受;CLI 端不重复用它做校验 (commander Option + 我们 verb 内部的
|
|
13
|
-
* clamp/parse 已足够)。
|
|
14
|
-
*/
|
|
15
|
-
export {};
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai announcements ...` + MCP tools `announcements_feed` /
|
|
3
|
-
* `announcements_stock` / `announcements_detail`.
|
|
4
|
-
*
|
|
5
|
-
* A 股公告 (cninfo 主源):
|
|
6
|
-
* - feed —— 全市场最近公告,按 published_at desc
|
|
7
|
-
* - stock —— 指定 canonical_code 的公告历史窗口(最长 5 年)
|
|
8
|
-
* - detail —— 单条公告完整正文(content_md + content_text + meta)
|
|
9
|
-
*
|
|
10
|
-
* 三个 op 共享 `announcements:read` scope,calling 协议参数与底层 OpenAPI
|
|
11
|
-
* 一致(type slug / since_days 等)。MCP 注册成三个 discrete tool,AI agent
|
|
12
|
-
* 可按 "我要看 X 的财报披露" / "今天市场公告" / "把这条公告全文给我" 各自取用。
|
|
13
|
-
*/
|
|
14
|
-
import { Command, Option } from "commander";
|
|
15
|
-
import { z } from "zod";
|
|
16
|
-
import { OPERATIONS } from "../_generated/operations.js";
|
|
17
|
-
import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
|
|
18
|
-
import { callOp } from "../runtime/verb_runner.js";
|
|
19
|
-
const CODE_RE = /^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/;
|
|
20
|
-
const ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/;
|
|
21
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
|
-
export const announcementsFeedSpec = {
|
|
23
|
-
name: "announcements_feed",
|
|
24
|
-
description: "Recent A-share announcement feed (cninfo source, sorted by published_at desc). Filter by `type` slug (e.g. annual_report / equity_change / restructuring) or `since` lower bound. Use when surveying market-wide disclosures over a recent window — for a specific stock prefer `announcements_stock`.",
|
|
25
|
-
inputSchema: {
|
|
26
|
-
type: z
|
|
27
|
-
.string()
|
|
28
|
-
.max(64)
|
|
29
|
-
.optional()
|
|
30
|
-
.describe("分类 slug 过滤(如 annual_report / equity_change / restructuring)。"),
|
|
31
|
-
since: z
|
|
32
|
-
.string()
|
|
33
|
-
.regex(ISO_DATETIME_RE)
|
|
34
|
-
.optional()
|
|
35
|
-
.describe("ISO datetime 下限(published_at >= since)。"),
|
|
36
|
-
limit: z
|
|
37
|
-
.number()
|
|
38
|
-
.int()
|
|
39
|
-
.min(1)
|
|
40
|
-
.max(200)
|
|
41
|
-
.default(50)
|
|
42
|
-
.describe("Max items (1-200, default 50)"),
|
|
43
|
-
offset: z
|
|
44
|
-
.number()
|
|
45
|
-
.int()
|
|
46
|
-
.min(0)
|
|
47
|
-
.default(0)
|
|
48
|
-
.describe("Pagination offset"),
|
|
49
|
-
},
|
|
50
|
-
handler: async (args, ctx) => {
|
|
51
|
-
const op = OPERATIONS["announcements.feed"];
|
|
52
|
-
if (!op)
|
|
53
|
-
throw new Error("announcements.feed op missing");
|
|
54
|
-
const callArgs = {
|
|
55
|
-
limit: args.limit,
|
|
56
|
-
offset: args.offset,
|
|
57
|
-
};
|
|
58
|
-
if (args.type)
|
|
59
|
-
callArgs.type = args.type;
|
|
60
|
-
if (args.since)
|
|
61
|
-
callArgs.since = args.since;
|
|
62
|
-
return callOp(op, callArgs, ctx);
|
|
63
|
-
},
|
|
64
|
-
backingOps: ["announcements.feed"],
|
|
65
|
-
};
|
|
66
|
-
export const announcementsStockSpec = {
|
|
67
|
-
name: "announcements_stock",
|
|
68
|
-
description: "Announcement history for a single A-share security (cninfo). Default lookback 30 days, max 5 years. AH dual-listing: pass the A-share canonical (announcements are A-share only — HK side has separate HKEX disclosures not covered here).",
|
|
69
|
-
inputSchema: {
|
|
70
|
-
code: z
|
|
71
|
-
.string()
|
|
72
|
-
.regex(CODE_RE)
|
|
73
|
-
.describe("A-share canonical code (e.g. SSE:600519)"),
|
|
74
|
-
since_days: z
|
|
75
|
-
.number()
|
|
76
|
-
.int()
|
|
77
|
-
.min(1)
|
|
78
|
-
.max(1825)
|
|
79
|
-
.default(30)
|
|
80
|
-
.describe("Lookback window in days (1-1825, default 30)"),
|
|
81
|
-
type: z
|
|
82
|
-
.string()
|
|
83
|
-
.max(64)
|
|
84
|
-
.optional()
|
|
85
|
-
.describe("分类 slug 过滤(与 feed 同集合)"),
|
|
86
|
-
limit: z
|
|
87
|
-
.number()
|
|
88
|
-
.int()
|
|
89
|
-
.min(1)
|
|
90
|
-
.max(200)
|
|
91
|
-
.default(50)
|
|
92
|
-
.describe("Max items (1-200, default 50)"),
|
|
93
|
-
offset: z
|
|
94
|
-
.number()
|
|
95
|
-
.int()
|
|
96
|
-
.min(0)
|
|
97
|
-
.default(0)
|
|
98
|
-
.describe("Pagination offset"),
|
|
99
|
-
},
|
|
100
|
-
handler: async (args, ctx) => {
|
|
101
|
-
const op = OPERATIONS["announcements.stock"];
|
|
102
|
-
if (!op)
|
|
103
|
-
throw new Error("announcements.stock op missing");
|
|
104
|
-
const callArgs = {
|
|
105
|
-
code: args.code,
|
|
106
|
-
since_days: args.since_days,
|
|
107
|
-
limit: args.limit,
|
|
108
|
-
offset: args.offset,
|
|
109
|
-
};
|
|
110
|
-
if (args.type)
|
|
111
|
-
callArgs.type = args.type;
|
|
112
|
-
return callOp(op, callArgs, ctx);
|
|
113
|
-
},
|
|
114
|
-
backingOps: ["announcements.stock"],
|
|
115
|
-
};
|
|
116
|
-
export const announcementsDetailSpec = {
|
|
117
|
-
name: "announcements_detail",
|
|
118
|
-
description: "Fetch one announcement by id (UUID from feed/stock items[].id). Returns full content_md + content_text + cninfo meta + parse status. Use when the summary in feed/stock is not enough and you need the full document body for citation or NLP.",
|
|
119
|
-
inputSchema: {
|
|
120
|
-
announcement_id: z
|
|
121
|
-
.string()
|
|
122
|
-
.regex(UUID_RE)
|
|
123
|
-
.describe("UUID (feed / stock 返回的 items[].id)"),
|
|
124
|
-
},
|
|
125
|
-
handler: async (args, ctx) => {
|
|
126
|
-
const op = OPERATIONS["announcements.detail"];
|
|
127
|
-
if (!op)
|
|
128
|
-
throw new Error("announcements.detail op missing");
|
|
129
|
-
return callOp(op, { announcement_id: args.announcement_id }, ctx);
|
|
130
|
-
},
|
|
131
|
-
backingOps: ["announcements.detail"],
|
|
132
|
-
};
|
|
133
|
-
function clampInt(raw, min, max, fallback) {
|
|
134
|
-
const n = Math.floor(Number(raw));
|
|
135
|
-
if (!Number.isFinite(n))
|
|
136
|
-
return fallback;
|
|
137
|
-
if (n < min)
|
|
138
|
-
return min;
|
|
139
|
-
if (n > max)
|
|
140
|
-
return max;
|
|
141
|
-
return n;
|
|
142
|
-
}
|
|
143
|
-
export function buildAnnouncementsCommand() {
|
|
144
|
-
const cmd = new Command("announcements").description("A-share announcements (cninfo) — recent feed, per-stock history, single-item detail.");
|
|
145
|
-
const feed = cmd.command("feed").description(announcementsFeedSpec.description);
|
|
146
|
-
feed.addOption(new Option("--type <slug>", "Filter by 分类 slug (annual_report, equity_change, ...)"));
|
|
147
|
-
feed.addOption(new Option("--since <ISO-datetime>", "Lower bound published_at (RFC3339)"));
|
|
148
|
-
feed.addOption(new Option("--limit <n>", "Max items (1-200)").default("50"));
|
|
149
|
-
feed.addOption(new Option("--offset <n>", "Pagination offset").default("0"));
|
|
150
|
-
feed.action(async (opts) => {
|
|
151
|
-
if (!OPERATIONS["announcements.feed"]) {
|
|
152
|
-
emitVerbError("internal_error", "announcements.feed missing", undefined, 2);
|
|
153
|
-
}
|
|
154
|
-
const args = {
|
|
155
|
-
limit: clampInt(opts.limit, 1, 200, 50),
|
|
156
|
-
offset: Math.max(0, Math.floor(Number(opts.offset)) || 0),
|
|
157
|
-
};
|
|
158
|
-
if (opts.type)
|
|
159
|
-
args.type = opts.type;
|
|
160
|
-
if (opts.since)
|
|
161
|
-
args.since = opts.since;
|
|
162
|
-
await executeVerb(async (ctx) => announcementsFeedSpec.handler(args, ctx));
|
|
163
|
-
});
|
|
164
|
-
const stock = cmd.command("stock").description(announcementsStockSpec.description);
|
|
165
|
-
stock.addOption(new Option("--code <canonical_code>", "A-share canonical (e.g. SSE:600519)")
|
|
166
|
-
.makeOptionMandatory(true));
|
|
167
|
-
stock.addOption(new Option("--since-days <n>", "Lookback window in days (1-1825)").default("30"));
|
|
168
|
-
stock.addOption(new Option("--type <slug>", "Filter by 分类 slug"));
|
|
169
|
-
stock.addOption(new Option("--limit <n>", "Max items (1-200)").default("50"));
|
|
170
|
-
stock.addOption(new Option("--offset <n>", "Pagination offset").default("0"));
|
|
171
|
-
stock.action(async (opts) => {
|
|
172
|
-
if (!OPERATIONS["announcements.stock"]) {
|
|
173
|
-
emitVerbError("internal_error", "announcements.stock missing", undefined, 2);
|
|
174
|
-
}
|
|
175
|
-
const args = {
|
|
176
|
-
code: opts.code,
|
|
177
|
-
since_days: clampInt(opts.sinceDays, 1, 1825, 30),
|
|
178
|
-
limit: clampInt(opts.limit, 1, 200, 50),
|
|
179
|
-
offset: Math.max(0, Math.floor(Number(opts.offset)) || 0),
|
|
180
|
-
};
|
|
181
|
-
if (opts.type)
|
|
182
|
-
args.type = opts.type;
|
|
183
|
-
await executeVerb(async (ctx) => announcementsStockSpec.handler(args, ctx));
|
|
184
|
-
});
|
|
185
|
-
const detail = cmd
|
|
186
|
-
.command("detail <announcement_id>")
|
|
187
|
-
.description(announcementsDetailSpec.description);
|
|
188
|
-
detail.action(async (announcementId) => {
|
|
189
|
-
if (!OPERATIONS["announcements.detail"]) {
|
|
190
|
-
emitVerbError("internal_error", "announcements.detail missing", undefined, 2);
|
|
191
|
-
}
|
|
192
|
-
await executeVerb(async (ctx) => announcementsDetailSpec.handler({ announcement_id: announcementId }, ctx));
|
|
193
|
-
});
|
|
194
|
-
return cmd;
|
|
195
|
-
}
|
package/dist/verbs/bars_batch.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai bars-batch` + MCP tool `bars_batch`.
|
|
3
|
-
*/
|
|
4
|
-
import { Command, Option } from "commander";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { OPERATIONS } from "../_generated/operations.js";
|
|
7
|
-
import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
|
|
8
|
-
import { callOp } from "../runtime/verb_runner.js";
|
|
9
|
-
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
10
|
-
const CODE_RE = /^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/;
|
|
11
|
-
export const barsBatchSpec = {
|
|
12
|
-
name: "bars_batch",
|
|
13
|
-
description: "Batch K-line for multiple A-share securities in one round-trip. Daily: ≤100 codes × ≤1yr; minute: ≤20 codes × ≤7d. Returns partial-success envelope {items, errors}.",
|
|
14
|
-
inputSchema: {
|
|
15
|
-
codes: z
|
|
16
|
-
.array(z.string().regex(CODE_RE))
|
|
17
|
-
.min(1)
|
|
18
|
-
.max(100)
|
|
19
|
-
.describe("Canonical codes (daily ≤100; minute ≤20)"),
|
|
20
|
-
from: z.string().regex(DATE_RE).describe("Inclusive start date YYYY-MM-DD"),
|
|
21
|
-
to: z.string().regex(DATE_RE).describe("Inclusive end date YYYY-MM-DD"),
|
|
22
|
-
minute: z
|
|
23
|
-
.boolean()
|
|
24
|
-
.optional()
|
|
25
|
-
.describe("Minute-level bars instead of daily (tighter caps apply)"),
|
|
26
|
-
},
|
|
27
|
-
handler: async (args, ctx) => {
|
|
28
|
-
const isMinute = Boolean(args.minute);
|
|
29
|
-
const cap = isMinute ? 20 : 100;
|
|
30
|
-
const codes = args.codes;
|
|
31
|
-
if (codes.length > cap) {
|
|
32
|
-
throw new Error(`codes count ${codes.length} exceeds cap ${cap} for ${isMinute ? "minute" : "daily"} bars_batch`);
|
|
33
|
-
}
|
|
34
|
-
const cliKey = isMinute ? "bars.minute-batch" : "bars.daily-batch";
|
|
35
|
-
const op = OPERATIONS[cliKey];
|
|
36
|
-
if (!op)
|
|
37
|
-
throw new Error(`${cliKey} op missing from codegen`);
|
|
38
|
-
return callOp(op, { codes, from: args.from, to: args.to }, ctx);
|
|
39
|
-
},
|
|
40
|
-
backingOps: ["bars.daily-batch", "bars.minute-batch"],
|
|
41
|
-
};
|
|
42
|
-
export function buildBarsBatchCommand() {
|
|
43
|
-
// CLI alias keeps the hyphenated form. MCP tool name uses underscore (MCP
|
|
44
|
-
// convention; underscores parse cleaner across hosts).
|
|
45
|
-
const cmd = new Command("bars-batch").description(barsBatchSpec.description);
|
|
46
|
-
cmd.addOption(new Option("--codes <csv>", "Canonical codes, comma-separated")
|
|
47
|
-
.makeOptionMandatory(true));
|
|
48
|
-
cmd.addOption(new Option("--from <date>", "Inclusive start YYYY-MM-DD").makeOptionMandatory(true));
|
|
49
|
-
cmd.addOption(new Option("--to <date>", "Inclusive end YYYY-MM-DD").makeOptionMandatory(true));
|
|
50
|
-
cmd.addOption(new Option("--minute", "Minute-level bars instead of daily"));
|
|
51
|
-
cmd.action(async (opts) => {
|
|
52
|
-
const codes = opts.codes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
53
|
-
const isMinute = Boolean(opts.minute);
|
|
54
|
-
const cap = isMinute ? 20 : 100;
|
|
55
|
-
if (codes.length === 0 || codes.length > cap) {
|
|
56
|
-
emitVerbError("invalid_args", `codes count ${codes.length} out of range; expected 1-${cap} for ${isMinute ? "minute" : "daily"} bars-batch`, isMinute
|
|
57
|
-
? "Daily mode (default) allows up to 100 codes; drop --minute or chunk the request."
|
|
58
|
-
: "Chunk the request into batches of ≤100 codes.", 1);
|
|
59
|
-
}
|
|
60
|
-
const args = { codes, from: opts.from, to: opts.to };
|
|
61
|
-
if (opts.minute)
|
|
62
|
-
args.minute = true;
|
|
63
|
-
await executeVerb(async (ctx) => barsBatchSpec.handler(args, ctx));
|
|
64
|
-
});
|
|
65
|
-
return cmd;
|
|
66
|
-
}
|
package/dist/verbs/chart.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai chart` + MCP tool `chart`.
|
|
3
|
-
*/
|
|
4
|
-
import { Command, Option } from "commander";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { OPERATIONS } from "../_generated/operations.js";
|
|
7
|
-
import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
|
|
8
|
-
import { callOp } from "../runtime/verb_runner.js";
|
|
9
|
-
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
10
|
-
export const chartSpec = {
|
|
11
|
-
name: "chart",
|
|
12
|
-
description: "Single-security K-line (daily by default; set minute=true for intraday with a single date). For multi-code use `bars_batch` instead.",
|
|
13
|
-
inputSchema: {
|
|
14
|
-
code: z
|
|
15
|
-
.string()
|
|
16
|
-
.regex(/^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/)
|
|
17
|
-
.describe("Single canonical code (e.g. SSE:600519)"),
|
|
18
|
-
days: z
|
|
19
|
-
.number()
|
|
20
|
-
.int()
|
|
21
|
-
.min(1)
|
|
22
|
-
.max(365)
|
|
23
|
-
.default(30)
|
|
24
|
-
.describe("Daily lookback (ignored when from/to or minute mode are set)"),
|
|
25
|
-
from: z
|
|
26
|
-
.string()
|
|
27
|
-
.regex(DATE_RE)
|
|
28
|
-
.optional()
|
|
29
|
-
.describe("YYYY-MM-DD inclusive start (daily mode; overrides days)"),
|
|
30
|
-
to: z
|
|
31
|
-
.string()
|
|
32
|
-
.regex(DATE_RE)
|
|
33
|
-
.optional()
|
|
34
|
-
.describe("YYYY-MM-DD inclusive end (daily mode; overrides days)"),
|
|
35
|
-
minute: z.boolean().optional().describe("Switch to minute-level bars (requires date)"),
|
|
36
|
-
date: z.string().regex(DATE_RE).optional().describe("Single trade date YYYY-MM-DD (minute mode)"),
|
|
37
|
-
},
|
|
38
|
-
handler: async (args, ctx) => {
|
|
39
|
-
if (args.minute) {
|
|
40
|
-
if (!args.date) {
|
|
41
|
-
throw new Error("minute=true requires date (YYYY-MM-DD)");
|
|
42
|
-
}
|
|
43
|
-
const op = OPERATIONS["bars.minute"];
|
|
44
|
-
if (!op)
|
|
45
|
-
throw new Error("bars.minute op missing");
|
|
46
|
-
// bars.minute expects a single `date` (YYYY-MM-DD), not from/to —
|
|
47
|
-
// see stockpulse-rs MinuteBarsQuery + OpenAPI 2026-05-12 correction.
|
|
48
|
-
return callOp(op, { code: args.code, date: args.date }, ctx);
|
|
49
|
-
}
|
|
50
|
-
const op = OPERATIONS["bars.daily"];
|
|
51
|
-
if (!op)
|
|
52
|
-
throw new Error("bars.daily op missing");
|
|
53
|
-
const callArgs = { code: args.code };
|
|
54
|
-
if (args.from && args.to) {
|
|
55
|
-
callArgs.from = args.from;
|
|
56
|
-
callArgs.to = args.to;
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
// bars.daily requires from/to (additionalProperties:false; no `days`).
|
|
60
|
-
// Derive a from/to window from --days client-side. Includes today;
|
|
61
|
-
// server skips non-trading days automatically.
|
|
62
|
-
const days = Math.max(1, Number(args.days ?? 30));
|
|
63
|
-
const to = new Date();
|
|
64
|
-
const from = new Date(to.getTime() - (days - 1) * 24 * 3600 * 1000);
|
|
65
|
-
const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
66
|
-
callArgs.from = fmt(from);
|
|
67
|
-
callArgs.to = fmt(to);
|
|
68
|
-
}
|
|
69
|
-
return callOp(op, callArgs, ctx);
|
|
70
|
-
},
|
|
71
|
-
backingOps: ["bars.daily", "bars.minute"],
|
|
72
|
-
};
|
|
73
|
-
function clamp(raw, min, max, fallback) {
|
|
74
|
-
const n = Math.floor(Number(raw));
|
|
75
|
-
if (!Number.isFinite(n))
|
|
76
|
-
return fallback;
|
|
77
|
-
if (n < min)
|
|
78
|
-
return min;
|
|
79
|
-
if (n > max)
|
|
80
|
-
return max;
|
|
81
|
-
return n;
|
|
82
|
-
}
|
|
83
|
-
export function buildChartCommand() {
|
|
84
|
-
const cmd = new Command(chartSpec.name).description(chartSpec.description);
|
|
85
|
-
cmd.addOption(new Option("--code <canonical_code>", "Single canonical code").makeOptionMandatory(true));
|
|
86
|
-
cmd.addOption(new Option("--days <n>", "Daily lookback (1-365, default 30)").default("30"));
|
|
87
|
-
cmd.addOption(new Option("--from <date>", "Inclusive start date YYYY-MM-DD (overrides --days)"));
|
|
88
|
-
cmd.addOption(new Option("--to <date>", "Inclusive end date YYYY-MM-DD (overrides --days)"));
|
|
89
|
-
cmd.addOption(new Option("--minute", "Switch to minute-level bars (requires --date)"));
|
|
90
|
-
cmd.addOption(new Option("--date <date>", "Single trade date YYYY-MM-DD (minute mode)"));
|
|
91
|
-
cmd.action(async (opts) => {
|
|
92
|
-
if (opts.minute && !opts.date) {
|
|
93
|
-
emitVerbError("invalid_args", "--minute requires --date YYYY-MM-DD (single trade date)", "Example: echopai chart --minute --code SSE:600519 --date 2026-05-09", 1);
|
|
94
|
-
}
|
|
95
|
-
const args = {
|
|
96
|
-
code: opts.code,
|
|
97
|
-
days: clamp(opts.days, 1, 365, 30),
|
|
98
|
-
};
|
|
99
|
-
if (opts.from)
|
|
100
|
-
args.from = opts.from;
|
|
101
|
-
if (opts.to)
|
|
102
|
-
args.to = opts.to;
|
|
103
|
-
if (opts.minute)
|
|
104
|
-
args.minute = true;
|
|
105
|
-
if (opts.date)
|
|
106
|
-
args.date = opts.date;
|
|
107
|
-
await executeVerb(async (ctx) => chartSpec.handler(args, ctx));
|
|
108
|
-
});
|
|
109
|
-
return cmd;
|
|
110
|
-
}
|