@dshtrading/watchlist 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/LICENSE ADDED
@@ -0,0 +1,75 @@
1
+ Copyright (c) 2026 zhu1090093659 (dsh-trading)
2
+
3
+ # PolyForm Noncommercial License 1.0.0
4
+
5
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncommercial Purposes
34
+
35
+ Any noncommercial purpose is a permitted purpose.
36
+
37
+ ## Personal Uses
38
+
39
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
40
+
41
+ ## Noncommercial Organizations
42
+
43
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
44
+
45
+ ## Fair Use
46
+
47
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
48
+
49
+ ## No Other Rights
50
+
51
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
52
+
53
+ ## Patent Defense
54
+
55
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
56
+
57
+ ## Violations
58
+
59
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
60
+
61
+ ## No Liability
62
+
63
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
64
+
65
+ ## Definitions
66
+
67
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
68
+
69
+ **You** refers to the individual or entity agreeing to these terms.
70
+
71
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
72
+
73
+ **Your licenses** are all the licenses granted to you for the software under these terms.
74
+
75
+ **Use** means anything you do with the software requiring one of your licenses.
@@ -0,0 +1,6 @@
1
+ import { SelectionStore, WatchlistStore } from "./index.js";
2
+ //#region src/file-store.d.ts
3
+ declare function createFileWatchlistStore(filePath: string): WatchlistStore;
4
+ declare function createFileSelectionStore(filePath: string): SelectionStore;
5
+ //#endregion
6
+ export { createFileSelectionStore, createFileWatchlistStore };
@@ -0,0 +1,132 @@
1
+ import { dirname } from "node:path";
2
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
+ //#region src/file-store.ts
4
+ /**
5
+ * 文件持久化版自选/选中 store(Node.js 宿主侧使用,tmp+rename 原子写,
6
+ * indicators/custom-fs 同款模式——issue #32 规格)。
7
+ */
8
+ /**
9
+ * 跨平台健壮原子写入:rename 遇 Windows EPERM/EBUSY(目标被占用)短暂退避重试;
10
+ * 重试耗尽或任何失败一律保留旧文件、清理 tmp、log + throw——目标文件永远只被
11
+ * 原子 rename 触碰,绝不非原子直写(防止半截写损坏 JSON 导致 load 静默重置)。
12
+ */
13
+ async function safeAtomicWrite(filePath, data) {
14
+ const dir = dirname(filePath);
15
+ const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
16
+ try {
17
+ await mkdir(dir, { recursive: true });
18
+ await writeFile(tmpPath, data, "utf8");
19
+ let lastError;
20
+ for (let attempt = 0; attempt < 3; attempt++) try {
21
+ await rename(tmpPath, filePath);
22
+ return;
23
+ } catch (err) {
24
+ if (err?.code !== "EPERM" && err?.code !== "EBUSY") throw err;
25
+ lastError = err;
26
+ await new Promise((resolve) => setTimeout(resolve, 25 * (attempt + 1)));
27
+ }
28
+ throw lastError;
29
+ } catch (error) {
30
+ console.error(`[dsh-trading/watchlist] failed to atomic flush watchlists to ${filePath}:`, error);
31
+ await unlink(tmpPath).catch(() => {});
32
+ throw error;
33
+ }
34
+ }
35
+ function createFileWatchlistStore(filePath) {
36
+ let cache = null;
37
+ let pendingWrite = Promise.resolve();
38
+ const enqueue = (fn) => {
39
+ const next = pendingWrite.then(fn, fn);
40
+ pendingWrite = next.then(() => {}, () => {});
41
+ return next;
42
+ };
43
+ async function load() {
44
+ if (cache !== null) return cache;
45
+ try {
46
+ const content = await readFile(filePath, "utf8");
47
+ const parsed = JSON.parse(content);
48
+ cache = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
49
+ } catch (err) {
50
+ if (err?.code !== "ENOENT") console.error(`[dsh-trading/watchlist] failed to read watchlists from ${filePath}:`, err);
51
+ cache = {};
52
+ }
53
+ return cache;
54
+ }
55
+ async function flush(map) {
56
+ return enqueue(async () => {
57
+ await safeAtomicWrite(filePath, JSON.stringify(map, null, 2));
58
+ });
59
+ }
60
+ return {
61
+ async list() {
62
+ return { ...await load() };
63
+ },
64
+ async save(next) {
65
+ cache = { ...next };
66
+ await flush(cache);
67
+ },
68
+ async add(market, instrument) {
69
+ const map = await load();
70
+ const rows = map[market] ?? [];
71
+ if (rows.some((row) => row.symbol === instrument.symbol)) return false;
72
+ const next = {
73
+ ...map,
74
+ [market]: [...rows, { ...instrument }]
75
+ };
76
+ cache = next;
77
+ await flush(next);
78
+ return true;
79
+ },
80
+ async remove(market, symbol) {
81
+ const map = await load();
82
+ const rows = map[market] ?? [];
83
+ const nextRows = rows.filter((row) => row.symbol !== symbol);
84
+ if (nextRows.length === rows.length) return false;
85
+ const next = {
86
+ ...map,
87
+ [market]: nextRows
88
+ };
89
+ cache = next;
90
+ await flush(next);
91
+ return true;
92
+ }
93
+ };
94
+ }
95
+ function createFileSelectionStore(filePath) {
96
+ let cache = null;
97
+ let pendingWrite = Promise.resolve();
98
+ const enqueue = (fn) => {
99
+ const next = pendingWrite.then(fn, fn);
100
+ pendingWrite = next.then(() => {}, () => {});
101
+ return next;
102
+ };
103
+ async function load() {
104
+ if (cache !== null) return cache;
105
+ try {
106
+ const content = await readFile(filePath, "utf8");
107
+ const parsed = JSON.parse(content);
108
+ cache = parsed !== null && typeof parsed === "object" ? { instrument: parsed.instrument ?? null } : { instrument: null };
109
+ } catch (err) {
110
+ if (err?.code !== "ENOENT") console.error(`[dsh-trading/watchlist] failed to read selection from ${filePath}:`, err);
111
+ cache = { instrument: null };
112
+ }
113
+ return cache;
114
+ }
115
+ async function flush(record) {
116
+ return enqueue(async () => {
117
+ await safeAtomicWrite(filePath, JSON.stringify(record, null, 2));
118
+ });
119
+ }
120
+ return {
121
+ async get() {
122
+ const record = await load();
123
+ return { instrument: record.instrument === null ? null : { ...record.instrument } };
124
+ },
125
+ async set(record) {
126
+ cache = { instrument: record.instrument === null ? null : { ...record.instrument } };
127
+ await flush(cache);
128
+ }
129
+ };
130
+ }
131
+ //#endregion
132
+ export { createFileSelectionStore, createFileWatchlistStore };
package/lib/index.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource } from "./seeds.js";
2
+ //#region src/index.d.ts
3
+ /**
4
+ * @dshtrading/watchlist —— host 侧自选股与选中标的存储(issue #32 / P3)。
5
+ *
6
+ * 背景:自选股原先只存浏览器 localStorage(dshtrading.watchlist.v1),node 半的
7
+ * Agent 触达不了——四块能力中 Agent 工具面唯一完全空白的一块。owner 2026-08-31
8
+ * 裁决(D3):存储升位 localStorage → host file store(含一次性迁移),localStorage
9
+ * 降级为缓存镜像,不再是 SSOT。
10
+ *
11
+ * 本模块(纯类型 + 内存 store,浏览器安全):
12
+ * - `WatchlistsMap`:按 market 分桶的多列表行(rows 只存用户定制行;市场种子列表
13
+ * 属客户端展示回退,不进 host store——空 market = 未定制,客户端回落种子)。
14
+ * - `SelectionRecord`:跨市场选中标的(watchlist_select 工具与左栏点击同源)。
15
+ * - `./plugin` 子路径:file store(~/.dsh/watchlists.json、~/.dsh/selection.json,
16
+ * tmp+rename 原子写)+ host 平面工具 watchlist_list/add/remove/select(全会话可见,
17
+ * owner 裁决 D4;select/add/remove 后 emit tradingEvents('watchlists'|'selection'))。
18
+ *
19
+ * 词汇纪律:symbol 用市场规范形(docs/symbol-vocabulary.md),本包不做归一化
20
+ * (写入方负责——工具参数与桥端点均原样落盘)。
21
+ *
22
+ * @module @dshtrading/watchlist
23
+ */
24
+ /** 跨市场标的行(market 为市场词汇 slug:crypto | us | cn | hk,开放新市场)。 */
25
+ interface WatchlistInstrument {
26
+ market: string;
27
+ symbol: string;
28
+ /** 展示名(可选;工具添加可缺省,客户端以 symbol 兜底展示)。 */
29
+ name?: string;
30
+ }
31
+ /** 多列表自选:market → 用户定制行数组(空数组/缺键 = 未定制)。 */
32
+ type WatchlistsMap = Record<string, WatchlistInstrument[]>;
33
+ /** 选中标的记录(中栏切图的 SSOT)。 */
34
+ interface SelectionRecord {
35
+ instrument: WatchlistInstrument | null;
36
+ }
37
+ interface WatchlistStore {
38
+ /** 全量读取(host SSOT;客户端启动同步与 SSE 重拉都走这里)。 */
39
+ list(): Promise<WatchlistsMap>;
40
+ /** 全量替换(桥 PUT / 客户端迁移导入)。 */
41
+ save(map: WatchlistsMap): Promise<void>;
42
+ /** 追加一行(同 market 内按 symbol 去重);返回是否新增。 */
43
+ add(market: string, instrument: WatchlistInstrument): Promise<boolean>;
44
+ /** 移除一行;返回是否 existed。 */
45
+ remove(market: string, symbol: string): Promise<boolean>;
46
+ }
47
+ interface SelectionStore {
48
+ get(): Promise<SelectionRecord>;
49
+ set(record: SelectionRecord): Promise<void>;
50
+ }
51
+ /** 内存版自选 store(单测用)。 */
52
+ declare function createMemoryWatchlistStore(initial?: WatchlistsMap): WatchlistStore;
53
+ /** 内存版选中 store(单测用)。 */
54
+ declare function createMemorySelectionStore(initial?: SelectionRecord): SelectionStore;
55
+ //#endregion
56
+ export { SelectionRecord, SelectionStore, WATCHLIST_SEEDS, WatchlistInstrument, WatchlistStore, WatchlistsMap, createMemorySelectionStore, createMemoryWatchlistStore, effectiveWatchlistRows, watchlistRowSource };
package/lib/index.js ADDED
@@ -0,0 +1,50 @@
1
+ import { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource } from "./seeds.js";
2
+ //#region src/index.ts
3
+ /** 内存版自选 store(单测用)。 */
4
+ function createMemoryWatchlistStore(initial = {}) {
5
+ let map = { ...initial };
6
+ return {
7
+ async list() {
8
+ return map;
9
+ },
10
+ async save(next) {
11
+ map = { ...next };
12
+ },
13
+ async add(market, instrument) {
14
+ const rows = map[market] ?? [];
15
+ if (rows.some((row) => row.symbol === instrument.symbol)) return false;
16
+ map = {
17
+ ...map,
18
+ [market]: [...rows, { ...instrument }]
19
+ };
20
+ return true;
21
+ },
22
+ async remove(market, symbol) {
23
+ const rows = map[market] ?? [];
24
+ const next = rows.filter((row) => row.symbol !== symbol);
25
+ if (next.length === rows.length) return false;
26
+ map = {
27
+ ...map,
28
+ [market]: next
29
+ };
30
+ return true;
31
+ }
32
+ };
33
+ }
34
+ /** 内存版选中 store(单测用)。 */
35
+ function createMemorySelectionStore(initial = { instrument: null }) {
36
+ let current = { ...initial };
37
+ return {
38
+ async get() {
39
+ return current;
40
+ },
41
+ async set(record) {
42
+ current = {
43
+ ...record,
44
+ instrument: record.instrument === null ? null : { ...record.instrument }
45
+ };
46
+ }
47
+ };
48
+ }
49
+ //#endregion
50
+ export { WATCHLIST_SEEDS, createMemorySelectionStore, createMemoryWatchlistStore, effectiveWatchlistRows, watchlistRowSource };
@@ -0,0 +1,34 @@
1
+ import { SelectionStore, WatchlistStore, createMemorySelectionStore, createMemoryWatchlistStore } from "./index.js";
2
+ import { createFileSelectionStore, createFileWatchlistStore } from "./file-store.js";
3
+ import { Context } from "@deepseek-ai/cordis";
4
+ //#region src/plugin.d.ts
5
+ /** Cordis 插件名 = patch 行 id(TEMPLATES §8),市场无关共享行命名空间。 */
6
+ declare const name = "dsh-trading-watchlist";
7
+ /** 本插件不硬依赖任何服务;tools 经 ctx.inject 声明。 */
8
+ declare const inject: string[];
9
+ /** 默认存储路径。 */
10
+ declare function defaultWatchlistStorePath(): string;
11
+ declare function defaultSelectionStorePath(): string;
12
+ /** tradingEvents 的最小发布面(鸭式;总线缺席时静默降级)。 */
13
+ interface TradingEventsPublisher {
14
+ emit(store: 'watchlists' | 'selection'): void;
15
+ }
16
+ interface WatchlistToolDeps {
17
+ watchlists: WatchlistStore;
18
+ selection: SelectionStore;
19
+ onWatchlistsChanged?: () => void;
20
+ onSelectionChanged?: () => void;
21
+ }
22
+ declare function createWatchlistListTool(deps: WatchlistToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
23
+ declare function createWatchlistAddTool(deps: WatchlistToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
24
+ declare function createWatchlistRemoveTool(deps: WatchlistToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
25
+ declare function createWatchlistSelectTool(deps: WatchlistToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
26
+ interface WatchlistPluginDeps {
27
+ watchlists: WatchlistStore;
28
+ selection: SelectionStore;
29
+ }
30
+ declare function registerWatchlistTools(ctx: Context, deps: WatchlistPluginDeps): void;
31
+ /** Host plugin body:file store provide + 4 工具注册(host 平面,全会话可见)。 */
32
+ declare function apply(ctx: Context): void;
33
+ //#endregion
34
+ export { TradingEventsPublisher, WatchlistPluginDeps, WatchlistToolDeps, apply, createFileSelectionStore, createFileWatchlistStore, createMemorySelectionStore, createMemoryWatchlistStore, createWatchlistAddTool, createWatchlistListTool, createWatchlistRemoveTool, createWatchlistSelectTool, defaultSelectionStorePath, defaultWatchlistStorePath, inject, name, registerWatchlistTools };
package/lib/plugin.js ADDED
@@ -0,0 +1,207 @@
1
+ import { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource } from "./seeds.js";
2
+ import { createMemorySelectionStore, createMemoryWatchlistStore } from "./index.js";
3
+ import { createFileSelectionStore, createFileWatchlistStore } from "./file-store.js";
4
+ import { defineTool } from "@deepseek-ai/dsh-tools";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ //#region src/plugin.ts
8
+ /** Cordis 插件名 = patch 行 id(TEMPLATES §8),市场无关共享行命名空间。 */
9
+ const name = "dsh-trading-watchlist";
10
+ /** 本插件不硬依赖任何服务;tools 经 ctx.inject 声明。 */
11
+ const inject = [];
12
+ /** 默认存储路径。 */
13
+ function defaultWatchlistStorePath() {
14
+ return path.join(os.homedir(), ".dsh", "watchlists.json");
15
+ }
16
+ function defaultSelectionStorePath() {
17
+ return path.join(os.homedir(), ".dsh", "selection.json");
18
+ }
19
+ function eventsOf(ctx) {
20
+ return ctx.get?.("tradingEvents", false);
21
+ }
22
+ function parseInstrumentArgs(raw) {
23
+ const market = typeof raw.market === "string" ? raw.market.trim() : "";
24
+ const symbol = typeof raw.symbol === "string" ? raw.symbol.trim() : "";
25
+ const name = typeof raw.name === "string" && raw.name.trim() ? raw.name.trim() : void 0;
26
+ if (!market || !symbol) throw new Error("market and symbol are required (market: crypto|us|cn|hk…, symbol: 市场规范形,如 BTCUSDT / AAPL / 600519 / 00700)");
27
+ return {
28
+ market,
29
+ symbol,
30
+ name
31
+ };
32
+ }
33
+ function createWatchlistListTool(deps) {
34
+ return defineTool({
35
+ name: "watchlist_list",
36
+ description: "List the user's watchlist exactly as displayed in the trading GUI sidebar, across all markets (crypto/us/cn/hk). Rows merge the user's customized entries (source \"custom\") with each market's default seed rows (source \"seed\", shown while that market has no custom edits) — the GUI shows the same rows, so this list IS what the user sees. It also maps display names to symbols (e.g. 苹果 → AAPL / us, 贵州茅台 → 600519 / cn). ALWAYS call this first when the user mentions any instrument by name or symbol; never conclude an instrument is untracked from docs or connector coverage alone. Read-only.",
37
+ parameters: {},
38
+ output: {
39
+ schema: { type: "string" },
40
+ render: (_args, value) => [{
41
+ type: "text",
42
+ text: value
43
+ }]
44
+ },
45
+ async execute() {
46
+ const map = await deps.watchlists.list();
47
+ const markets = [.../* @__PURE__ */ new Set([...Object.keys(map), ...Object.keys(WATCHLIST_SEEDS)])].filter((market) => effectiveWatchlistRows(map, market).length > 0);
48
+ const watchlists = {};
49
+ const sources = {};
50
+ let total = 0;
51
+ for (const market of markets) {
52
+ const rows = effectiveWatchlistRows(map, market);
53
+ watchlists[market] = rows;
54
+ sources[market] = watchlistRowSource(map, market);
55
+ total += rows.length;
56
+ }
57
+ return JSON.stringify({
58
+ ok: true,
59
+ total,
60
+ markets,
61
+ sources,
62
+ watchlists
63
+ });
64
+ }
65
+ });
66
+ }
67
+ function createWatchlistAddTool(deps) {
68
+ return defineTool({
69
+ name: "watchlist_add",
70
+ description: "Add an instrument to the user watchlist (cross-market, no tool-name market prefix). Deduplicated per market by symbol. The open GUI sidebar refreshes live over the SSE invalidation channel.",
71
+ parameters: {
72
+ market: {
73
+ type: "string",
74
+ required: true,
75
+ description: "Market vocabulary slug: crypto | us | cn | hk"
76
+ },
77
+ symbol: {
78
+ type: "string",
79
+ required: true,
80
+ description: "Market-canonical symbol (docs/symbol-vocabulary.md), e.g. BTCUSDT / AAPL / 600519 / 00700"
81
+ },
82
+ name: {
83
+ type: "string",
84
+ description: "Optional display name, e.g. 贵州茅台"
85
+ }
86
+ },
87
+ output: {
88
+ schema: { type: "string" },
89
+ render: (_args, value) => [{
90
+ type: "text",
91
+ text: value
92
+ }]
93
+ },
94
+ async execute(raw) {
95
+ const instrument = parseInstrumentArgs(raw ?? {});
96
+ const added = await deps.watchlists.add(instrument.market, instrument);
97
+ if (added) deps.onWatchlistsChanged?.();
98
+ return JSON.stringify({
99
+ ok: true,
100
+ added,
101
+ note: added ? `Added ${instrument.symbol} (${instrument.market}) to the watchlist.` : `${instrument.symbol} is already in the ${instrument.market} watchlist (deduplicated, nothing changed).`
102
+ });
103
+ }
104
+ });
105
+ }
106
+ function createWatchlistRemoveTool(deps) {
107
+ return defineTool({
108
+ name: "watchlist_remove",
109
+ description: "Remove an instrument from the user watchlist (cross-market). The open GUI sidebar refreshes live over the SSE invalidation channel.",
110
+ parameters: {
111
+ market: {
112
+ type: "string",
113
+ required: true,
114
+ description: "Market vocabulary slug: crypto | us | cn | hk"
115
+ },
116
+ symbol: {
117
+ type: "string",
118
+ required: true,
119
+ description: "Market-canonical symbol to remove"
120
+ }
121
+ },
122
+ output: {
123
+ schema: { type: "string" },
124
+ render: (_args, value) => [{
125
+ type: "text",
126
+ text: value
127
+ }]
128
+ },
129
+ async execute(raw) {
130
+ const { market, symbol } = parseInstrumentArgs(raw ?? {});
131
+ const removed = await deps.watchlists.remove(market, symbol);
132
+ if (removed) deps.onWatchlistsChanged?.();
133
+ return JSON.stringify({
134
+ ok: true,
135
+ removed,
136
+ note: removed ? `Removed ${symbol} from the ${market} watchlist.` : `${symbol} was not in the ${market} watchlist (nothing changed).`
137
+ });
138
+ }
139
+ });
140
+ }
141
+ function createWatchlistSelectTool(deps) {
142
+ return defineTool({
143
+ name: "watchlist_select",
144
+ description: "Select an instrument as the focused chart in the trading GUI (cross-market). The open GUI switches the middle-stage chart to this instrument live over the SSE channel. Prefer an instrument that exists in watchlist_list (its display name is reused); unknown symbols are accepted with the raw symbol as display fallback.",
145
+ parameters: {
146
+ market: {
147
+ type: "string",
148
+ required: true,
149
+ description: "Market vocabulary slug: crypto | us | cn | hk"
150
+ },
151
+ symbol: {
152
+ type: "string",
153
+ required: true,
154
+ description: "Market-canonical symbol to focus"
155
+ }
156
+ },
157
+ output: {
158
+ schema: { type: "string" },
159
+ render: (_args, value) => [{
160
+ type: "text",
161
+ text: value
162
+ }]
163
+ },
164
+ async execute(raw) {
165
+ const { market, symbol } = parseInstrumentArgs(raw ?? {});
166
+ const map = await deps.watchlists.list();
167
+ const instrument = effectiveWatchlistRows(map, market).find((row) => row.symbol === symbol) ?? {
168
+ market,
169
+ symbol
170
+ };
171
+ await deps.selection.set({ instrument });
172
+ deps.onSelectionChanged?.();
173
+ return JSON.stringify({
174
+ ok: true,
175
+ selected: instrument,
176
+ note: `Focused the chart on ${symbol} (${market}) — the open GUI switches live.`
177
+ });
178
+ }
179
+ });
180
+ }
181
+ function registerWatchlistTools(ctx, deps) {
182
+ ctx.inject(["tools"], (toolCtx) => {
183
+ const tools = toolCtx.tools;
184
+ if (!tools || typeof tools.register !== "function") return;
185
+ const hookDeps = {
186
+ ...deps,
187
+ onWatchlistsChanged: () => eventsOf(ctx)?.emit("watchlists"),
188
+ onSelectionChanged: () => eventsOf(ctx)?.emit("selection")
189
+ };
190
+ const shared = { register: (tool) => {
191
+ if (tools.get(tool.name) === void 0) tools.register(tool);
192
+ } };
193
+ shared.register(createWatchlistListTool(hookDeps));
194
+ shared.register(createWatchlistAddTool(hookDeps));
195
+ shared.register(createWatchlistRemoveTool(hookDeps));
196
+ shared.register(createWatchlistSelectTool(hookDeps));
197
+ });
198
+ }
199
+ /** Host plugin body:file store provide + 4 工具注册(host 平面,全会话可见)。 */
200
+ function apply(ctx) {
201
+ registerWatchlistTools(ctx, {
202
+ watchlists: createFileWatchlistStore(defaultWatchlistStorePath()),
203
+ selection: createFileSelectionStore(defaultSelectionStorePath())
204
+ });
205
+ }
206
+ //#endregion
207
+ export { apply, createFileSelectionStore, createFileWatchlistStore, createMemorySelectionStore, createMemoryWatchlistStore, createWatchlistAddTool, createWatchlistListTool, createWatchlistRemoveTool, createWatchlistSelectTool, defaultSelectionStorePath, defaultWatchlistStorePath, inject, name, registerWatchlistTools };
package/lib/seeds.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { WatchlistInstrument, WatchlistsMap } from "./index.js";
2
+ //#region src/seeds.d.ts
3
+ /** 各市场种子行(connector-validated symbol formats)。 */
4
+ declare const WATCHLIST_SEEDS: WatchlistsMap;
5
+ /** 单市场展示行:用户定制行优先;未定制(缺键/空数组)回落该市场种子。 */
6
+ declare function effectiveWatchlistRows(map: WatchlistsMap, market: string): WatchlistInstrument[];
7
+ /** 单市场行来源:'custom' = 用户定制行;'seed' = 种子展示行(未定制)。 */
8
+ declare function watchlistRowSource(map: WatchlistsMap, market: string): 'custom' | 'seed';
9
+ //#endregion
10
+ export { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource };
package/lib/seeds.js ADDED
@@ -0,0 +1,95 @@
1
+ //#region src/seeds.ts
2
+ /** 各市场种子行(connector-validated symbol formats)。 */
3
+ const WATCHLIST_SEEDS = {
4
+ crypto: [
5
+ {
6
+ market: "crypto",
7
+ symbol: "BTCUSDT",
8
+ name: "Bitcoin"
9
+ },
10
+ {
11
+ market: "crypto",
12
+ symbol: "ETHUSDT",
13
+ name: "Ethereum"
14
+ },
15
+ {
16
+ market: "crypto",
17
+ symbol: "SOLUSDT",
18
+ name: "Solana"
19
+ },
20
+ {
21
+ market: "crypto",
22
+ symbol: "BNBUSDT",
23
+ name: "BNB"
24
+ }
25
+ ],
26
+ us: [
27
+ {
28
+ market: "us",
29
+ symbol: "AAPL",
30
+ name: "苹果"
31
+ },
32
+ {
33
+ market: "us",
34
+ symbol: "MSFT",
35
+ name: "微软"
36
+ },
37
+ {
38
+ market: "us",
39
+ symbol: "NVDA",
40
+ name: "英伟达"
41
+ },
42
+ {
43
+ market: "us",
44
+ symbol: "GOOGL",
45
+ name: "谷歌"
46
+ }
47
+ ],
48
+ cn: [
49
+ {
50
+ market: "cn",
51
+ symbol: "600519",
52
+ name: "贵州茅台"
53
+ },
54
+ {
55
+ market: "cn",
56
+ symbol: "000001",
57
+ name: "平安银行"
58
+ },
59
+ {
60
+ market: "cn",
61
+ symbol: "601318",
62
+ name: "中国平安"
63
+ }
64
+ ],
65
+ hk: [
66
+ {
67
+ market: "hk",
68
+ symbol: "00700",
69
+ name: "腾讯控股"
70
+ },
71
+ {
72
+ market: "hk",
73
+ symbol: "09988",
74
+ name: "阿里巴巴-W"
75
+ },
76
+ {
77
+ market: "hk",
78
+ symbol: "03690",
79
+ name: "美团-W"
80
+ }
81
+ ]
82
+ };
83
+ /** 单市场展示行:用户定制行优先;未定制(缺键/空数组)回落该市场种子。 */
84
+ function effectiveWatchlistRows(map, market) {
85
+ const rows = map[market];
86
+ if (rows !== void 0 && rows.length > 0) return rows;
87
+ return WATCHLIST_SEEDS[market] ?? [];
88
+ }
89
+ /** 单市场行来源:'custom' = 用户定制行;'seed' = 种子展示行(未定制)。 */
90
+ function watchlistRowSource(map, market) {
91
+ const rows = map[market];
92
+ return rows !== void 0 && rows.length > 0 ? "custom" : "seed";
93
+ }
94
+ //#endregion
95
+ export { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@dshtrading/watchlist",
3
+ "description": "Host-side watchlist + selection stores for dsh-trading: multi-list file store (~/.dsh/watchlists.json), selection store (~/.dsh/selection.json), and cross-market watchlist_* tools (watchlist_list/add/remove/select) on the host plane",
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
+ "./plugin": {
14
+ "types": "./lib/plugin.d.ts",
15
+ "default": "./lib/plugin.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "lib"
21
+ ],
22
+ "license": "PolyForm-Noncommercial-1.0.0",
23
+ "dependencies": {},
24
+ "peerDependencies": {
25
+ "@deepseek-ai/cordis": ">=4.0.0",
26
+ "@deepseek-ai/dsh-tools": ">=0.1.2-alpha.1"
27
+ },
28
+ "devDependencies": {
29
+ "tsdown": "^0.22.0",
30
+ "vitest": "^3.0.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsdown",
34
+ "test": "vitest run"
35
+ }
36
+ }