@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.
package/lib/news.js ADDED
@@ -0,0 +1,485 @@
1
+ //#region src/news.ts
2
+ const EASTMONEY_URL = "https://np-listapi.eastmoney.com/comm/web/getFastNewsList";
3
+ const HKEX_BASE = "https://www1.hkexnews.hk";
4
+ const DEFAULT_WINDOW_HOURS = 24;
5
+ const DEFAULT_LIMIT = 20;
6
+ const MAX_LIMIT = 50;
7
+ const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (dsh-trading/hk_get_news)";
8
+ /** 下钻 fetch 统一 10s 超时(docs/replication.md §9;上游挂起不得拖垮 60s 轮询链)。 */
9
+ const UPSTREAM_TIMEOUT_MS = 1e4;
10
+ /** 港交所 marketId(stockList 前缀,如 '116.00700')。 */
11
+ const HK_MARKET_ID = "116.";
12
+ /** 港股关键词(无关联代码时仍判为港股相关)。 */
13
+ const HK_KEYWORDS = [
14
+ "港股",
15
+ "港交所",
16
+ "香港",
17
+ "恒指",
18
+ "恒生"
19
+ ];
20
+ /** 公告放宽时间窗:上市公司公告 90 天(一季度)内有效(媒体快讯仍按 24h 窗)。 */
21
+ const ANNOUNCEMENT_MAX_AGE_MS = 7776e6;
22
+ /** HKEX 公告检索请求窗:宽于 90 天过滤窗,避免时区/日期边界裁剪(DATE_TIME 港图时间)。 */
23
+ const HKEX_REQUEST_WINDOW_MS = 8208e6;
24
+ function parseHkShowTime(showTime) {
25
+ const t = Date.parse(showTime.trim().replace(" ", "T") + "+08:00");
26
+ return Number.isFinite(t) ? t : NaN;
27
+ }
28
+ /** 港股相关性:stockList 含 116. 前缀代码(港交所),或 title 含港股关键词。 */
29
+ function isHkRelevant(item) {
30
+ if (item.relatedCodes?.some((c) => c.startsWith(HK_MARKET_ID))) return true;
31
+ const title = item.title.toUpperCase();
32
+ return HK_KEYWORDS.some((k) => title.includes(k.toUpperCase()));
33
+ }
34
+ async function fetchEastmoneyHk(fetchImpl, limit) {
35
+ const url = new URL(EASTMONEY_URL);
36
+ url.searchParams.set("client", "web");
37
+ url.searchParams.set("biz", "web_724");
38
+ url.searchParams.set("sortEnd", "");
39
+ url.searchParams.set("req_trace", "1");
40
+ url.searchParams.set("fastColumn", "103");
41
+ url.searchParams.set("pageSize", String(Math.max(limit * 2, 40)));
42
+ const response = await fetchImpl(url, {
43
+ headers: {
44
+ accept: "application/json",
45
+ "user-agent": UA
46
+ },
47
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
48
+ });
49
+ if (!response.ok) {
50
+ const body = await response.text().catch(() => "");
51
+ throw new Error(`eastmoney: HTTP ${response.status}${body ? ` — ${body.slice(0, 160)}` : ""}`);
52
+ }
53
+ const list = JSON.parse(await response.text()).data?.fastNewsList;
54
+ if (!Array.isArray(list)) throw new Error("eastmoney: unexpected payload (expected data.fastNewsList[])");
55
+ const items = [];
56
+ for (const it of list) {
57
+ if (!it.title || it.code === void 0) continue;
58
+ const ts = typeof it.showTime === "string" ? parseHkShowTime(it.showTime) : NaN;
59
+ if (!Number.isFinite(ts)) continue;
60
+ const relatedCodes = Array.isArray(it.stockList) ? it.stockList.filter((s) => typeof s === "string" && s.includes(".")) : void 0;
61
+ const candidate = {
62
+ source: "eastmoney",
63
+ title: it.title,
64
+ url: `https://finance.eastmoney.com/a/${encodeURIComponent(String(it.code))}.html`,
65
+ publishedAt: new Date(ts).toISOString(),
66
+ ...relatedCodes && relatedCodes.length > 0 ? { relatedCodes } : {}
67
+ };
68
+ if (isHkRelevant(candidate)) items.push(candidate);
69
+ }
70
+ return items;
71
+ }
72
+ function inWindow(publishedAt, nowMs, windowMs) {
73
+ const ts = Date.parse(publishedAt);
74
+ if (!Number.isFinite(ts)) return false;
75
+ return ts >= nowMs - windowMs && ts <= nowMs + 6e4;
76
+ }
77
+ function matchesSymbol(item, rawSymbol) {
78
+ const needle = rawSymbol?.trim();
79
+ if (!needle) return true;
80
+ const title = item.title.toUpperCase();
81
+ const normalizedNeedle = needle.toUpperCase().replace(/\.HK$/i, "").replace(/^0+/, "");
82
+ const tokens = [needle.toUpperCase(), normalizedNeedle];
83
+ if (tokens.some((t) => t && title.includes(t))) return true;
84
+ if (item.relatedCodes) return tokens.some((t) => t && item.relatedCodes.some((c) => c.startsWith("116.") && c.slice(4).replace(/^0+/, "") === t));
85
+ return false;
86
+ }
87
+ function clampNumber(value, fallback, min, max) {
88
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
89
+ return Math.min(Math.max(Math.trunc(value), min), max);
90
+ }
91
+ async function fetchEastmoneyHkAnnouncements(fetchImpl, rawSymbol, limit) {
92
+ const clean = rawSymbol.trim().replace(/\.HK$/i, "").padStart(5, "0");
93
+ if (!/^\d{5}$/.test(clean)) return [];
94
+ const url = new URL("https://np-anotice-stock.eastmoney.com/api/security/ann");
95
+ url.searchParams.set("page_size", String(Math.max(limit, 20)));
96
+ url.searchParams.set("page_index", "1");
97
+ url.searchParams.set("ann_type", "H");
98
+ url.searchParams.set("client_source", "web");
99
+ url.searchParams.set("stock_list", clean);
100
+ const response = await fetchImpl(url, {
101
+ headers: {
102
+ accept: "application/json",
103
+ "user-agent": UA
104
+ },
105
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
106
+ });
107
+ if (!response.ok) {
108
+ const body = await response.text().catch(() => "");
109
+ throw new Error(`eastmoney-announcement: HTTP ${response.status}${body ? ` — ${body.slice(0, 160)}` : ""}`);
110
+ }
111
+ const list = JSON.parse(await response.text()).data?.list;
112
+ if (!Array.isArray(list)) throw new Error("eastmoney-announcement: unexpected payload (expected data.list[])");
113
+ const items = [];
114
+ for (const it of list) {
115
+ const title = it.title_ch || it.title;
116
+ if (!title || !it.art_code) continue;
117
+ const ts = parseHkShowTime((it.display_time || it.notice_date || "").slice(0, 19));
118
+ if (!Number.isFinite(ts)) continue;
119
+ items.push({
120
+ source: "eastmoney-announcement",
121
+ title,
122
+ url: `https://data.eastmoney.com/notices/detail/${clean}/${encodeURIComponent(it.art_code)}.html`,
123
+ publishedAt: new Date(ts).toISOString(),
124
+ relatedCodes: [`116.${clean}`]
125
+ });
126
+ }
127
+ return items;
128
+ }
129
+ /**
130
+ * 股票代码 → HKEX stockId 内码 memo。prefix.do 冷请求 ~3.7s,stockId 是稳定静态
131
+ * 映射(kit 层参考数据,非行情缓存,不违铁律 #5 桥无状态语义);仅缓存成功值。
132
+ * 失败负缓存 5 分钟(评审 L1)+ in-flight promise 合并并发(评审 L1)。
133
+ */
134
+ const hkexStockIdMemo = /* @__PURE__ */ new Map();
135
+ const hkexStockIdFailureMemo = /* @__PURE__ */ new Map();
136
+ const hkexStockIdInflight = /* @__PURE__ */ new Map();
137
+ /** 失败负缓存 TTL:5 分钟内不重试失败的 stockId lookup。 */
138
+ const LOOKUP_FAILURE_TTL_MS = 3e5;
139
+ /** 清空 HKEX stockId memo(单测隔离用;生产运行期不需要调用)。 */
140
+ function resetHkexAnnouncementMemo() {
141
+ hkexStockIdMemo.clear();
142
+ hkexStockIdFailureMemo.clear();
143
+ hkexStockIdInflight.clear();
144
+ }
145
+ /** HKEX 披露易 DATE_TIME:`DD/MM/YYYY HH:MM`(日/月倒序,港图时间东八区)→ 毫秒。 */
146
+ function parseHkexDateTime(value) {
147
+ const m = /^(\d{2})\/(\d{2})\/(\d{4})\s+(\d{2}):(\d{2})$/.exec(value.trim());
148
+ if (m === null) return NaN;
149
+ const t = Date.parse(`${m[3]}-${m[2]}-${m[1]}T${m[4]}:${m[5]}:00+08:00`);
150
+ return Number.isFinite(t) ? t : NaN;
151
+ }
152
+ async function fetchHkexStockId(fetchImpl, code) {
153
+ const cached = hkexStockIdMemo.get(code);
154
+ if (cached !== void 0) return cached;
155
+ const failedAt = hkexStockIdFailureMemo.get(code);
156
+ if (failedAt !== void 0 && Date.now() - failedAt < LOOKUP_FAILURE_TTL_MS) throw new Error("hkex: lookup recently failed (negative cache, retry later)");
157
+ let inflight = hkexStockIdInflight.get(code);
158
+ if (inflight === void 0) {
159
+ inflight = fetchHkexStockIdUncached(fetchImpl, code).finally(() => {
160
+ hkexStockIdInflight.delete(code);
161
+ });
162
+ hkexStockIdInflight.set(code, inflight);
163
+ }
164
+ return inflight;
165
+ }
166
+ async function fetchHkexStockIdUncached(fetchImpl, code) {
167
+ const url = new URL(`${HKEX_BASE}/search/prefix.do`);
168
+ url.searchParams.set("callback", "callback");
169
+ url.searchParams.set("lang", "ZH");
170
+ url.searchParams.set("type", "A");
171
+ url.searchParams.set("name", code);
172
+ url.searchParams.set("market", "SEHK");
173
+ let response;
174
+ try {
175
+ response = await fetchImpl(url, {
176
+ headers: {
177
+ accept: "*/*",
178
+ "user-agent": UA
179
+ },
180
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
181
+ });
182
+ } catch (err) {
183
+ hkexStockIdFailureMemo.set(code, Date.now());
184
+ throw err;
185
+ }
186
+ if (!response.ok) {
187
+ const body = await response.text().catch(() => "");
188
+ hkexStockIdFailureMemo.set(code, Date.now());
189
+ throw new Error(`hkex: HTTP ${response.status}${body ? ` — ${body.slice(0, 160)}` : ""}`);
190
+ }
191
+ const text = await response.text();
192
+ let parsed;
193
+ try {
194
+ parsed = JSON.parse(text.replace(/^callback\(/, "").replace(/\);\s*$/, ""));
195
+ } catch {
196
+ hkexStockIdFailureMemo.set(code, Date.now());
197
+ throw new Error("hkex: unexpected payload (invalid JSONP)");
198
+ }
199
+ const list = parsed.stockInfo;
200
+ const hit = Array.isArray(list) ? list.find((it) => it.code === code) : void 0;
201
+ if (typeof hit?.stockId !== "number") {
202
+ hkexStockIdFailureMemo.set(code, Date.now());
203
+ throw new Error("hkex: unexpected payload (expected stockInfo[].stockId for exact code match)");
204
+ }
205
+ hkexStockIdMemo.set(code, hit.stockId);
206
+ return hit.stockId;
207
+ }
208
+ /** 请求窗日期串(YYYYMMDD,东八区日历日,评审 M3:HKEX fromDate/toDate 按港图 +08:00 日界截窗,UTC 日期会在每天 00:00–07:59 漏掉当日披露)。 */
209
+ function ymdHkCompact(ms) {
210
+ return new Date(ms + 288e5).toISOString().slice(0, 10).replace(/-/g, "");
211
+ }
212
+ async function fetchHkexAnnouncements(fetchImpl, rawSymbol, limit, nowMs) {
213
+ const clean = rawSymbol.trim().replace(/\.HK$/i, "").padStart(5, "0");
214
+ if (!/^\d{5}$/.test(clean)) return [];
215
+ const stockId = await fetchHkexStockId(fetchImpl, clean);
216
+ const now = nowMs;
217
+ const url = new URL(`${HKEX_BASE}/search/titleSearchServlet.do`);
218
+ url.searchParams.set("sortDir", "0");
219
+ url.searchParams.set("sortByOptions", "DateTime");
220
+ url.searchParams.set("category", "0");
221
+ url.searchParams.set("market", "SEHK");
222
+ url.searchParams.set("stockId", String(stockId));
223
+ url.searchParams.set("documentType", "-1");
224
+ url.searchParams.set("fromDate", ymdHkCompact(now - HKEX_REQUEST_WINDOW_MS));
225
+ url.searchParams.set("toDate", ymdHkCompact(now));
226
+ url.searchParams.set("title", "");
227
+ url.searchParams.set("searchType", "1");
228
+ url.searchParams.set("t1code", "-2");
229
+ url.searchParams.set("t2Gcode", "-2");
230
+ url.searchParams.set("t2code", "-2");
231
+ url.searchParams.set("rowRange", String(Math.max(limit, 20)));
232
+ url.searchParams.set("lang", "ZH");
233
+ const response = await fetchImpl(url, {
234
+ headers: {
235
+ accept: "*/*",
236
+ "user-agent": UA
237
+ },
238
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
239
+ });
240
+ if (!response.ok) {
241
+ const body = await response.text().catch(() => "");
242
+ throw new Error(`hkex-announcement: HTTP ${response.status}${body ? ` — ${body.slice(0, 160)}` : ""}`);
243
+ }
244
+ let parsed;
245
+ try {
246
+ parsed = JSON.parse(await response.text());
247
+ } catch {
248
+ throw new Error("hkex-announcement: unexpected payload (invalid JSON)");
249
+ }
250
+ const result = parsed.result;
251
+ let rows;
252
+ try {
253
+ rows = typeof result === "string" ? JSON.parse(result) : Array.isArray(result) ? result : [];
254
+ } catch {
255
+ throw new Error("hkex-announcement: unexpected payload (invalid nested result JSON)");
256
+ }
257
+ if (!Array.isArray(rows)) throw new Error("hkex-announcement: unexpected payload (expected result[])");
258
+ const items = [];
259
+ for (const row of rows) {
260
+ if (!row.TITLE || !row.FILE_LINK) continue;
261
+ if (typeof row.STOCK_CODE === "string" && !row.STOCK_CODE.startsWith(clean)) continue;
262
+ const ts = parseHkexDateTime(row.DATE_TIME ?? "");
263
+ if (!Number.isFinite(ts)) continue;
264
+ items.push({
265
+ source: "hkex-announcement",
266
+ title: row.TITLE,
267
+ url: `${HKEX_BASE}${row.FILE_LINK}`,
268
+ publishedAt: new Date(ts).toISOString(),
269
+ relatedCodes: [`116.${clean}`]
270
+ });
271
+ }
272
+ return items;
273
+ }
274
+ /** 繁→简映射(HKEX 标题为繁体、东财转简体):仅覆盖公告标题高频字,缺字即原样保留。 */
275
+ const TRAD_TO_SIMP = {
276
+ 報: "报",
277
+ 變: "变",
278
+ 動: "动",
279
+ 購: "购",
280
+ 會: "会",
281
+ 業: "业",
282
+ 資: "资",
283
+ 訊: "讯",
284
+ 關: "关",
285
+ 聯: "联",
286
+ 營: "营",
287
+ 運: "运",
288
+ 財: "财",
289
+ 務: "务",
290
+ 長: "长",
291
+ 開: "开",
292
+ 東: "东",
293
+ 門: "门",
294
+ 項: "项",
295
+ 證: "证",
296
+ 淨: "净",
297
+ 虧: "亏",
298
+ 損: "损",
299
+ 積: "积",
300
+ 極: "极",
301
+ 環: "环",
302
+ 場: "场",
303
+ 標: "标",
304
+ 題: "题",
305
+ 續: "续",
306
+ 職: "职",
307
+ 執: "执",
308
+ 導: "导",
309
+ 師: "师",
310
+ 顧: "顾",
311
+ 問: "问",
312
+ 講: "讲",
313
+ 評: "评",
314
+ 議: "议",
315
+ 決: "决",
316
+ 賠: "赔",
317
+ 償: "偿",
318
+ 質: "质",
319
+ 貸: "贷",
320
+ 結: "结",
321
+ 費: "费",
322
+ 訂: "订",
323
+ 閱: "阅",
324
+ 億: "亿",
325
+ 兩: "两",
326
+ 廣: "广",
327
+ 廠: "厂",
328
+ 銀: "银",
329
+ 聲: "声",
330
+ 識: "识",
331
+ 體: "体",
332
+ 總: "总",
333
+ 經: "经",
334
+ 銷: "销",
335
+ 產: "产",
336
+ 賣: "卖",
337
+ 買: "买",
338
+ 賬: "账",
339
+ 戶: "户",
340
+ 權: "权",
341
+ 讓: "让",
342
+ 擔: "担",
343
+ 託: "托",
344
+ 約: "约",
345
+ 訴: "诉",
346
+ 訟: "讼",
347
+ 詢: "询",
348
+ 復: "复",
349
+ 對: "对",
350
+ 審: "审",
351
+ 計: "计",
352
+ 備: "备",
353
+ 稅: "税",
354
+ 應: "应",
355
+ 鏈: "链",
356
+ 儲: "储",
357
+ 據: "据",
358
+ 領: "领",
359
+ 補: "补",
360
+ 貼: "贴",
361
+ 凍: "冻",
362
+ 註: "注",
363
+ 績: "绩",
364
+ 預: "预",
365
+ 減: "减",
366
+ 轉: "转",
367
+ 離: "离",
368
+ 辭: "辞",
369
+ 選: "选",
370
+ 舉: "举",
371
+ 臨: "临",
372
+ 時: "时",
373
+ 間: "间",
374
+ 規: "规",
375
+ 則: "则",
376
+ 範: "范",
377
+ 貨: "货",
378
+ 價: "价",
379
+ 錄: "录",
380
+ 屬: "属",
381
+ 區: "区",
382
+ 歲: "岁",
383
+ 萬: "万",
384
+ 與: "与",
385
+ 內: "内",
386
+ 實: "实",
387
+ 濟: "济",
388
+ 狀: "状",
389
+ 況: "况",
390
+ 顯: "显",
391
+ 釋: "释",
392
+ 說: "说",
393
+ 詳: "详",
394
+ 壓: "压",
395
+ 風: "风",
396
+ 險: "险",
397
+ 靈: "灵",
398
+ 遠: "远",
399
+ 遜: "逊",
400
+ 達: "达",
401
+ 遲: "迟",
402
+ 劃: "划",
403
+ 歷: "历",
404
+ 歸: "归",
405
+ 當: "当",
406
+ 鐘: "钟",
407
+ 頭: "头",
408
+ 發: "发",
409
+ 電: "电",
410
+ 龍: "龙"
411
+ };
412
+ /**
413
+ * 公告标题归一化:去公司名前缀(东财习惯 `公司:标题`)→ 繁转简 → 去全部标点/符号/空白。
414
+ * 归一化后跨源比对(同源不去重:同日两份同名披露是合法的不同文件)。
415
+ */
416
+ function normalizeAnnouncementTitle(title) {
417
+ const stripped = title.replace(/^[^::]*[::]\s*/, "");
418
+ let converted = "";
419
+ for (const ch of stripped) converted += TRAD_TO_SIMP[ch] ?? ch;
420
+ return converted.replace(/[\s\p{P}\p{S}]+/gu, "");
421
+ }
422
+ /**
423
+ * 公告标题以类别开头(翌日披露报表/中期报告…),繁简转换与括注差异使全文等值不可靠。
424
+ * 前缀判重的收紧口径(2026-09-03 评审 M1):共同前缀 ≥6 字之外还要求**共同后缀 ≥2 字**。
425
+ * 真实误删对因此被排除(证据 hkex-titlesearch-00700.body):裸类别 `翌日披露报表`(恰 6 字)vs
426
+ * 长标题(长边结尾不同)、同族不同文件(`…已发行股份变动` vs `…变动及股份购回`,中段分叉)、
427
+ * 泛化日期头(`截至二零二六年七月…月报表` vs `截至二零二六年六月三十日…业绩公布`)。仍能兜住
428
+ * 真实变体对(同一文件两源的括注/繁简/措辞差异,头尾主体一致)——失败方向只剩漏去重
429
+ * (重复展示),不是误删(宁漏勿误)。
430
+ */
431
+ function isCrossSourceDup(a, b) {
432
+ if (a.source === b.source) return false;
433
+ if (!a.source.includes("announcement") || !b.source.includes("announcement")) return false;
434
+ const ta = Date.parse(a.publishedAt);
435
+ const tb = Date.parse(b.publishedAt);
436
+ if (!Number.isFinite(ta) || !Number.isFinite(tb)) return false;
437
+ if (Math.abs(ta - tb) > 864e5) return false;
438
+ const na = normalizeAnnouncementTitle(a.title);
439
+ const nb = normalizeAnnouncementTitle(b.title);
440
+ if (na.length === 0 || nb.length === 0) return false;
441
+ if (na === nb) return true;
442
+ const n = Math.min(na.length, nb.length);
443
+ if (n < 8) return false;
444
+ let common = 0;
445
+ while (common < n && na[common] === nb[common]) common++;
446
+ if (common < 6) return false;
447
+ if (na.startsWith(nb) || nb.startsWith(na)) return false;
448
+ let suffix = 0;
449
+ while (suffix < n - common && na[na.length - 1 - suffix] === nb[nb.length - 1 - suffix]) suffix++;
450
+ return suffix >= 2;
451
+ }
452
+ function dedupeCrossSourceAnnouncements(items) {
453
+ const kept = [];
454
+ for (const item of items) if (!kept.some((k) => isCrossSourceDup(k, item))) kept.push(item);
455
+ return kept;
456
+ }
457
+ async function aggregateNews(options = {}) {
458
+ const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
459
+ const now = options.now ?? Date.now();
460
+ const windowMs = clampNumber(options.windowHours, DEFAULT_WINDOW_HOURS, 1, 168) * 36e5;
461
+ const limit = clampNumber(options.limit, DEFAULT_LIMIT, 1, MAX_LIMIT);
462
+ const fetchers = [fetchEastmoneyHk(fetchImpl, limit)];
463
+ if (options.symbol && options.symbol.trim()) {
464
+ fetchers.push(fetchEastmoneyHkAnnouncements(fetchImpl, options.symbol, limit));
465
+ fetchers.push(fetchHkexAnnouncements(fetchImpl, options.symbol, limit, now));
466
+ }
467
+ const results = await Promise.allSettled(fetchers);
468
+ const items = [];
469
+ const unavailable = [];
470
+ for (const result of results) if (result.status === "fulfilled") for (const item of result.value) {
471
+ const maxAge = item.source.includes("announcement") ? ANNOUNCEMENT_MAX_AGE_MS : windowMs;
472
+ if (!inWindow(item.publishedAt, now, maxAge)) continue;
473
+ if (!matchesSymbol(item, options.symbol)) continue;
474
+ items.push(item);
475
+ }
476
+ else unavailable.push(result.reason instanceof Error ? result.reason.message : String(result.reason));
477
+ const deduped = dedupeCrossSourceAnnouncements(items);
478
+ deduped.sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
479
+ return {
480
+ items: deduped.slice(0, limit),
481
+ unavailable
482
+ };
483
+ }
484
+ //#endregion
485
+ export { aggregateNews, isHkRelevant, parseHkShowTime, parseHkexDateTime, resetHkexAnnouncementMemo };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@dshtrading/kit-hk",
3
+ "description": "Hong Kong market toolkit plugin for dsh-trading: HK risk checklist skill provider and HK fast-news (degraded) / fundamentals tools; runs as an hk-trader preset row (session-scoped)",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "lib",
17
+ "assets"
18
+ ],
19
+ "license": "PolyForm-Noncommercial-1.0.0",
20
+ "dependencies": {
21
+ "@dshtrading/knowledge": "0.1.0",
22
+ "@dshtrading/indicators": "0.1.0"
23
+ },
24
+ "peerDependencies": {
25
+ "@deepseek-ai/cordis": ">=4.0.0",
26
+ "@deepseek-ai/dsh-skill": ">=0.1.2-alpha.1",
27
+ "@deepseek-ai/dsh-tools": ">=0.1.2-alpha.1",
28
+ "@deepseek-ai/schemastery": ">=3.18.0"
29
+ },
30
+ "devDependencies": {
31
+ "tsdown": "^0.22.0",
32
+ "vitest": "^3.0.0",
33
+ "@dshtrading/api": "0.1.0"
34
+ },
35
+ "scripts": {
36
+ "build": "tsdown",
37
+ "test": "vitest run"
38
+ }
39
+ }