@dshtrading/kit-hk 0.1.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.
@@ -0,0 +1,28 @@
1
+ import { FinancialReportMatrix, FundamentalsPackage, StockFundamentals } from "./api/lib/index.js";
2
+ //#region src/fundamentals.d.ts
3
+ interface HkFundamentalsOptions {
4
+ symbol: string;
5
+ fetch?: typeof globalThis.fetch;
6
+ }
7
+ interface HkFundamentalsResult {
8
+ data?: StockFundamentals;
9
+ amplitudePercent?: number;
10
+ turnoverValueHkd?: number;
11
+ nameEn?: string;
12
+ unavailable?: string[];
13
+ }
14
+ /** 规范化港股代码:接受 00700 / 700 / 00700.HK / 700.hk,输出 5 位补零代码与规范符号 00700.HK。 */
15
+ declare function normalizeHkSymbol(input: string): {
16
+ code5: string;
17
+ canonical: string;
18
+ };
19
+ declare function fetchHkFundamentals(options: HkFundamentalsOptions): Promise<HkFundamentalsResult>;
20
+ /** 格式化港股财报日期为期别标签(YYYY/Q1, YYYY/H1, YYYY/Q3, YYYY/FY)。 */
21
+ declare function formatHkReportPeriod(dateStr: string): string;
22
+ /** 从东财/公开端点动态拉取港股多期财务指标。 */
23
+ declare function fetchHkFinancialMatrix(symbol: string, fetchImpl?: typeof globalThis.fetch): Promise<FinancialReportMatrix | undefined>;
24
+ /** 获取完整港股基本面数据包。 */
25
+ declare function fetchHkFundamentalsPackage(symbol: string, fetchImpl?: typeof globalThis.fetch): Promise<FundamentalsPackage>;
26
+ declare function renderHkFundamentals(result: HkFundamentalsResult, requestedSymbol: string): string;
27
+ //#endregion
28
+ export { HkFundamentalsOptions, HkFundamentalsResult, fetchHkFinancialMatrix, fetchHkFundamentals, fetchHkFundamentalsPackage, formatHkReportPeriod, normalizeHkSymbol, renderHkFundamentals };
@@ -0,0 +1,216 @@
1
+ //#region src/fundamentals.ts
2
+ const TENCENT_HK_QUOTE_BASE = "https://qt.gtimg.cn/q=r_hk";
3
+ /**
4
+ * 东财公开端点统一取数(2026-09-02 审查整改):最小 UA(不做浏览器伪装/伪造
5
+ * Referer,docs/replication.md 数据源边界)+ 10s AbortSignal 超时(对齐
6
+ * connector-tencent 模式,防爬取端点挂起拖死桥请求)。
7
+ */
8
+ const UPSTREAM_TIMEOUT_MS = 1e4;
9
+ async function fetchJsonUpstream(url, fetchImpl) {
10
+ const res = await fetchImpl(url, {
11
+ headers: { "User-Agent": "Mozilla/5.0" },
12
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
13
+ });
14
+ if (!res.ok) return void 0;
15
+ return await res.json();
16
+ }
17
+ /** 规范化港股代码:接受 00700 / 700 / 00700.HK / 700.hk,输出 5 位补零代码与规范符号 00700.HK。 */
18
+ function normalizeHkSymbol(input) {
19
+ const raw = input.trim().toLowerCase().replace(/\.hk$/, "");
20
+ if (!/^\d{1,5}$/.test(raw)) throw new Error(`hk_get_fundamentals: invalid HK stock symbol ${JSON.stringify(input)} — expected e.g. 00700, 700, 00700.HK`);
21
+ const code5 = raw.padStart(5, "0");
22
+ return {
23
+ code5,
24
+ canonical: `${code5}.HK`
25
+ };
26
+ }
27
+ function num(val) {
28
+ if (val === void 0 || val === "") return void 0;
29
+ const n = Number(val);
30
+ return Number.isFinite(n) ? n : void 0;
31
+ }
32
+ async function fetchHkFundamentals(options) {
33
+ const fetchImpl = options.fetch ?? globalThis.fetch;
34
+ const { code5, canonical } = normalizeHkSymbol(options.symbol);
35
+ const unavailable = [];
36
+ try {
37
+ const res = await fetchImpl(`${TENCENT_HK_QUOTE_BASE}${code5}`);
38
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
39
+ const buf = await res.arrayBuffer();
40
+ const text = new TextDecoder("gbk").decode(buf);
41
+ const match = /="([^"]+)"/.exec(text);
42
+ if (!match || !match[1]) throw new Error(`no quote data returned for r_hk${code5}`);
43
+ const fields = match[1].split("~");
44
+ if (fields.length < 45) throw new Error(`incomplete quote data received for r_hk${code5} (fields: ${fields.length})`);
45
+ const name = fields[1];
46
+ num(fields[3]);
47
+ const turnoverValueHkd = num(fields[37]);
48
+ const peDynamic = num(fields[39]);
49
+ const amplitudePercent = num(fields[43]);
50
+ const totalMarketCapYi = num(fields[44]);
51
+ const floatMarketCapYi = num(fields[45]);
52
+ const nameEn = fields[46];
53
+ const dividendYield = num(fields[47]);
54
+ const fiftyTwoWeekHigh = num(fields[48]);
55
+ const fiftyTwoWeekLow = num(fields[49]);
56
+ const peTtm = num(fields[57]) ?? peDynamic;
57
+ const pb = num(fields[58]);
58
+ const turnoverRate = num(fields[59]);
59
+ return {
60
+ data: {
61
+ symbol: canonical,
62
+ name,
63
+ marketCap: totalMarketCapYi ? totalMarketCapYi * 1e8 : void 0,
64
+ floatMarketCap: floatMarketCapYi ? floatMarketCapYi * 1e8 : void 0,
65
+ peTtm,
66
+ peDynamic,
67
+ pb,
68
+ dividendYield: dividendYield ? dividendYield / 100 : void 0,
69
+ turnoverRate,
70
+ fiftyTwoWeekHigh,
71
+ fiftyTwoWeekLow,
72
+ timestamp: Date.now()
73
+ },
74
+ amplitudePercent,
75
+ turnoverValueHkd,
76
+ nameEn,
77
+ unavailable
78
+ };
79
+ } catch (err) {
80
+ unavailable.push(`tencent-hk-quote: ${err instanceof Error ? err.message : String(err)}`);
81
+ return { unavailable };
82
+ }
83
+ }
84
+ /** 格式化港股财报日期为期别标签(YYYY/Q1, YYYY/H1, YYYY/Q3, YYYY/FY)。 */
85
+ function formatHkReportPeriod(dateStr) {
86
+ if (!dateStr) return "";
87
+ const m = /(\d{4})[-/](\d{2})[-/](\d{2})/.exec(dateStr);
88
+ if (!m) return dateStr.slice(0, 7);
89
+ const [, y, mo] = m;
90
+ if (mo === "03" || mo === "3") return `${y}/Q1`;
91
+ if (mo === "06" || mo === "6") return `${y}/H1`;
92
+ if (mo === "09" || mo === "9") return `${y}/Q3`;
93
+ if (mo === "12") return `${y}/FY`;
94
+ return `${y}/${mo}`;
95
+ }
96
+ /** 从东财/公开端点动态拉取港股多期财务指标。 */
97
+ async function fetchHkFinancialMatrix(symbol, fetchImpl = globalThis.fetch) {
98
+ try {
99
+ const { code5 } = normalizeHkSymbol(symbol);
100
+ const json = await fetchJsonUpstream(`https://datacenter-web.eastmoney.com/api/data/v1/get?reportName=RPT_HKF10_FN_MAININDICATOR&columns=ALL&filter=(SECUCODE%3D%22${code5}.HK%22)&pageNumber=1&pageSize=8&sortTypes=-1&sortColumns=REPORT_DATE`, fetchImpl);
101
+ if (json === void 0) return void 0;
102
+ const rawRows = json.result?.data;
103
+ if (!Array.isArray(rawRows) || rawRows.length === 0) return void 0;
104
+ const list = [...rawRows].reverse();
105
+ const periods = list.map((r) => formatHkReportPeriod(String(r.REPORT_DATE ?? "")));
106
+ const latestPeriod = periods[periods.length - 1] ?? "";
107
+ const latestReportTitle = latestPeriod ? `${latestPeriod.replace("/", "财年")} 财报` : void 0;
108
+ const makeRow = (id, name, valKey, yoyKey, unit) => {
109
+ const values = {};
110
+ list.forEach((r, idx) => {
111
+ const p = periods[idx];
112
+ const rawVal = r[valKey];
113
+ const val = typeof rawVal === "number" && Number.isFinite(rawVal) ? rawVal : void 0;
114
+ let changePercent;
115
+ if (yoyKey && typeof r[yoyKey] === "number") changePercent = r[yoyKey];
116
+ values[p] = {
117
+ value: val,
118
+ changePercent
119
+ };
120
+ });
121
+ return {
122
+ id,
123
+ name,
124
+ unit,
125
+ values
126
+ };
127
+ };
128
+ return {
129
+ currency: "HKD",
130
+ latestReportTitle,
131
+ periods,
132
+ groups: [
133
+ {
134
+ id: "per_share",
135
+ title: "每股指标",
136
+ rows: [
137
+ makeRow("bps", "每股净资产", "BPS", "BPS_YOY", "HKD"),
138
+ makeRow("basic_eps", "基本每股收益", "BASIC_EPS", "BASIC_EPS_YOY", "HKD"),
139
+ makeRow("dividend_ps", "每股股息", "DPS", void 0, "HKD")
140
+ ]
141
+ },
142
+ {
143
+ id: "profitability",
144
+ title: "盈利能力",
145
+ rows: [
146
+ makeRow("gross_margin", "销售毛利率", "GROSS_PROFIT_RATIO", void 0, "%"),
147
+ makeRow("net_margin", "销售净利率", "NET_PROFIT_RATIO", void 0, "%"),
148
+ makeRow("roe", "净资产收益率 (ROE)", "ROE", void 0, "%"),
149
+ makeRow("roa", "总资产收益率 (ROA)", "ROA", void 0, "%")
150
+ ]
151
+ },
152
+ {
153
+ id: "growth",
154
+ title: "收益与成长",
155
+ rows: [makeRow("revenue", "营业总收入", "TOTAL_OPERATE_INCOME", "TOTAL_OPERATE_INCOME_YOY", "HKD"), makeRow("net_profit", "股东应占溢利/净利润", "PARENT_NETPROFIT", "PARENT_NETPROFIT_YOY", "HKD")]
156
+ }
157
+ ]
158
+ };
159
+ } catch {
160
+ return;
161
+ }
162
+ }
163
+ /** 获取完整港股基本面数据包。 */
164
+ async function fetchHkFundamentalsPackage(symbol, fetchImpl = globalThis.fetch) {
165
+ const { canonical } = normalizeHkSymbol(symbol);
166
+ const [quoteRes, matrix] = await Promise.all([fetchHkFundamentals({
167
+ symbol,
168
+ fetch: fetchImpl
169
+ }), fetchHkFinancialMatrix(symbol, fetchImpl)]);
170
+ const stock = quoteRes.data ? {
171
+ ...quoteRes.data,
172
+ amplitudePercent: quoteRes.amplitudePercent
173
+ } : void 0;
174
+ return {
175
+ market: "hk",
176
+ symbol: canonical,
177
+ stock,
178
+ matrix,
179
+ profile: stock ? {
180
+ symbol: canonical,
181
+ name: stock.name ?? quoteRes.nameEn,
182
+ description: `${stock.name ?? canonical}(港股上市公司),包含港股每股指标、盈利能力与历史多期财报。`
183
+ } : void 0
184
+ };
185
+ }
186
+ function renderHkFundamentals(result, requestedSymbol) {
187
+ const { data, amplitudePercent, turnoverValueHkd, nameEn, unavailable = [] } = result;
188
+ if (!data) return `hk_get_fundamentals ${requestedSymbol}: no fundamental data available.${unavailable.length > 0 ? ` (errors: ${unavailable.join("; ")})` : ""}`;
189
+ const lines = [`hk_get_fundamentals ${data.symbol}${data.name ? ` (${data.name})` : ""}${nameEn ? ` [${nameEn}]` : ""}:`];
190
+ if (data.marketCap !== void 0) {
191
+ const yi = (data.marketCap / 1e8).toFixed(2);
192
+ lines.push(`- 总市值: ${yi} 亿港元 (HKD)`);
193
+ }
194
+ if (data.floatMarketCap !== void 0) {
195
+ const yi = (data.floatMarketCap / 1e8).toFixed(2);
196
+ lines.push(`- 流通市值: ${yi} 亿港元 (HKD)`);
197
+ }
198
+ if (data.peTtm !== void 0) lines.push(`- 滚动市盈率 (PE TTM): ${data.peTtm.toFixed(2)}`);
199
+ if (data.peDynamic !== void 0) lines.push(`- 动态市盈率: ${data.peDynamic.toFixed(2)}`);
200
+ if (data.pb !== void 0) lines.push(`- 市净率 (PB): ${data.pb.toFixed(2)}`);
201
+ if (data.dividendYield !== void 0) {
202
+ const pct = (data.dividendYield * 100).toFixed(2);
203
+ lines.push(`- 股息率 (Dividend Yield): ${pct}%`);
204
+ }
205
+ if (data.turnoverRate !== void 0) lines.push(`- 换手率: ${data.turnoverRate.toFixed(2)}%`);
206
+ if (amplitudePercent !== void 0) lines.push(`- 振幅: ${amplitudePercent.toFixed(2)}%`);
207
+ if (turnoverValueHkd !== void 0) {
208
+ const yi = (turnoverValueHkd / 1e8).toFixed(2);
209
+ lines.push(`- 今日成交额: ${yi} 亿港元`);
210
+ }
211
+ if (data.fiftyTwoWeekLow !== void 0 && data.fiftyTwoWeekHigh !== void 0) lines.push(`- 52 周最高/最低: HK$${data.fiftyTwoWeekLow.toFixed(3)} ~ HK$${data.fiftyTwoWeekHigh.toFixed(3)}`);
212
+ if (unavailable.length > 0) lines.push(` (errors: ${unavailable.join("; ")})`);
213
+ return lines.join("\n");
214
+ }
215
+ //#endregion
216
+ export { fetchHkFinancialMatrix, fetchHkFundamentals, fetchHkFundamentalsPackage, formatHkReportPeriod, normalizeHkSymbol, renderHkFundamentals };
package/lib/index.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { HkFundamentalsOptions, HkFundamentalsResult, fetchHkFinancialMatrix, fetchHkFundamentals, fetchHkFundamentalsPackage, formatHkReportPeriod, normalizeHkSymbol, renderHkFundamentals } from "./fundamentals.js";
2
+ import { AggregateNewsOptions, AggregateNewsResult, NewsItem, NewsSource, aggregateNews, isHkRelevant, parseHkShowTime, parseHkexDateTime, resetHkexAnnouncementMemo } from "./news.js";
3
+ import Schema from "@deepseek-ai/schemastery";
4
+ import { SkillProvider } from "@deepseek-ai/dsh-skill";
5
+ import { Context } from "@deepseek-ai/cordis";
6
+ //#region src/index.d.ts
7
+ declare const provider: SkillProvider;
8
+ interface Config {
9
+ dryRun: boolean;
10
+ liveTrading: boolean;
11
+ }
12
+ declare const Config: Schema<Config>;
13
+ declare const inject: string[];
14
+ declare const name = "dsh-trading-hk-kit";
15
+ declare function apply(ctx: Context, _config: Config): void;
16
+ declare function createGetNewsTool(): import("@deepseek-ai/dsh-tools").ToolDefinition;
17
+ declare function createGetFundamentalsTool(options?: {
18
+ fetch?: typeof globalThis.fetch;
19
+ }): import("@deepseek-ai/dsh-tools").ToolDefinition;
20
+ //#endregion
21
+ export { AggregateNewsOptions, AggregateNewsResult, Config, HkFundamentalsOptions, HkFundamentalsResult, NewsItem, NewsSource, aggregateNews, apply, createGetFundamentalsTool, createGetNewsTool, fetchHkFinancialMatrix, fetchHkFundamentals, fetchHkFundamentalsPackage, formatHkReportPeriod, inject, isHkRelevant, name, normalizeHkSymbol, parseHkShowTime, parseHkexDateTime, provider, renderHkFundamentals, resetHkexAnnouncementMemo };
package/lib/index.js ADDED
@@ -0,0 +1,224 @@
1
+ import { aggregateNews, isHkRelevant, parseHkShowTime, parseHkexDateTime, resetHkexAnnouncementMemo } from "./news.js";
2
+ import { fetchHkFinancialMatrix, fetchHkFundamentals, fetchHkFundamentalsPackage, formatHkReportPeriod, normalizeHkSymbol, renderHkFundamentals } from "./fundamentals.js";
3
+ import { readFile } from "node:fs/promises";
4
+ import { fileURLToPath } from "node:url";
5
+ import Schema from "@deepseek-ai/schemastery";
6
+ import { BUNDLED_SKILL_RANK } from "@deepseek-ai/dsh-skill";
7
+ import { defineTool } from "@deepseek-ai/dsh-tools";
8
+ import { createGetIndicatorsTool } from "@dshtrading/indicators/tool";
9
+ //#region src/index.ts
10
+ /**
11
+ * HK 工具箱插件(dsh-trading hk 切片)。
12
+ *
13
+ * 包含:
14
+ * 1. skill provider:hk-risk-checklist、indicator-authoring、trading-strategy-paradigms、knowledge-curation 与 trading-notes-setup 随包分发;
15
+ * 2. hk_get_news 与 hk_get_fundamentals 工具;
16
+ * 3. indicator_author 创作工具(Issue #19);
17
+ * 4. knowledge_ingest 与 knowledge_search 知识库工具(Issue #24)。
18
+ *
19
+ * @module @dshtrading/kit-hk
20
+ */
21
+ const PROVIDER_NAME = "dsh-trading-hk";
22
+ const SKILL_BODY_URL = new URL("../assets/skills/hk-risk-checklist.md", import.meta.url);
23
+ const AUTHORING_BODY_URL = new URL("../assets/skills/indicator-authoring.md", import.meta.url);
24
+ const STRATEGY_BODY_URL = new URL("../assets/skills/trading-strategy-paradigms.md", import.meta.url);
25
+ const KNOWLEDGE_CURATION_BODY_URL = new URL("../assets/skills/knowledge-curation.md", import.meta.url);
26
+ const JOURNAL_BODY_URL = new URL("../assets/skills/trading-notes-setup.md", import.meta.url);
27
+ const RESOURCE_BASE = {
28
+ kind: "directory",
29
+ path: fileURLToPath(new URL("../assets/skills/", import.meta.url))
30
+ };
31
+ const CANDIDATE = {
32
+ name: "hk-risk-checklist",
33
+ description: "港股交易风控检查清单:开仓前逐项核对 T+0 回转与无涨跌幅限制、碎股(board lot)与手数、供股/配股摊薄、窝轮牛熊证杠杆与强制收回、港元汇率与港股通差异。",
34
+ invocation: {
35
+ modelInvocable: true,
36
+ userInvocable: true
37
+ },
38
+ provider: PROVIDER_NAME,
39
+ source: "bundled",
40
+ resourceBase: RESOURCE_BASE,
41
+ rank: BUNDLED_SKILL_RANK,
42
+ locator: SKILL_BODY_URL
43
+ };
44
+ const SKILL_CANDIDATES = [
45
+ CANDIDATE,
46
+ {
47
+ name: "indicator-authoring",
48
+ description: "自定义技术指标创作指南:根据用户自然语言需求生成符合契约的指标代码(TD9/SuperTrend/OBV+MA等),并通过 indicator_author 工具验证与落库。",
49
+ invocation: {
50
+ modelInvocable: true,
51
+ userInvocable: true
52
+ },
53
+ provider: PROVIDER_NAME,
54
+ source: "bundled",
55
+ resourceBase: RESOURCE_BASE,
56
+ rank: BUNDLED_SKILL_RANK,
57
+ locator: AUTHORING_BODY_URL
58
+ },
59
+ {
60
+ name: "trading-strategy-paradigms",
61
+ description: "经典交易策略参考范式指南:提供短线(唐奇安突破/RSI极值回归)、波段(EMA双均线/布林带下轨回归)、长线(200日均线基线/12月动量)6大策略原理、参数调优、8项回测指标研读与风险防范 SOP。",
62
+ invocation: {
63
+ modelInvocable: true,
64
+ userInvocable: true
65
+ },
66
+ provider: PROVIDER_NAME,
67
+ source: "bundled",
68
+ resourceBase: RESOURCE_BASE,
69
+ rank: BUNDLED_SKILL_RANK,
70
+ locator: STRATEGY_BODY_URL
71
+ },
72
+ {
73
+ name: "knowledge-curation",
74
+ description: "财经观点沉淀与知识库策展指南:基于 Content Insight 事实核查产物,规范化提取知识卡片字段、受控词表对齐、查重与关联建立,通过 knowledge_ingest 工具入库。",
75
+ invocation: {
76
+ modelInvocable: true,
77
+ userInvocable: true
78
+ },
79
+ provider: PROVIDER_NAME,
80
+ source: "bundled",
81
+ resourceBase: RESOURCE_BASE,
82
+ rank: BUNDLED_SKILL_RANK,
83
+ locator: KNOWLEDGE_CURATION_BODY_URL
84
+ },
85
+ {
86
+ name: "trading-notes-setup",
87
+ description: "交易日志建立与记录规范:检查/创建工作区 .trading-journal/ 双轨目录(agent 轨 + human 轨),分别记录 agent 与人类各自的操作。会话启动检查发现工作区没有交易日志目录时调用本技能建立骨架;记录条目格式以本技能为权威。",
88
+ invocation: {
89
+ modelInvocable: true,
90
+ userInvocable: true
91
+ },
92
+ provider: PROVIDER_NAME,
93
+ source: "bundled",
94
+ resourceBase: RESOURCE_BASE,
95
+ rank: BUNDLED_SKILL_RANK,
96
+ locator: JOURNAL_BODY_URL
97
+ }
98
+ ];
99
+ const provider = {
100
+ name: PROVIDER_NAME,
101
+ list: () => Promise.resolve(SKILL_CANDIDATES),
102
+ async get(candidate) {
103
+ const target = SKILL_CANDIDATES.find((c) => c.name === candidate.name) ?? CANDIDATE;
104
+ return {
105
+ name: target.name,
106
+ description: target.description,
107
+ invocation: target.invocation,
108
+ provider: target.provider,
109
+ source: target.source,
110
+ resourceBase: RESOURCE_BASE,
111
+ content: await readFile(target.locator, "utf8")
112
+ };
113
+ }
114
+ };
115
+ const Config = Schema.object({
116
+ dryRun: Schema.boolean().default(true),
117
+ liveTrading: Schema.boolean().default(false)
118
+ });
119
+ const inject = ["skills", "tools"];
120
+ const name = "dsh-trading-hk-kit";
121
+ function apply(ctx, _config) {
122
+ ctx.skills.registerProvider(() => provider);
123
+ const newsTool = createGetNewsTool();
124
+ const fundamentalsTool = createGetFundamentalsTool();
125
+ const tools = ctx.tools;
126
+ const registerOnce = (tool) => {
127
+ if (tools.get(tool.name) !== void 0) {
128
+ ctx.logger("dsh-trading-hk-kit").info("[dsh-trading-hk-kit] tool %s already registered by another provider — skipped (mutual exclusion)", tool.name);
129
+ return;
130
+ }
131
+ tools.register(tool);
132
+ };
133
+ registerOnce(newsTool);
134
+ registerOnce(fundamentalsTool);
135
+ const serviceGetter = ctx;
136
+ const marketData = (serviceGetter.get?.("tradingMarketDataRegistry", false))?.active("hk")?.service ?? serviceGetter.get?.("tradingHkMarketData", false);
137
+ if (marketData !== void 0) registerOnce(createGetIndicatorsTool({
138
+ marketData,
139
+ market: "hk"
140
+ }));
141
+ const lifecycle = ctx;
142
+ lifecycle.inject?.(["tradingNewsRegistry"], (scope) => {
143
+ const registry = scope.tradingNewsRegistry;
144
+ if (registry && typeof registry.register === "function") lifecycle.effect?.(() => registry.register("hk", aggregateNews), "kit-hk news registration");
145
+ });
146
+ }
147
+ const DEFAULT_NEWS_WINDOW_HOURS = 24;
148
+ const DEFAULT_NEWS_LIMIT = 20;
149
+ function renderNewsItem(item) {
150
+ return `[${item.source}] ${item.publishedAt} ${item.title}\n ${item.url}`;
151
+ }
152
+ function createGetNewsTool() {
153
+ return defineTool({
154
+ name: "hk_get_news",
155
+ description: "Get recent Hong Kong stock market news, derived from Eastmoney financial fast-news (HK column) filtered to HK-relevant items (HKEX-listed marketId=116 codes or HK keywords). DEGRADED SOURCE — Eastmoney is a unified CN financial feed; HK coverage is PARTIAL (HK news without an HK-listed code or HK keyword is not captured; not a dedicated HK news source). Each item carries source name (东方财富), publish time and a link for traceability; fetches metadata only, never redistributes article bodies. Optionally filter by symbol (HK code, e.g. 00700 / 00700.HK) and by a time window. No credentials required.",
156
+ parameters: {
157
+ symbol: {
158
+ type: "string",
159
+ description: "Optional symbol to filter by, market-canonical vocabulary, e.g. 00700 or 00700.HK (Tencent). Best-effort matched against HK-listed stock codes."
160
+ },
161
+ windowHours: {
162
+ type: "number",
163
+ description: `Only keep items published within the last N hours (1-168, default ${DEFAULT_NEWS_WINDOW_HOURS}).`,
164
+ default: DEFAULT_NEWS_WINDOW_HOURS
165
+ },
166
+ limit: {
167
+ type: "number",
168
+ description: `Max items to return (1-50, default ${DEFAULT_NEWS_LIMIT}).`,
169
+ default: DEFAULT_NEWS_LIMIT
170
+ }
171
+ },
172
+ output: {
173
+ schema: { type: "string" },
174
+ render: (_args, value) => [{
175
+ type: "text",
176
+ text: String(value)
177
+ }]
178
+ },
179
+ async execute(raw) {
180
+ const args = raw ?? {};
181
+ const options = {
182
+ symbol: typeof args.symbol === "string" ? args.symbol : void 0,
183
+ windowHours: typeof args.windowHours === "number" ? args.windowHours : void 0,
184
+ limit: typeof args.limit === "number" ? args.limit : void 0
185
+ };
186
+ const { items, unavailable } = await aggregateNews(options);
187
+ if (items.length === 0 && unavailable.length === 0) return "hk_get_news: no news items found within the requested window (degraded source: Eastmoney HK column may have no HK-relevant items in-window).";
188
+ const symbolNote = options.symbol ? ` symbol=${options.symbol.trim()}` : "";
189
+ const lines = [`hk_get_news — ${items.length} item(s)${symbolNote}, window=${options.windowHours ?? DEFAULT_NEWS_WINDOW_HOURS}h (newest-first; DEGRADED — Eastmoney HK column, partial HK coverage):`, ...items.map(renderNewsItem)];
190
+ if (unavailable.length > 0) lines.push(" (source(s) unavailable this call: " + unavailable.join("; ") + ")");
191
+ return lines.join("\n");
192
+ }
193
+ });
194
+ }
195
+ function createGetFundamentalsTool(options = {}) {
196
+ return defineTool({
197
+ name: "hk_get_fundamentals",
198
+ description: "Get fundamental valuation and financial indicators for Hong Kong stocks (Total Market Cap, Float Market Cap, Dynamic P/E, Trailing P/E, P/B, Dividend Yield, Turnover Rate, Amplitude, Turnover Value, 52-Week Range) via Tencent HK public market quote API. Accepts market-canonical code (e.g. 00700.HK) or 1-5 digit code (700, 00700). No credentials required.",
199
+ parameters: { symbol: {
200
+ type: "string",
201
+ required: true,
202
+ description: "Hong Kong stock symbol or code, market-canonical vocabulary, e.g. 00700.HK, 09988.HK, 00700, 700"
203
+ } },
204
+ output: {
205
+ schema: { type: "string" },
206
+ render: (_args, value) => [{
207
+ type: "text",
208
+ text: String(value)
209
+ }]
210
+ },
211
+ async execute(raw) {
212
+ const args = raw ?? {};
213
+ const symbol = typeof args.symbol === "string" ? args.symbol.trim() : "";
214
+ if (!symbol) throw new Error("hk_get_fundamentals: symbol parameter is required (e.g. 00700.HK or 00700)");
215
+ const result = await fetchHkFundamentals({
216
+ symbol,
217
+ fetch: options.fetch
218
+ });
219
+ return renderHkFundamentals(result, symbol);
220
+ }
221
+ });
222
+ }
223
+ //#endregion
224
+ export { Config, aggregateNews, apply, createGetFundamentalsTool, createGetNewsTool, fetchHkFinancialMatrix, fetchHkFundamentals, fetchHkFundamentalsPackage, formatHkReportPeriod, inject, isHkRelevant, name, normalizeHkSymbol, parseHkShowTime, parseHkexDateTime, provider, renderHkFundamentals, resetHkexAnnouncementMemo };
package/lib/news.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ //#region src/news.d.ts
2
+ /**
3
+ * hk_get_news 取数层(WS4 #1:#6 子工作流;spike EVIDENCE 判定 hk 无干净公共源 → 采用「降级」方案)。
4
+ *
5
+ * 降级口径(用户裁决 2026-08-30):用东方财富快讯第 103 列(「港股」列,但实为统一 CN 金融流、覆盖不纯),
6
+ * 客户端按 `stockList` 中的**港交所 marketId=116** 代码 + 标题港股关键词过滤出港股相关标的新闻。诚实标注:
7
+ * 覆盖**部分**(港股新闻若不带港股关联代码/关键词则不捕获),非专用港股新闻源;与 CryptoPanic 的降级同理。
8
+ *
9
+ * 公告双源(2026-09-03,多供应商冗余裁决;spikes/impl-hk-cn-announce-sources/ EVIDENCE):
10
+ * 东财 ann_type=H(主源,秒级时间)+ HKEX 披露易 titleSearchServlet(备份源),allSettled 并行 +
11
+ * 跨源去重(±24h 内:归一化标题全文等值,或共同前缀 ≥6 字且共同后缀 ≥2 字——
12
+ * 2026-09-03 评审 M1 收紧:裸类别短标题/严格前缀对/泛化日期头同日不同文件不判重,
13
+ * 失败方向宁漏勿误,繁体经映射表转简体后比对)。
14
+ *
15
+ * 铁律 #5:只引 title/showTime/链接(元数据),不取 summary/正文,不再分发。每源失败 fail-soft。
16
+ */
17
+ type NewsSource = 'eastmoney' | 'eastmoney-announcement' | 'hkex-announcement';
18
+ interface NewsItem {
19
+ source: string;
20
+ title: string;
21
+ url: string;
22
+ publishedAt: string;
23
+ relatedCodes?: string[];
24
+ }
25
+ interface AggregateNewsOptions {
26
+ /** 标的(市场规范词汇,如 00700 / 00700.HK / 0700);缺省 = 不过滤。 */
27
+ symbol?: string | undefined;
28
+ windowHours?: number | undefined;
29
+ limit?: number | undefined;
30
+ fetch?: typeof globalThis.fetch | undefined;
31
+ now?: number | undefined;
32
+ /** CryptoPanic API token(桥面透传,hk 聚合器忽略;对齐 api 契约形状)。 */
33
+ cryptoPanicKey?: string | undefined;
34
+ }
35
+ interface AggregateNewsResult {
36
+ items: NewsItem[];
37
+ unavailable: string[];
38
+ }
39
+ declare function parseHkShowTime(showTime: string): number;
40
+ /** 港股相关性:stockList 含 116. 前缀代码(港交所),或 title 含港股关键词。 */
41
+ declare function isHkRelevant(item: {
42
+ title: string;
43
+ relatedCodes?: string[];
44
+ }): boolean;
45
+ /** 清空 HKEX stockId memo(单测隔离用;生产运行期不需要调用)。 */
46
+ declare function resetHkexAnnouncementMemo(): void;
47
+ /** HKEX 披露易 DATE_TIME:`DD/MM/YYYY HH:MM`(日/月倒序,港图时间东八区)→ 毫秒。 */
48
+ declare function parseHkexDateTime(value: string): number;
49
+ declare function aggregateNews(options?: AggregateNewsOptions): Promise<AggregateNewsResult>;
50
+ //#endregion
51
+ export { AggregateNewsOptions, AggregateNewsResult, NewsItem, NewsSource, aggregateNews, isHkRelevant, parseHkShowTime, parseHkexDateTime, resetHkexAnnouncementMemo };