echopai 2.2.0 → 2.3.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.
@@ -5,48 +5,79 @@
5
5
  * - ALL_VERB_SPECS: 共用声明,MCP serve 枚举出 tools/list
6
6
  * - build*Command(): commander 端入口工厂
7
7
  */
8
+ export { announcementsFeedSpec, announcementsStockSpec, announcementsDetailSpec, buildAnnouncementsCommand, } from "./announcements.js";
8
9
  export { barsBatchSpec, buildBarsBatchCommand } from "./bars_batch.js";
9
10
  export { chartSpec, buildChartCommand } from "./chart.js";
11
+ export { conceptsListSpec, conceptsSnapshotSpec, conceptsShowSpec, conceptsAlertsSpec, conceptsAlertsHistorySpec, conceptsDailyBarsSpec, conceptsMinuteBarsSpec, conceptsNewsSpec, conceptsViewsSpec, buildConceptsCommand, } from "./concepts.js";
10
12
  export { digestSpec, buildDigestCommand } from "./digest.js";
11
13
  export { financialsQuoteSnapshotSpec, financialsPitSpec, financialsReportsSpec, financialsSeriesSpec, buildFinancialsCommand, } from "./financials.js";
12
14
  export { hotSpec, buildHotCommand } from "./hot.js";
15
+ export { limitUpPoolSpec, limitUpSummarySpec, limitUpHistorySpec, buildLimitUpCommand, } from "./limit_up.js";
13
16
  export { lookupSpec, buildLookupCommand } from "./lookup.js";
17
+ export { marketStatusSpec, marketMoversSpec, buildMarketCommand } from "./market.js";
14
18
  export { newsSpec, buildNewsCommand } from "./news.js";
15
19
  export { quoteSpec, buildQuoteCommand } from "./quote.js";
16
20
  export { scanSpec, buildScanCommand } from "./scan.js";
17
21
  export { searchSpec, buildSearchCommand } from "./search.js";
18
- export { sentimentSpec, buildSentimentCommand } from "./sentiment.js";
22
+ export { sentimentSpec, sentimentOverviewSpec, sentimentBreadthSpec, sentimentTurnoverSpec, sentimentPctDistributionSpec, buildSentimentCommand, } from "./sentiment.js";
19
23
  export { viewsSpec, buildViewsCommand } from "./views.js";
24
+ import { announcementsFeedSpec, announcementsStockSpec, announcementsDetailSpec, } from "./announcements.js";
20
25
  import { barsBatchSpec } from "./bars_batch.js";
21
26
  import { chartSpec } from "./chart.js";
27
+ import { conceptsListSpec, conceptsSnapshotSpec, conceptsShowSpec, conceptsAlertsSpec, conceptsAlertsHistorySpec, conceptsDailyBarsSpec, conceptsMinuteBarsSpec, conceptsNewsSpec, conceptsViewsSpec, } from "./concepts.js";
22
28
  import { digestSpec } from "./digest.js";
23
29
  import { financialsQuoteSnapshotSpec, financialsPitSpec, financialsReportsSpec, financialsSeriesSpec, } from "./financials.js";
24
30
  import { hotSpec } from "./hot.js";
31
+ import { limitUpPoolSpec, limitUpSummarySpec, limitUpHistorySpec, } from "./limit_up.js";
25
32
  import { lookupSpec } from "./lookup.js";
33
+ import { marketStatusSpec, marketMoversSpec } from "./market.js";
26
34
  import { newsSpec } from "./news.js";
27
35
  import { quoteSpec } from "./quote.js";
28
36
  import { scanSpec } from "./scan.js";
29
37
  import { searchSpec } from "./search.js";
30
- import { sentimentSpec } from "./sentiment.js";
38
+ import { sentimentSpec, sentimentOverviewSpec, sentimentBreadthSpec, sentimentTurnoverSpec, sentimentPctDistributionSpec, } from "./sentiment.js";
31
39
  import { viewsSpec } from "./views.js";
32
40
  /**
33
41
  * Ordering follows product priority (feedback_views_over_news.md):
34
42
  * lookup (universal first step) → digest (one-shot fan-out, agent's opening
35
43
  * move once it has a canonical_code) → search (hybrid semantic discovery
36
- * for themes / concepts) → quote → views (PRIMARY research) → news
37
- * (supplementary) → sentiment hot chart → bars_batch → scan →
38
- * financials (valuation snapshot is the headline; pit / reports / series
39
- * cover deeper fundamentals access).
44
+ * for themes / concepts) → quote → market_status (cheap session gate) →
45
+ * views (PRIMARY research) → news (supplementary)announcements (cninfo
46
+ * disclosures) sentiment hot concepts (theme universe + alerts) →
47
+ * limit-up (短线情绪/连板梯队) chart → bars_batch → scan → financials
48
+ * (valuation snapshot is the headline; pit / reports / series cover deeper
49
+ * fundamentals access).
40
50
  */
41
51
  export const ALL_VERB_SPECS = [
42
52
  lookupSpec,
43
53
  digestSpec,
44
54
  searchSpec,
45
55
  quoteSpec,
56
+ marketStatusSpec,
57
+ marketMoversSpec,
46
58
  viewsSpec,
47
59
  newsSpec,
60
+ announcementsFeedSpec,
61
+ announcementsStockSpec,
62
+ announcementsDetailSpec,
48
63
  sentimentSpec,
64
+ sentimentOverviewSpec,
65
+ sentimentBreadthSpec,
66
+ sentimentTurnoverSpec,
67
+ sentimentPctDistributionSpec,
49
68
  hotSpec,
69
+ conceptsListSpec,
70
+ conceptsSnapshotSpec,
71
+ conceptsShowSpec,
72
+ conceptsAlertsSpec,
73
+ conceptsAlertsHistorySpec,
74
+ conceptsDailyBarsSpec,
75
+ conceptsMinuteBarsSpec,
76
+ conceptsNewsSpec,
77
+ conceptsViewsSpec,
78
+ limitUpPoolSpec,
79
+ limitUpSummarySpec,
80
+ limitUpHistorySpec,
50
81
  chartSpec,
51
82
  barsBatchSpec,
52
83
  scanSpec,
@@ -0,0 +1,156 @@
1
+ /**
2
+ * `echopai limit-up ...` + MCP tools `limit_up_pool` / `limit_up_summary` /
3
+ * `limit_up_history`.
4
+ *
5
+ * 涨停股池 / 涨停统计 / 涨停趋势 — 短线情绪与连板梯队的核心数据源,全部走
6
+ * stockpulse-rs upstream(`market_limit_up_pool` ReplacingMergeTree + 与情绪页
7
+ * 同源的 `market_breadth_intraday` 炸板/跌停 snapshot)。
8
+ *
9
+ * 盘前回退(与所有行情类页面一致):< 9:00 → 上一交易日;9:00-9:14 集合竞价
10
+ * 前空窗期返回 items=[] / trade_date=null;≥ 9:15 起取今日。
11
+ *
12
+ * 用例:
13
+ * - 短线 agent: limit-up pool → 当日涨停股逐股;按 consecutive_days desc 取
14
+ * 最高连板;脉冲到 echopai chart 看 K 线
15
+ * - 大盘情绪: limit-up summary → 涨停数 / 炸板数 / 连板梯队;配合
16
+ * `sentiment overview` 形成情绪面板
17
+ * - 趋势研判: limit-up history --days 30 → 近一月每日涨停数 / 最高连板
18
+ */
19
+ import { Command, Option } from "commander";
20
+ import { z } from "zod";
21
+ import { OPERATIONS } from "../_generated/operations.js";
22
+ import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
23
+ import { callOp } from "../runtime/verb_runner.js";
24
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
25
+ const INCLUDE_VALUES = ["active", "all"];
26
+ function clampInt(raw, min, max, fallback) {
27
+ const n = Math.floor(Number(raw));
28
+ if (!Number.isFinite(n))
29
+ return fallback;
30
+ if (n < min)
31
+ return min;
32
+ if (n > max)
33
+ return max;
34
+ return n;
35
+ }
36
+ export const limitUpPoolSpec = {
37
+ name: "limit_up_pool",
38
+ description: "A-share limit-up pool for a given trade date — per-stock detail: first/last seal time, final pct, seal amount/volume, broken count, consecutive_days (连板数), board_count_within (N-day 板数), one-word-board flag, current status (limit_up / broken), board classification. Sort: consecutive_days desc → board_count_within desc → seal_amount desc. Pre-open fallback applies. Use when an agent needs the concrete list of '今天涨停了哪些票'.",
39
+ inputSchema: {
40
+ trade_date: z
41
+ .string()
42
+ .regex(DATE_RE)
43
+ .optional()
44
+ .describe("ISO date YYYY-MM-DD; omit for latest (pre-open fallback)"),
45
+ include: z
46
+ .enum(INCLUDE_VALUES)
47
+ .default("active")
48
+ .describe("active = 排除 ST/退市(默认); all = 全部"),
49
+ include_broken: z
50
+ .boolean()
51
+ .default(false)
52
+ .describe("true 时连炸板(status=broken)也一起返回;默认只返回当前封板 status=limit_up"),
53
+ },
54
+ handler: async (args, ctx) => {
55
+ const op = OPERATIONS["limit-up.pool"];
56
+ if (!op)
57
+ throw new Error("limit-up.pool op missing");
58
+ const callArgs = {
59
+ include: args.include,
60
+ include_broken: args.include_broken,
61
+ };
62
+ if (args.trade_date)
63
+ callArgs.trade_date = args.trade_date;
64
+ return callOp(op, callArgs, ctx);
65
+ },
66
+ backingOps: ["limit-up.pool"],
67
+ };
68
+ export const limitUpSummarySpec = {
69
+ name: "limit_up_summary",
70
+ description: "Limit-up summary for a given trade date: 涨停数 (limit_up_count) / 炸板数 (broken_count) / 跌停数 (limit_down_count) / 最高连板 (max_height) / 连板梯队 ladder[{height, count}]. 炸板/跌停 与情绪页同源 (market_breadth_intraday 当日最新分钟行). Pre-open fallback applies.",
71
+ inputSchema: {
72
+ trade_date: z
73
+ .string()
74
+ .regex(DATE_RE)
75
+ .optional()
76
+ .describe("ISO date YYYY-MM-DD; omit for latest"),
77
+ include: z
78
+ .enum(INCLUDE_VALUES)
79
+ .default("active")
80
+ .describe("active = all_a_ex_st 口径(默认); all = all_a 口径"),
81
+ },
82
+ handler: async (args, ctx) => {
83
+ const op = OPERATIONS["limit-up.summary"];
84
+ if (!op)
85
+ throw new Error("limit-up.summary op missing");
86
+ const callArgs = { include: args.include };
87
+ if (args.trade_date)
88
+ callArgs.trade_date = args.trade_date;
89
+ return callOp(op, callArgs, ctx);
90
+ },
91
+ backingOps: ["limit-up.summary"],
92
+ };
93
+ export const limitUpHistorySpec = {
94
+ name: "limit_up_history",
95
+ description: "Daily limit-up trend over the last N trading days: { trade_date, limit_up_count, broken_count, max_height }. 涨停数/最高连板 from market_limit_up_pool; 炸板数 from market_breadth_intraday (all_a_ex_st). Sorted by trade_date asc (oldest first).",
96
+ inputSchema: {
97
+ days: z
98
+ .number()
99
+ .int()
100
+ .min(1)
101
+ .max(250)
102
+ .default(30)
103
+ .describe("Lookback window in trading days (1-250, default 30)"),
104
+ },
105
+ handler: async (args, ctx) => {
106
+ const op = OPERATIONS["limit-up.history"];
107
+ if (!op)
108
+ throw new Error("limit-up.history op missing");
109
+ return callOp(op, { days: args.days }, ctx);
110
+ },
111
+ backingOps: ["limit-up.history"],
112
+ };
113
+ export function buildLimitUpCommand() {
114
+ const cmd = new Command("limit-up").description("A-share limit-up board — pool (per-stock detail) / summary (counts + ladder) / history (daily trend).");
115
+ const pool = cmd.command("pool").description(limitUpPoolSpec.description);
116
+ pool.addOption(new Option("--trade-date <YYYY-MM-DD>", "Trade date; omit for latest"));
117
+ pool.addOption(new Option("--include <s>", "active = 排除 ST/退市; all = 全部")
118
+ .choices([...INCLUDE_VALUES])
119
+ .default("active"));
120
+ pool.addOption(new Option("--include-broken", "Also return 炸板 (status=broken); default only limit_up"));
121
+ pool.action(async (opts) => {
122
+ if (!OPERATIONS["limit-up.pool"]) {
123
+ emitVerbError("internal_error", "limit-up.pool missing", undefined, 2);
124
+ }
125
+ const args = {
126
+ include: opts.include,
127
+ include_broken: Boolean(opts.includeBroken),
128
+ };
129
+ if (opts.tradeDate)
130
+ args.trade_date = opts.tradeDate;
131
+ await executeVerb(async (ctx) => limitUpPoolSpec.handler(args, ctx));
132
+ });
133
+ const summary = cmd.command("summary").description(limitUpSummarySpec.description);
134
+ summary.addOption(new Option("--trade-date <YYYY-MM-DD>", "Trade date; omit for latest"));
135
+ summary.addOption(new Option("--include <s>", "active = all_a_ex_st 口径; all = all_a 口径")
136
+ .choices([...INCLUDE_VALUES])
137
+ .default("active"));
138
+ summary.action(async (opts) => {
139
+ if (!OPERATIONS["limit-up.summary"]) {
140
+ emitVerbError("internal_error", "limit-up.summary missing", undefined, 2);
141
+ }
142
+ const args = { include: opts.include };
143
+ if (opts.tradeDate)
144
+ args.trade_date = opts.tradeDate;
145
+ await executeVerb(async (ctx) => limitUpSummarySpec.handler(args, ctx));
146
+ });
147
+ const history = cmd.command("history").description(limitUpHistorySpec.description);
148
+ history.addOption(new Option("--days <n>", "Lookback window (1-250)").default("30"));
149
+ history.action(async (opts) => {
150
+ if (!OPERATIONS["limit-up.history"]) {
151
+ emitVerbError("internal_error", "limit-up.history missing", undefined, 2);
152
+ }
153
+ await executeVerb(async (ctx) => limitUpHistorySpec.handler({ days: clampInt(opts.days, 1, 250, 30) }, ctx));
154
+ });
155
+ return cmd;
156
+ }
@@ -30,7 +30,7 @@ export function mapLookupResponse(raw) {
30
30
  }
31
31
  export const lookupSpec = {
32
32
  name: "lookup",
33
- description: "Resolve a Chinese name / A-share code / pinyin initials to canonical codes (e.g. SSE:600519). Use this first whenever the agent has a description but needs a canonical_code for downstream calls.",
33
+ description: "Resolve a Chinese name / A-share code / pinyin initials to canonical codes (e.g. SSE:600519). Use this first whenever the agent has a description but needs a canonical_code for downstream calls. AH dual-listing: when a company is listed on both A-share and HK (e.g. 工商银行 SSE:601398 / HK:01398), prefer the A-share canonical for all downstream analysis unless the user explicitly asks for the HK side.",
34
34
  inputSchema: {
35
35
  text: z
36
36
  .string()
@@ -0,0 +1,185 @@
1
+ /**
2
+ * `echopai market ...` + MCP tools `market_status` / `market_movers`.
3
+ *
4
+ * - status — A 股市场会话状态:pre-open / open / lunch / closed +
5
+ * 当前/下一交易日 + 节假日 flag。非计费,agent 在 quote / bars 前调一次
6
+ * 决定盘前回退口径。
7
+ * - movers — /market 行情榜单:客户端对 `quote.scan` 的 ~5800 只 A 股
8
+ * snapshot 按用户选择的字段 (pct / speed / amount / turnover / total_mv /
9
+ * pct_asc) 排序后取 top N。仅本地排序,agent 不需要自行下载 3 MB 再筛。
10
+ *
11
+ * `market_movers` 不是新的 partner endpoint,而是 CLI 端对 `quote.scan` 的
12
+ * 客户端排序 + slice。背后 op 还是 `quote.scan`,scope 一致(quote:*)。
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
+ export const marketStatusSpec = {
20
+ name: "market_status",
21
+ description: "Current A-share market session: pre-open / open / lunch / closed; current and next trading day; holiday flag. Cheap and non-billable — call it before any quote / bars fetch when the agent needs to decide pre-market fallback vs intraday vs after-close behavior.",
22
+ inputSchema: {},
23
+ handler: async (_args, ctx) => {
24
+ const op = OPERATIONS["market.status"];
25
+ if (!op)
26
+ throw new Error("market.status op missing");
27
+ return callOp(op, {}, ctx);
28
+ },
29
+ backingOps: ["market.status"],
30
+ };
31
+ const EXCHANGE_VALUES = ["SSE", "SZSE", "BSE"];
32
+ const INCLUDE_VALUES = ["active", "all"];
33
+ // `pct_asc` = 跌幅榜 (ascending pct); 其余字段均按 desc 取头部.
34
+ const MOVERS_SORT_VALUES = [
35
+ "pct",
36
+ "pct_asc",
37
+ "speed",
38
+ "amount",
39
+ "turnover",
40
+ "total_mv",
41
+ ];
42
+ function moversFieldOf(sort) {
43
+ switch (sort) {
44
+ case "pct":
45
+ case "pct_asc":
46
+ return "pct";
47
+ case "speed":
48
+ return "speed_3min";
49
+ case "turnover":
50
+ return "turnover_rate";
51
+ case "total_mv":
52
+ return "total_mv";
53
+ case "amount":
54
+ default:
55
+ return "amount";
56
+ }
57
+ }
58
+ function numericOrNeg(v) {
59
+ if (typeof v === "number" && Number.isFinite(v))
60
+ return v;
61
+ if (typeof v === "string") {
62
+ const n = Number(v);
63
+ if (Number.isFinite(n))
64
+ return n;
65
+ }
66
+ return Number.NEGATIVE_INFINITY;
67
+ }
68
+ export const marketMoversSpec = {
69
+ name: "market_movers",
70
+ description: "A-share market movers — top N from the full real-time snapshot, sorted by chosen field. Sort keys: `pct` (涨幅榜) / `pct_asc` (跌幅榜) / `speed` (3-min 涨速 speed_3min) / `amount` (成交额) / `turnover` (换手率 turnover_rate) / `total_mv` (总市值). Backed by `quote.scan` so 9:00-9:14 集合竞价时段会返空 (with `note`). Defaults: sort=pct, top=20, include=active (排除 ST / 退市). Use this for /market-style 'who's running today' instead of pulling the full 3 MB scan and sorting client-side.",
71
+ inputSchema: {
72
+ sort: z
73
+ .enum(MOVERS_SORT_VALUES)
74
+ .default("pct")
75
+ .describe("Sort field: pct / pct_asc / speed / amount / turnover / total_mv"),
76
+ top: z
77
+ .number()
78
+ .int()
79
+ .min(1)
80
+ .max(500)
81
+ .default(20)
82
+ .describe("Top N items (1-500, default 20)"),
83
+ exchange: z
84
+ .enum(EXCHANGE_VALUES)
85
+ .optional()
86
+ .describe("Narrow to one exchange: SSE / SZSE / BSE"),
87
+ include: z
88
+ .enum(INCLUDE_VALUES)
89
+ .default("active")
90
+ .describe("active = 排除 ST/退市(默认); all = 全集(含 ST + 退市 + 未知 status)"),
91
+ },
92
+ handler: async (args, ctx) => {
93
+ const op = OPERATIONS["quote.scan"];
94
+ if (!op)
95
+ throw new Error("quote.scan op missing");
96
+ const callArgs = {};
97
+ if (args.exchange)
98
+ callArgs.exchange = args.exchange;
99
+ // quote.scan input schema currently exposes only `exchange`; `include`
100
+ // is supported by the server side (see stockpulse-rs stocks.rs) but
101
+ // not yet declared in the partner inputSchema. Pass via raw URL anyway
102
+ // by injecting before callOp — but to avoid ajv strict mode kicking,
103
+ // we route through callOp which has additionalProperties:false. So we
104
+ // currently DON'T forward `include` until the contract picks it up.
105
+ // Default server behavior (`active`) matches our CLI default; if the
106
+ // user explicitly wants `all`, fall through to verb-level filter below.
107
+ const env = await callOp(op, callArgs, ctx);
108
+ return sliceMoversEnvelope(env, args.sort, Number(args.top ?? 20), args.include === "all");
109
+ },
110
+ backingOps: ["quote.scan"],
111
+ };
112
+ function sliceMoversEnvelope(env, sort, top, _includeAll) {
113
+ const body = env.data;
114
+ if (!body || typeof body !== "object" || !Array.isArray(body.items)) {
115
+ return env;
116
+ }
117
+ const items = body.items.slice();
118
+ const field = moversFieldOf(sort);
119
+ const asc = sort === "pct_asc";
120
+ items.sort((a, b) => {
121
+ const av = numericOrNeg(a[field]);
122
+ const bv = numericOrNeg(b[field]);
123
+ return asc ? av - bv : bv - av;
124
+ });
125
+ const sliced = items.slice(0, Math.max(1, Math.floor(top)));
126
+ const data = {
127
+ ...body,
128
+ items: sliced,
129
+ sort,
130
+ sort_field: field,
131
+ top,
132
+ };
133
+ const meta = {
134
+ ...(env.meta ?? {}),
135
+ client_sort: sort,
136
+ client_sort_field: field,
137
+ client_top: top,
138
+ };
139
+ return { data, meta };
140
+ }
141
+ function clampInt(raw, min, max, fallback) {
142
+ const n = Math.floor(Number(raw));
143
+ if (!Number.isFinite(n))
144
+ return fallback;
145
+ if (n < min)
146
+ return min;
147
+ if (n > max)
148
+ return max;
149
+ return n;
150
+ }
151
+ export function buildMarketCommand() {
152
+ const cmd = new Command("market").description("Market session state + real-time movers (涨幅/涨速/成交额/换手率/总市值).");
153
+ const status = cmd
154
+ .command("status")
155
+ .description(marketStatusSpec.description);
156
+ status.action(async () => {
157
+ if (!OPERATIONS["market.status"]) {
158
+ emitVerbError("internal_error", "market.status missing", undefined, 2);
159
+ }
160
+ await executeVerb(async (ctx) => marketStatusSpec.handler({}, ctx));
161
+ });
162
+ const movers = cmd.command("movers").description(marketMoversSpec.description);
163
+ movers.addOption(new Option("--sort <field>", "Sort field")
164
+ .choices([...MOVERS_SORT_VALUES])
165
+ .default("pct"));
166
+ movers.addOption(new Option("--top <n>", "Top N items (1-500)").default("20"));
167
+ movers.addOption(new Option("--exchange <ex>", "Filter by exchange").choices([...EXCHANGE_VALUES]));
168
+ movers.addOption(new Option("--include <s>", "active (默认排除 ST/退市) | all")
169
+ .choices([...INCLUDE_VALUES])
170
+ .default("active"));
171
+ movers.action(async (opts) => {
172
+ if (!OPERATIONS["quote.scan"]) {
173
+ emitVerbError("internal_error", "quote.scan missing", undefined, 2);
174
+ }
175
+ const args = {
176
+ sort: opts.sort,
177
+ top: clampInt(opts.top, 1, 500, 20),
178
+ include: opts.include,
179
+ };
180
+ if (opts.exchange)
181
+ args.exchange = opts.exchange;
182
+ await executeVerb(async (ctx) => marketMoversSpec.handler(args, ctx));
183
+ });
184
+ return cmd;
185
+ }
@@ -10,12 +10,17 @@ import { executeVerb } from "../runtime/verb_cmd.js";
10
10
  import { callOp } from "../runtime/verb_runner.js";
11
11
  export const newsSpec = {
12
12
  name: "news",
13
- description: "SUPPLEMENTARY news / market briefs (short time-horizon event stream). Use ONLY to fill gaps not covered by `views`; prefer `views` as the primary research source. With `query` → full-text search; without `query` → time-window feed.",
13
+ description: "SUPPLEMENTARY news / market briefs (short time-horizon event stream). Use ONLY to fill gaps not covered by `views`; prefer `views` as the primary research source. Three modes: `code` (canonical) → news mentioning that security; `query` (free text) → full-text search; neitherrecent time-window feed. AH dual-listing: when filtering by `code` for an A+H listed company, pass the A-share canonical (e.g. SSE:601398) for consolidated coverage; HK side only if the user explicitly asks. Note: `query` is matched against news text, so passing a canonical_code as `query` will not work — use `code` for security filtering.",
14
14
  inputSchema: {
15
+ code: z
16
+ .string()
17
+ .regex(/^((SSE|SZSE|BSE):[0-9]{6}|HK:[0-9]{5}|US:[A-Z][A-Z0-9.\-]{0,5})$/)
18
+ .optional()
19
+ .describe("Filter by security canonical_code (e.g. SSE:600519, HK:00700). Takes precedence over `query`."),
15
20
  query: z
16
21
  .string()
17
22
  .optional()
18
- .describe("Free-text query (Chinese or English); omit for time-window feed"),
23
+ .describe("Free-text query (Chinese or English) matched against news content; omit for time-window feed. Do NOT pass canonical_code here — use `code` instead."),
19
24
  hours: z
20
25
  .number()
21
26
  .int()
@@ -26,6 +31,12 @@ export const newsSpec = {
26
31
  limit: z.number().int().min(1).max(100).default(20).describe("Max items"),
27
32
  },
28
33
  handler: async (args, ctx) => {
34
+ if (args.code) {
35
+ const op = OPERATIONS["news.list"];
36
+ if (!op)
37
+ throw new Error("news.list op missing");
38
+ return callOp(op, { security: args.code, since_hours: args.hours, limit: args.limit }, ctx);
39
+ }
29
40
  if (args.query) {
30
41
  const op = OPERATIONS["news.search"];
31
42
  if (!op)
@@ -37,7 +48,7 @@ export const newsSpec = {
37
48
  throw new Error("news.feed op missing");
38
49
  return callOp(op, {}, ctx);
39
50
  },
40
- backingOps: ["news.search", "news.feed"],
51
+ backingOps: ["news.list", "news.search", "news.feed"],
41
52
  };
42
53
  function clamp(raw, min, max, fallback) {
43
54
  const n = Math.floor(Number(raw));
@@ -51,6 +62,7 @@ function clamp(raw, min, max, fallback) {
51
62
  }
52
63
  export function buildNewsCommand() {
53
64
  const cmd = new Command(newsSpec.name).description(newsSpec.description);
65
+ cmd.addOption(new Option("--code <canonical_code>", "Filter by security canonical_code (e.g. SSE:600519); takes precedence over --query"));
54
66
  cmd.addOption(new Option("--query <text>", "Free-text query; omit for time-window feed"));
55
67
  cmd.addOption(new Option("--hours <n>", "Lookback window (1-168 hours)").default("24"));
56
68
  cmd.addOption(new Option("--limit <n>", "Max items").default("20"));
@@ -59,6 +71,8 @@ export function buildNewsCommand() {
59
71
  hours: clamp(opts.hours, 1, 168, 24),
60
72
  limit: clamp(opts.limit, 1, 100, 20),
61
73
  };
74
+ if (opts.code)
75
+ args.code = opts.code;
62
76
  if (opts.query)
63
77
  args.query = opts.query;
64
78
  await executeVerb(async (ctx) => newsSpec.handler(args, ctx));
@@ -9,7 +9,7 @@ import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
9
9
  import { callOp } from "../runtime/verb_runner.js";
10
10
  export const quoteSpec = {
11
11
  name: "quote",
12
- description: "Real-time quote for 1-200 A-share securities (last price, volume, change %, bid/ask). For >200 codes use `scan` instead.",
12
+ description: "Real-time quote for 1-200 A-share securities (last price, volume, change %, bid/ask). For >200 codes use `scan` instead. AH dual-listing: HK codes are not supported here; for an A+H listed company use the A-share canonical (e.g. SSE:601398 not HK:01398).",
13
13
  inputSchema: {
14
14
  codes: z
15
15
  .array(z.string().regex(/^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/))