@dshtrading/client-ui-trading 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 +75 -0
- package/README.md +55 -0
- package/lib/bridge.js +794 -0
- package/lib/client/contract.d.ts +11 -0
- package/lib/client/locales.d.ts +6 -0
- package/lib/client/locales.js +943 -0
- package/lib/client.js +20671 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +141 -0
- package/lib/sse.js +30 -0
- package/package.json +84 -0
package/lib/bridge.js
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import { aggregateNews, fetchCnFundamentalsPackage } from "@dshtrading/kit-cn";
|
|
2
|
+
import { aggregateNews as aggregateNews$1, fetchHkFundamentalsPackage } from "@dshtrading/kit-hk";
|
|
3
|
+
import { aggregateNews as aggregateNews$2, fetchUsFundamentalsPackage } from "@dshtrading/kit-us";
|
|
4
|
+
import { aggregateNews as aggregateNews$3, fetchCryptoFundamentalsPackage } from "@dshtrading/kit-crypto";
|
|
5
|
+
import { createMemoryCustomIndicatorStore } from "@dshtrading/indicators";
|
|
6
|
+
import { createMemoryKnowledgeCardStore } from "@dshtrading/knowledge";
|
|
7
|
+
import { createMemoryCustomStrategyStore } from "@dshtrading/strategies";
|
|
8
|
+
import { createMemorySelectionStore, createMemoryWatchlistStore } from "@dshtrading/watchlist";
|
|
9
|
+
//#region src/bridge.ts
|
|
10
|
+
const MARKET_IDS = [
|
|
11
|
+
"crypto",
|
|
12
|
+
"us",
|
|
13
|
+
"cn",
|
|
14
|
+
"hk"
|
|
15
|
+
];
|
|
16
|
+
/** market → Context 服务键(@dshtrading/api 的 Context 增强)。 */
|
|
17
|
+
const MARKET_SERVICE_KEYS = {
|
|
18
|
+
crypto: "tradingCryptoMarketData",
|
|
19
|
+
us: "tradingUsMarketData",
|
|
20
|
+
cn: "tradingCnMarketData",
|
|
21
|
+
hk: "tradingHkMarketData"
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* 桥宿主工厂(registry-first 解析的唯一实现,node 半与单测共用):
|
|
25
|
+
* - getMarketService:注册表有激活注册项 → 用之;否则回退 legacy 市场键直读
|
|
26
|
+
* (2026-08-30 前形态:连接器老 dataplane 互斥 provide 市场键的部署)。
|
|
27
|
+
* - activeProvider:优先报告实际供数的注册项 provider;注册表未裁决时回退 router 值
|
|
28
|
+
* (选中但未注册 → 用户能在 GUI 看到设置目标,行情区报「未安装/未激活」)。
|
|
29
|
+
*/
|
|
30
|
+
function createBridgeHost(services) {
|
|
31
|
+
return {
|
|
32
|
+
getMarketService: (market) => {
|
|
33
|
+
const active = services.registry?.active(market);
|
|
34
|
+
if (active !== void 0) return active.service;
|
|
35
|
+
return services.legacy(market);
|
|
36
|
+
},
|
|
37
|
+
getTradeService: (market) => services.tradeRegistry?.active(market)?.service,
|
|
38
|
+
activeProvider: (market) => services.registry?.active(market)?.provider ?? services.router?.activeProvider(market),
|
|
39
|
+
customIndicatorsStore: services.customIndicatorsStore ?? createMemoryCustomIndicatorStore(),
|
|
40
|
+
knowledgeStore: services.knowledgeStore ?? createMemoryKnowledgeCardStore(),
|
|
41
|
+
strategyStore: services.strategyStore ?? createMemoryCustomStrategyStore(),
|
|
42
|
+
watchlistStore: services.watchlistStore ?? createMemoryWatchlistStore(),
|
|
43
|
+
selectionStore: services.selectionStore ?? createMemorySelectionStore(),
|
|
44
|
+
newsRegistry: services.newsRegistry,
|
|
45
|
+
newsKey: services.newsKey
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** 单次 K 线 limit 封顶(与 Binance/Bybit 单请求上限对齐;OKX 超出 300 的部分由连接器 after 游标翻页补足)。 */
|
|
49
|
+
const MAX_KLINE_LIMIT = 1e3;
|
|
50
|
+
var BridgeProtocolError = class extends Error {
|
|
51
|
+
status;
|
|
52
|
+
constructor(status, message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.status = status;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function isMarketId(value) {
|
|
58
|
+
return MARKET_IDS.includes(value);
|
|
59
|
+
}
|
|
60
|
+
/** 动态标的全集缓存 TTL(30分钟)。 */
|
|
61
|
+
const SYMBOLS_CACHE_TTL_MS = 18e5;
|
|
62
|
+
/**
|
|
63
|
+
* 基本面数据包缓存 TTL(5分钟):财报级/日级数据,非 tick;同键 in-flight 去重
|
|
64
|
+
* 让 tab 翻转与快速切标的只打一轮上游(issue #36 整改,2026-09-02)。
|
|
65
|
+
*/
|
|
66
|
+
const FUNDAMENTALS_CACHE_TTL_MS = 3e5;
|
|
67
|
+
/** 从 Error 上提取结构化错误词汇(连接器按 TradingError 形状附加 code)。 */
|
|
68
|
+
function errorPayload(error) {
|
|
69
|
+
if (error instanceof Error) return {
|
|
70
|
+
code: typeof error.code === "string" ? error.code : "TRADING_UNKNOWN",
|
|
71
|
+
message: error.message
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
code: "TRADING_UNKNOWN",
|
|
75
|
+
message: String(error)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
var TradingBridge = class {
|
|
79
|
+
host;
|
|
80
|
+
symbolsCache = /* @__PURE__ */ new Map();
|
|
81
|
+
fundamentalsCache = /* @__PURE__ */ new Map();
|
|
82
|
+
fundamentalsInflight = /* @__PURE__ */ new Map();
|
|
83
|
+
constructor(host) {
|
|
84
|
+
this.host = host;
|
|
85
|
+
}
|
|
86
|
+
/** 已安装(有行情服务)的市场清单 + 当前 provider slug。 */
|
|
87
|
+
markets() {
|
|
88
|
+
const markets = [];
|
|
89
|
+
for (const id of MARKET_IDS) {
|
|
90
|
+
if (this.host.getMarketService(id) === void 0) continue;
|
|
91
|
+
const provider = this.host.activeProvider(id);
|
|
92
|
+
markets.push(provider === void 0 ? { id } : {
|
|
93
|
+
id,
|
|
94
|
+
provider
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return { markets };
|
|
98
|
+
}
|
|
99
|
+
/** 批量报价:逐 symbol 独立成功/失败(一个坏代码不拖垮整批)。 */
|
|
100
|
+
async tickers(market, symbols) {
|
|
101
|
+
if (!isMarketId(market)) throw new BridgeProtocolError(400, `unknown market ${JSON.stringify(market)}`);
|
|
102
|
+
const unique = [...new Set(symbols.map((symbol) => symbol.trim()).filter(Boolean))];
|
|
103
|
+
if (unique.length === 0) throw new BridgeProtocolError(400, "tickers: symbols is required");
|
|
104
|
+
if (unique.length > 32) throw new BridgeProtocolError(400, `tickers: too many symbols (${unique.length} > 32)`);
|
|
105
|
+
const service = this.host.getMarketService(market);
|
|
106
|
+
if (service === void 0) throw new BridgeProtocolError(400, `market ${market} is not installed`);
|
|
107
|
+
const outcomes = await Promise.all(unique.map(async (symbol) => {
|
|
108
|
+
try {
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
ticker: await service.getTicker(symbol)
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
...errorPayload(error)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}));
|
|
120
|
+
const tickers = {};
|
|
121
|
+
unique.forEach((symbol, index) => {
|
|
122
|
+
tickers[symbol] = outcomes[index];
|
|
123
|
+
});
|
|
124
|
+
return { tickers };
|
|
125
|
+
}
|
|
126
|
+
/** K 线:透传 interval(连接器自行校验各自支持集)。 */
|
|
127
|
+
async klines(market, symbol, interval, rawLimit) {
|
|
128
|
+
if (!isMarketId(market)) throw new BridgeProtocolError(400, `unknown market ${JSON.stringify(market)}`);
|
|
129
|
+
const trimmed = symbol.trim();
|
|
130
|
+
if (trimmed === "") throw new BridgeProtocolError(400, "klines: symbol is required");
|
|
131
|
+
const limit = rawLimit === null || rawLimit === void 0 ? void 0 : Number(rawLimit);
|
|
132
|
+
if (limit !== void 0 && (!Number.isInteger(limit) || limit <= 0 || limit > 1e3)) throw new BridgeProtocolError(400, `klines: limit must be an integer in 1..${MAX_KLINE_LIMIT}`);
|
|
133
|
+
const service = this.host.getMarketService(market);
|
|
134
|
+
if (service === void 0) throw new BridgeProtocolError(400, `market ${market} is not installed`);
|
|
135
|
+
return { klines: await service.getKlines(trimmed, interval, limit) };
|
|
136
|
+
}
|
|
137
|
+
/** 动态标的全集(带 30min 进程内缓存;未实现或异常静默回落空列表)。 */
|
|
138
|
+
async symbols(market, query) {
|
|
139
|
+
if (!isMarketId(market)) throw new BridgeProtocolError(400, `unknown market ${JSON.stringify(market)}`);
|
|
140
|
+
const service = this.host.getMarketService(market);
|
|
141
|
+
if (service === void 0) throw new BridgeProtocolError(400, `market ${market} is not installed`);
|
|
142
|
+
if (typeof service.listInstruments !== "function") return { symbols: [] };
|
|
143
|
+
const trimmed = query?.trim().toLowerCase();
|
|
144
|
+
if (trimmed) try {
|
|
145
|
+
const list = await service.listInstruments(trimmed);
|
|
146
|
+
let symbols = Array.isArray(list) ? list.map((item) => ({
|
|
147
|
+
symbol: item.symbol,
|
|
148
|
+
...item.name ? { name: item.name } : {}
|
|
149
|
+
})) : [];
|
|
150
|
+
if (!(symbols.length === 0 || symbols.every((s) => s.symbol.toLowerCase().includes(trimmed) || s.name !== void 0 && s.name.toLowerCase().includes(trimmed)))) symbols = symbols.filter((s) => s.symbol.toLowerCase().includes(trimmed) || s.name !== void 0 && s.name.toLowerCase().includes(trimmed));
|
|
151
|
+
return { symbols };
|
|
152
|
+
} catch {
|
|
153
|
+
return { symbols: [] };
|
|
154
|
+
}
|
|
155
|
+
const cached = this.symbolsCache.get(market);
|
|
156
|
+
if (cached !== void 0 && Date.now() - cached.fetchedAt < 18e5) return { symbols: cached.list };
|
|
157
|
+
try {
|
|
158
|
+
const list = await service.listInstruments();
|
|
159
|
+
const symbols = Array.isArray(list) ? list.map((item) => ({
|
|
160
|
+
symbol: item.symbol,
|
|
161
|
+
...item.name ? { name: item.name } : {}
|
|
162
|
+
})) : [];
|
|
163
|
+
this.symbolsCache.set(market, {
|
|
164
|
+
list: symbols,
|
|
165
|
+
fetchedAt: Date.now()
|
|
166
|
+
});
|
|
167
|
+
return { symbols };
|
|
168
|
+
} catch {
|
|
169
|
+
return { symbols: [] };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 衍生品指标快照(GUI「衍生品」面板,issue #38):单 symbol 透传注册表解析出的
|
|
174
|
+
* 行情服务。连接器未实现可选 getDerivatives(现货/股票数据源)→ 业务错误
|
|
175
|
+
* TRADING_NOT_IMPLEMENTED(HTTP 200 + ok:false),前端直接隐藏面板(不降级)。
|
|
176
|
+
*/
|
|
177
|
+
async derivatives(market, symbol) {
|
|
178
|
+
if (!isMarketId(market)) throw new BridgeProtocolError(400, `unknown market ${JSON.stringify(market)}`);
|
|
179
|
+
const trimmed = symbol.trim();
|
|
180
|
+
if (trimmed === "") throw new BridgeProtocolError(400, "derivatives: symbol is required");
|
|
181
|
+
const service = this.host.getMarketService(market);
|
|
182
|
+
if (service === void 0) throw new BridgeProtocolError(400, `market ${market} is not installed`);
|
|
183
|
+
if (typeof service.getDerivatives !== "function") throw Object.assign(/* @__PURE__ */ new Error(`market ${market} provider does not implement derivatives — spot market`), { code: "TRADING_NOT_IMPLEMENTED" });
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
derivatives: await service.getDerivatives(trimmed)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 衍生品历史序列(GUI「衍生品」页签趋势卡,issue #54):单 symbol 透传。
|
|
191
|
+
* 连接器未实现可选 getDerivativesHistory → TRADING_NOT_IMPLEMENTED 业务错误
|
|
192
|
+
* (HTTP 200 + ok:false),前端隐藏趋势卡、保留快照读数(与快照同纪律)。
|
|
193
|
+
*/
|
|
194
|
+
async derivativesHistory(market, symbol) {
|
|
195
|
+
if (!isMarketId(market)) throw new BridgeProtocolError(400, `unknown market ${JSON.stringify(market)}`);
|
|
196
|
+
const trimmed = symbol.trim();
|
|
197
|
+
if (trimmed === "") throw new BridgeProtocolError(400, "derivatives history: symbol is required");
|
|
198
|
+
const service = this.host.getMarketService(market);
|
|
199
|
+
if (service === void 0) throw new BridgeProtocolError(400, `market ${market} is not installed`);
|
|
200
|
+
if (typeof service.getDerivativesHistory !== "function") throw Object.assign(/* @__PURE__ */ new Error(`market ${market} provider does not implement derivatives history`), { code: "TRADING_NOT_IMPLEMENTED" });
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
history: await service.getDerivativesHistory(trimmed)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 盘口快照(GUI「盘口」竖栏,issue #39):单 symbol 透传。连接器未实现可选
|
|
208
|
+
* getOrderbook(yahoo/stooq/腾讯 r_hk)→ TRADING_NOT_IMPLEMENTED 业务错误,
|
|
209
|
+
* 前端竖栏显示「该市场未提供盘口」。
|
|
210
|
+
*/
|
|
211
|
+
async orderbook(market, symbol) {
|
|
212
|
+
if (!isMarketId(market)) throw new BridgeProtocolError(400, `unknown market ${JSON.stringify(market)}`);
|
|
213
|
+
const trimmed = symbol.trim();
|
|
214
|
+
if (trimmed === "") throw new BridgeProtocolError(400, "orderbook: symbol is required");
|
|
215
|
+
const service = this.host.getMarketService(market);
|
|
216
|
+
if (service === void 0) throw new BridgeProtocolError(400, `market ${market} is not installed`);
|
|
217
|
+
if (typeof service.getOrderbook !== "function") throw Object.assign(/* @__PURE__ */ new Error(`market ${market} provider does not implement orderbook`), { code: "TRADING_NOT_IMPLEMENTED" });
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
orderbook: await service.getOrderbook(trimmed)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 最近逐笔成交(GUI「分笔」流水,issue #39):透传 limit(服务端封顶)。
|
|
225
|
+
* 未实现可选 getRecentTrades(腾讯沪深行情行无逐笔端点)→ TRADING_NOT_IMPLEMENTED。
|
|
226
|
+
*/
|
|
227
|
+
async trades(market, symbol, rawLimit) {
|
|
228
|
+
if (!isMarketId(market)) throw new BridgeProtocolError(400, `unknown market ${JSON.stringify(market)}`);
|
|
229
|
+
const trimmed = symbol.trim();
|
|
230
|
+
if (trimmed === "") throw new BridgeProtocolError(400, "trades: symbol is required");
|
|
231
|
+
const limit = rawLimit === null || rawLimit === void 0 ? void 0 : Number(rawLimit);
|
|
232
|
+
if (limit !== void 0 && (!Number.isInteger(limit) || limit <= 0 || limit > 100)) throw new BridgeProtocolError(400, `trades: limit must be an integer in 1..100`);
|
|
233
|
+
const service = this.host.getMarketService(market);
|
|
234
|
+
if (service === void 0) throw new BridgeProtocolError(400, `market ${market} is not installed`);
|
|
235
|
+
if (typeof service.getRecentTrades !== "function") throw Object.assign(/* @__PURE__ */ new Error(`market ${market} provider does not implement recent trades`), { code: "TRADING_NOT_IMPLEMENTED" });
|
|
236
|
+
return {
|
|
237
|
+
ok: true,
|
|
238
|
+
trades: await service.getRecentTrades(trimmed, limit)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/** 交易服务解析:注册表无注册项(未安装交易连接器)→ undefined(调用方 400)。 */
|
|
242
|
+
#requireTradeService(market) {
|
|
243
|
+
if (!isMarketId(market)) throw new BridgeProtocolError(400, `unknown market ${JSON.stringify(market)}`);
|
|
244
|
+
const trade = this.host.getTradeService?.(market);
|
|
245
|
+
if (trade === void 0) throw new BridgeProtocolError(400, `no trade service for market ${market}`);
|
|
246
|
+
return trade;
|
|
247
|
+
}
|
|
248
|
+
async positions(market) {
|
|
249
|
+
return {
|
|
250
|
+
ok: true,
|
|
251
|
+
positions: await this.#requireTradeService(market).getPositions()
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
async balances(market) {
|
|
255
|
+
const trade = this.#requireTradeService(market);
|
|
256
|
+
if (typeof trade.getBalances !== "function") throw Object.assign(/* @__PURE__ */ new Error("trade service does not implement balances"), { code: "TRADING_NOT_IMPLEMENTED" });
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
balances: await trade.getBalances()
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async openOrders(market) {
|
|
263
|
+
const trade = this.#requireTradeService(market);
|
|
264
|
+
if (typeof trade.listOpenOrders !== "function") throw Object.assign(/* @__PURE__ */ new Error("trade service does not implement open orders"), { code: "TRADING_NOT_IMPLEMENTED" });
|
|
265
|
+
return {
|
|
266
|
+
ok: true,
|
|
267
|
+
orders: await trade.listOpenOrders()
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async tradeFills(market) {
|
|
271
|
+
const trade = this.#requireTradeService(market);
|
|
272
|
+
if (typeof trade.listTradeFills !== "function") throw Object.assign(/* @__PURE__ */ new Error("trade service does not implement trade fills"), { code: "TRADING_NOT_IMPLEMENTED" });
|
|
273
|
+
return {
|
|
274
|
+
ok: true,
|
|
275
|
+
fills: await trade.listTradeFills()
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* GUI 下单(真交易执行):支持实盘报单(默认 dryRun: false)。
|
|
280
|
+
* 若底层连接器未配置 API 凭证或未开启 liveTrading,由服务缝闸门抛出标准错误,
|
|
281
|
+
* 桥层如实向前端返回,杜绝伪造假成交。
|
|
282
|
+
*/
|
|
283
|
+
async placeOrderFromGui(market, body) {
|
|
284
|
+
const trade = this.#requireTradeService(market);
|
|
285
|
+
const symbol = typeof body.symbol === "string" ? body.symbol.trim() : "";
|
|
286
|
+
const side = body.side === "sell" ? "sell" : body.side === "buy" ? "buy" : void 0;
|
|
287
|
+
const type = body.type === "limit" ? "limit" : body.type === "market" ? "market" : void 0;
|
|
288
|
+
const quantity = typeof body.quantity === "number" ? body.quantity : NaN;
|
|
289
|
+
if (symbol === "") throw new BridgeProtocolError(400, "place order: symbol is required");
|
|
290
|
+
if (side === void 0) throw new BridgeProtocolError(400, "place order: side must be buy or sell");
|
|
291
|
+
if (type === void 0) throw new BridgeProtocolError(400, "place order: type must be market or limit");
|
|
292
|
+
if (!Number.isFinite(quantity) || quantity <= 0) throw new BridgeProtocolError(400, "place order: quantity must be a positive number");
|
|
293
|
+
const price = typeof body.price === "number" ? body.price : void 0;
|
|
294
|
+
if (type === "limit" && (price === void 0 || !Number.isFinite(price) || price <= 0)) throw new BridgeProtocolError(400, "place order: limit orders require a positive price");
|
|
295
|
+
const requestedDryRun = typeof body.dryRun === "boolean" ? body.dryRun : false;
|
|
296
|
+
return {
|
|
297
|
+
ok: true,
|
|
298
|
+
order: await trade.placeOrder({
|
|
299
|
+
symbol,
|
|
300
|
+
side,
|
|
301
|
+
type,
|
|
302
|
+
quantity,
|
|
303
|
+
...price !== void 0 ? { price } : {},
|
|
304
|
+
dryRun: requestedDryRun
|
|
305
|
+
})
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/** GUI 撤单(issue #40):按 market + orderId + 可选 symbol 分发到激活的交易服务。 */
|
|
309
|
+
async cancelOrderFromGui(market, orderId, symbol) {
|
|
310
|
+
const trade = this.#requireTradeService(market);
|
|
311
|
+
const trimmedId = orderId.trim();
|
|
312
|
+
if (!trimmedId) throw new BridgeProtocolError(400, "cancel order: id is required");
|
|
313
|
+
await trade.cancelOrder(trimmedId, symbol?.trim());
|
|
314
|
+
return {
|
|
315
|
+
ok: true,
|
|
316
|
+
canceled: true
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/** 自定义指标列表。 */
|
|
320
|
+
async customIndicators() {
|
|
321
|
+
const store = this.host.customIndicatorsStore;
|
|
322
|
+
if (store === void 0) return {
|
|
323
|
+
ok: true,
|
|
324
|
+
indicators: []
|
|
325
|
+
};
|
|
326
|
+
return {
|
|
327
|
+
ok: true,
|
|
328
|
+
indicators: await store.list()
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** 删除自定义指标。 */
|
|
332
|
+
async deleteCustomIndicator(id) {
|
|
333
|
+
const store = this.host.customIndicatorsStore;
|
|
334
|
+
if (store === void 0) return {
|
|
335
|
+
ok: true,
|
|
336
|
+
removed: false
|
|
337
|
+
};
|
|
338
|
+
return {
|
|
339
|
+
ok: true,
|
|
340
|
+
removed: await store.remove(id)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/** 获取全部沉淀的知识卡片列表。 */
|
|
344
|
+
async knowledgeCards() {
|
|
345
|
+
const store = this.host.knowledgeStore;
|
|
346
|
+
if (store === void 0) return {
|
|
347
|
+
ok: true,
|
|
348
|
+
cards: []
|
|
349
|
+
};
|
|
350
|
+
return {
|
|
351
|
+
ok: true,
|
|
352
|
+
cards: await store.list()
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* 标的新闻与公告聚合(issue #37):按市场从 newsRegistry 解析到 Kit 注册的
|
|
357
|
+
* 聚合器;未注册时回退到各 Kit 导出的 aggregateNews 纯函数(无活跃会话时亦可用)。
|
|
358
|
+
* 只返回与标的相关的条目(2026-09-03 owner 裁决):无相关新闻/公告就返回空列表,
|
|
359
|
+
* 不再回退展示大盘要闻——此前的智能回退会把已抓到的公告挤出 limit 截尾窗。
|
|
360
|
+
*/
|
|
361
|
+
async news(market, symbol, rawLimit) {
|
|
362
|
+
if (!isMarketId(market)) throw new BridgeProtocolError(400, `unknown market ${JSON.stringify(market)}`);
|
|
363
|
+
const aggregator = this.host.newsRegistry?.get(market) ?? (market === "cn" ? aggregateNews : market === "hk" ? aggregateNews$1 : market === "us" ? aggregateNews$2 : market === "crypto" ? aggregateNews$3 : void 0);
|
|
364
|
+
if (aggregator === void 0) throw Object.assign(/* @__PURE__ */ new Error(`market ${market} does not have a news provider`), { code: "TRADING_NOT_IMPLEMENTED" });
|
|
365
|
+
const limit = rawLimit === null || rawLimit === void 0 ? void 0 : Number(rawLimit);
|
|
366
|
+
if (limit !== void 0 && (!Number.isInteger(limit) || limit <= 0 || limit > 50)) throw new BridgeProtocolError(400, `news: limit must be an integer in 1..50`);
|
|
367
|
+
const result = await aggregator({
|
|
368
|
+
symbol: symbol ?? void 0,
|
|
369
|
+
limit: limit ?? 20,
|
|
370
|
+
windowHours: 24,
|
|
371
|
+
cryptoPanicKey: this.host.newsKey?.()
|
|
372
|
+
});
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
items: result.items,
|
|
376
|
+
unavailable: result.unavailable
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/** 自定义策略名册(issue #31):返回记录,前端校验后并入名册。 */
|
|
380
|
+
async customStrategies() {
|
|
381
|
+
const store = this.host.strategyStore;
|
|
382
|
+
if (store === void 0) return {
|
|
383
|
+
ok: true,
|
|
384
|
+
strategies: []
|
|
385
|
+
};
|
|
386
|
+
return {
|
|
387
|
+
ok: true,
|
|
388
|
+
strategies: await store.list()
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/** 删除自定义策略(issue #31)。 */
|
|
392
|
+
async deleteCustomStrategy(id) {
|
|
393
|
+
const store = this.host.strategyStore;
|
|
394
|
+
if (store === void 0) return {
|
|
395
|
+
ok: true,
|
|
396
|
+
removed: false
|
|
397
|
+
};
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
removed: await store.remove(id)
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 标的基本面快照与多期财务矩阵(GUI「基本面」页签,2026-09-02 / Issue #36)。
|
|
405
|
+
*
|
|
406
|
+
* 语义(重构自协作者初版,2026-09-02 审查整改):
|
|
407
|
+
* - 数据包(kit 的多期报表/股东/分红等下钻)直接按市场取,不要求连接器实现
|
|
408
|
+
* getFundamentals——us/crypto 由此可达;快照部分为可选增强(连接器实现了才合并)。
|
|
409
|
+
* - 快照与数据包并行取(Promise.all);任一失败只降级自己那半,另一半照常返回。
|
|
410
|
+
* - 两个都失败 → TRADING_NOT_IMPLEMENTED 业务错误(HTTP 200 + ok:false),
|
|
411
|
+
* 前端显示诚实空态;不再有「半旧数据冒充新标的」的路径。
|
|
412
|
+
* - 5 分钟进程内 TTL 缓存(财报级数据,非 tick)+ 同键 in-flight 去重,
|
|
413
|
+
* 对齐本桥 symbols() 的缓存先例;一次 tab 翻转只打一轮上游。
|
|
414
|
+
*/
|
|
415
|
+
async fundamentals(market, symbol) {
|
|
416
|
+
if (!isMarketId(market)) throw new BridgeProtocolError(400, `unknown market ${JSON.stringify(market)}`);
|
|
417
|
+
const trimmed = symbol.trim();
|
|
418
|
+
if (trimmed === "") throw new BridgeProtocolError(400, "fundamentals: symbol is required");
|
|
419
|
+
const service = this.host.getMarketService(market);
|
|
420
|
+
if (service === void 0) throw new BridgeProtocolError(400, `market ${market} is not installed`);
|
|
421
|
+
const cacheKey = `${market}:${trimmed}`;
|
|
422
|
+
const cached = this.fundamentalsCache.get(cacheKey);
|
|
423
|
+
if (cached !== void 0 && Date.now() - cached.fetchedAt < 3e5) return {
|
|
424
|
+
ok: true,
|
|
425
|
+
fundamentals: cached.pkg
|
|
426
|
+
};
|
|
427
|
+
const inflight = this.fundamentalsInflight.get(cacheKey);
|
|
428
|
+
if (inflight !== void 0) return {
|
|
429
|
+
ok: true,
|
|
430
|
+
fundamentals: await inflight
|
|
431
|
+
};
|
|
432
|
+
const job = (async () => {
|
|
433
|
+
const [snapshot, pkg] = await Promise.all([typeof service.getFundamentals === "function" ? service.getFundamentals(trimmed).catch(() => void 0) : Promise.resolve(void 0), this.#fetchPkg(market, trimmed)]);
|
|
434
|
+
if (snapshot === void 0 && pkg === void 0) throw Object.assign(/* @__PURE__ */ new Error(`no fundamentals data for ${market}/${trimmed} — provider and drill-down both unavailable`), { code: "TRADING_NOT_IMPLEMENTED" });
|
|
435
|
+
const result = pkg !== void 0 ? {
|
|
436
|
+
...pkg,
|
|
437
|
+
stock: {
|
|
438
|
+
...pkg.stock ?? {},
|
|
439
|
+
...snapshot ?? {}
|
|
440
|
+
},
|
|
441
|
+
timestamp: snapshot?.timestamp ?? Date.now()
|
|
442
|
+
} : snapshot;
|
|
443
|
+
this.fundamentalsCache.set(cacheKey, {
|
|
444
|
+
pkg: result,
|
|
445
|
+
fetchedAt: Date.now()
|
|
446
|
+
});
|
|
447
|
+
return result;
|
|
448
|
+
})();
|
|
449
|
+
this.fundamentalsInflight.set(cacheKey, job);
|
|
450
|
+
try {
|
|
451
|
+
return {
|
|
452
|
+
ok: true,
|
|
453
|
+
fundamentals: await job
|
|
454
|
+
};
|
|
455
|
+
} finally {
|
|
456
|
+
this.fundamentalsInflight.delete(cacheKey);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/** 按市场拉 kit 基本面数据包;kit 未覆盖或上游失败 → undefined(不算错误)。 */
|
|
460
|
+
async #fetchPkg(market, symbol) {
|
|
461
|
+
try {
|
|
462
|
+
const pkg = market === "cn" ? await fetchCnFundamentalsPackage(symbol) : market === "hk" ? await fetchHkFundamentalsPackage(symbol) : market === "us" ? await fetchUsFundamentalsPackage(symbol) : market === "crypto" ? await fetchCryptoFundamentalsPackage(symbol) : void 0;
|
|
463
|
+
if (pkg === void 0) return void 0;
|
|
464
|
+
return pkg.matrix !== void 0 || pkg.stock !== void 0 || pkg.crypto !== void 0 || pkg.profile?.description !== void 0 || pkg.profile?.industry !== void 0 || (pkg.shareholders?.length ?? 0) > 0 || (pkg.reports?.length ?? 0) > 0 || (pkg.mainOperations?.length ?? 0) > 0 || (pkg.dividends?.length ?? 0) > 0 || pkg.forecast !== void 0 || pkg.holderSummary !== void 0 || pkg.efficiency !== void 0 || (pkg.insiderTrades?.length ?? 0) > 0 || (pkg.institutionalHoldings?.length ?? 0) > 0 || (pkg.dividends?.length ?? 0) > 0 || (pkg.splits?.length ?? 0) > 0 ? pkg : void 0;
|
|
465
|
+
} catch {}
|
|
466
|
+
}
|
|
467
|
+
/** 全量读取自选行(不含客户端种子回退)。 */
|
|
468
|
+
async watchlistRows() {
|
|
469
|
+
const store = this.host.watchlistStore;
|
|
470
|
+
if (store === void 0) return {
|
|
471
|
+
ok: true,
|
|
472
|
+
watchlists: {}
|
|
473
|
+
};
|
|
474
|
+
return {
|
|
475
|
+
ok: true,
|
|
476
|
+
watchlists: await store.list()
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/** 全量替换自选(客户端启动同步)。 */
|
|
480
|
+
async replaceWatchlists(body) {
|
|
481
|
+
const store = this.host.watchlistStore;
|
|
482
|
+
if (store === void 0) return {
|
|
483
|
+
ok: true,
|
|
484
|
+
watchlists: {}
|
|
485
|
+
};
|
|
486
|
+
const map = parseWatchlistsMap(body);
|
|
487
|
+
await store.save(map);
|
|
488
|
+
return {
|
|
489
|
+
ok: true,
|
|
490
|
+
watchlists: await store.list()
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/** 追加一行(POST /watchlists)。 */
|
|
494
|
+
async addWatchlistRow(body) {
|
|
495
|
+
const store = this.host.watchlistStore;
|
|
496
|
+
if (store === void 0) return {
|
|
497
|
+
ok: true,
|
|
498
|
+
added: false,
|
|
499
|
+
instrument: parseInstrumentBody(body)
|
|
500
|
+
};
|
|
501
|
+
const instrument = parseInstrumentBody(body);
|
|
502
|
+
return {
|
|
503
|
+
ok: true,
|
|
504
|
+
added: await store.add(instrument.market, instrument),
|
|
505
|
+
instrument
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/** 移除一行(DELETE /watchlists?market&symbol)。 */
|
|
509
|
+
async removeWatchlistRow(market, symbol) {
|
|
510
|
+
const store = this.host.watchlistStore;
|
|
511
|
+
if (store === void 0 || !market || !symbol) return {
|
|
512
|
+
ok: true,
|
|
513
|
+
removed: false
|
|
514
|
+
};
|
|
515
|
+
return {
|
|
516
|
+
ok: true,
|
|
517
|
+
removed: await store.remove(market, symbol)
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* 一次性迁移导入(POST /watchlists/import):host 非空拒绝(幂等,防重复导入)。
|
|
522
|
+
*/
|
|
523
|
+
async importWatchlists(body) {
|
|
524
|
+
const store = this.host.watchlistStore;
|
|
525
|
+
if (store === void 0) return {
|
|
526
|
+
ok: false,
|
|
527
|
+
imported: false,
|
|
528
|
+
reason: "watchlist store is not mounted"
|
|
529
|
+
};
|
|
530
|
+
const existing = await store.list();
|
|
531
|
+
if (Object.values(existing).reduce((sum, rows) => sum + (rows?.length ?? 0), 0) > 0) return {
|
|
532
|
+
ok: false,
|
|
533
|
+
imported: false,
|
|
534
|
+
reason: "host watchlist store is not empty — migration already done (idempotent guard)"
|
|
535
|
+
};
|
|
536
|
+
const map = parseWatchlistsMap(body);
|
|
537
|
+
await store.save(map);
|
|
538
|
+
return {
|
|
539
|
+
ok: true,
|
|
540
|
+
imported: true
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
/** 读取选中标的(GET /selection)。 */
|
|
544
|
+
async selection() {
|
|
545
|
+
const store = this.host.selectionStore;
|
|
546
|
+
if (store === void 0) return {
|
|
547
|
+
ok: true,
|
|
548
|
+
instrument: null
|
|
549
|
+
};
|
|
550
|
+
return {
|
|
551
|
+
ok: true,
|
|
552
|
+
instrument: (await store.get()).instrument
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/** 设置选中标的(PUT /selection;watchlist_select 工具与左栏点击同源)。 */
|
|
556
|
+
async putSelection(body) {
|
|
557
|
+
const store = this.host.selectionStore;
|
|
558
|
+
const parsed = body;
|
|
559
|
+
const instrument = parsed?.instrument === void 0 || parsed.instrument === null ? null : {
|
|
560
|
+
market: String(parsed.instrument.market ?? ""),
|
|
561
|
+
symbol: String(parsed.instrument.symbol ?? ""),
|
|
562
|
+
...parsed.instrument.name !== void 0 ? { name: String(parsed.instrument.name) } : {}
|
|
563
|
+
};
|
|
564
|
+
if (store === void 0) return {
|
|
565
|
+
ok: true,
|
|
566
|
+
instrument
|
|
567
|
+
};
|
|
568
|
+
await store.set({ instrument });
|
|
569
|
+
return {
|
|
570
|
+
ok: true,
|
|
571
|
+
instrument
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
/** 自选 map 的形状校验(Record<market, Instrument[]>,宽容 name 缺省)。 */
|
|
576
|
+
function parseWatchlistsMap(body) {
|
|
577
|
+
if (typeof body !== "object" || body === null) throw new BridgeProtocolError(400, "watchlists body must be an object");
|
|
578
|
+
const raw = body.watchlists ?? body;
|
|
579
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new BridgeProtocolError(400, "watchlists must be an object keyed by market");
|
|
580
|
+
const out = {};
|
|
581
|
+
for (const [market, rows] of Object.entries(raw)) {
|
|
582
|
+
if (!Array.isArray(rows)) continue;
|
|
583
|
+
out[market] = rows.map((row) => {
|
|
584
|
+
const r = row;
|
|
585
|
+
if (typeof r?.symbol !== "string" || !r.symbol) throw new BridgeProtocolError(400, `watchlists[${market}] rows must have string symbol`);
|
|
586
|
+
return {
|
|
587
|
+
market: typeof r.market === "string" ? r.market : market,
|
|
588
|
+
symbol: r.symbol,
|
|
589
|
+
...typeof r.name === "string" && r.name ? { name: r.name } : {}
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return out;
|
|
594
|
+
}
|
|
595
|
+
/** 单行 instrument 的形状校验。 */
|
|
596
|
+
function parseInstrumentBody(body) {
|
|
597
|
+
const raw = body ?? {};
|
|
598
|
+
const market = typeof raw.market === "string" ? raw.market.trim() : "";
|
|
599
|
+
const symbol = typeof raw.symbol === "string" ? raw.symbol.trim() : "";
|
|
600
|
+
if (!market || !symbol) throw new BridgeProtocolError(400, "instrument body requires string market and symbol");
|
|
601
|
+
return {
|
|
602
|
+
market,
|
|
603
|
+
symbol,
|
|
604
|
+
...typeof raw.name === "string" && raw.name ? { name: raw.name } : {}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
/** 请求分发:把 (method, pathname, searchParams) 路由到桥方法,返回 (status, payload)。 */
|
|
608
|
+
async function dispatchBridgeRequest(bridge, method, pathname, search, body) {
|
|
609
|
+
if (method === "GET") switch (pathname) {
|
|
610
|
+
case "/markets": return {
|
|
611
|
+
status: 200,
|
|
612
|
+
payload: bridge.markets()
|
|
613
|
+
};
|
|
614
|
+
case "/tickers": {
|
|
615
|
+
const market = search.get("market") ?? "";
|
|
616
|
+
const symbols = (search.get("symbols") ?? "").split(",");
|
|
617
|
+
return {
|
|
618
|
+
status: 200,
|
|
619
|
+
payload: await bridge.tickers(market, symbols)
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
case "/klines": {
|
|
623
|
+
const market = search.get("market") ?? "";
|
|
624
|
+
const symbol = search.get("symbol") ?? "";
|
|
625
|
+
const interval = search.get("interval") ?? "1d";
|
|
626
|
+
const limit = search.get("limit");
|
|
627
|
+
return {
|
|
628
|
+
status: 200,
|
|
629
|
+
payload: await bridge.klines(market, symbol, interval, limit)
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
case "/symbols": {
|
|
633
|
+
const market = search.get("market") ?? "";
|
|
634
|
+
const query = search.get("query") ?? void 0;
|
|
635
|
+
return {
|
|
636
|
+
status: 200,
|
|
637
|
+
payload: await bridge.symbols(market, query)
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
case "/fundamentals": {
|
|
641
|
+
const market = search.get("market") ?? "";
|
|
642
|
+
const symbol = search.get("symbol") ?? "";
|
|
643
|
+
return {
|
|
644
|
+
status: 200,
|
|
645
|
+
payload: await bridge.fundamentals(market, symbol)
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
case "/derivatives": {
|
|
649
|
+
const market = search.get("market") ?? "";
|
|
650
|
+
const symbol = search.get("symbol") ?? "";
|
|
651
|
+
return {
|
|
652
|
+
status: 200,
|
|
653
|
+
payload: await bridge.derivatives(market, symbol)
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
case "/derivatives/history": {
|
|
657
|
+
const market = search.get("market") ?? "";
|
|
658
|
+
const symbol = search.get("symbol") ?? "";
|
|
659
|
+
return {
|
|
660
|
+
status: 200,
|
|
661
|
+
payload: await bridge.derivativesHistory(market, symbol)
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
case "/orderbook": {
|
|
665
|
+
const market = search.get("market") ?? "";
|
|
666
|
+
const symbol = search.get("symbol") ?? "";
|
|
667
|
+
return {
|
|
668
|
+
status: 200,
|
|
669
|
+
payload: await bridge.orderbook(market, symbol)
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
case "/trades": {
|
|
673
|
+
const market = search.get("market") ?? "";
|
|
674
|
+
const symbol = search.get("symbol") ?? "";
|
|
675
|
+
return {
|
|
676
|
+
status: 200,
|
|
677
|
+
payload: await bridge.trades(market, symbol, search.get("limit"))
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
case "/trade/positions": return {
|
|
681
|
+
status: 200,
|
|
682
|
+
payload: await bridge.positions(search.get("market") ?? "")
|
|
683
|
+
};
|
|
684
|
+
case "/trade/balances": return {
|
|
685
|
+
status: 200,
|
|
686
|
+
payload: await bridge.balances(search.get("market") ?? "")
|
|
687
|
+
};
|
|
688
|
+
case "/trade/orders": return {
|
|
689
|
+
status: 200,
|
|
690
|
+
payload: await bridge.openOrders(search.get("market") ?? "")
|
|
691
|
+
};
|
|
692
|
+
case "/trade/fills": return {
|
|
693
|
+
status: 200,
|
|
694
|
+
payload: await bridge.tradeFills(search.get("market") ?? "")
|
|
695
|
+
};
|
|
696
|
+
case "/indicators/custom": return {
|
|
697
|
+
status: 200,
|
|
698
|
+
payload: await bridge.customIndicators()
|
|
699
|
+
};
|
|
700
|
+
case "/knowledge/cards": return {
|
|
701
|
+
status: 200,
|
|
702
|
+
payload: await bridge.knowledgeCards()
|
|
703
|
+
};
|
|
704
|
+
case "/news": {
|
|
705
|
+
const market = search.get("market") ?? "";
|
|
706
|
+
if (!market) throw new BridgeProtocolError(400, "news: market is required");
|
|
707
|
+
return {
|
|
708
|
+
status: 200,
|
|
709
|
+
payload: await bridge.news(market, search.get("symbol"), search.get("limit"))
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
case "/strategies/custom": return {
|
|
713
|
+
status: 200,
|
|
714
|
+
payload: await bridge.customStrategies()
|
|
715
|
+
};
|
|
716
|
+
case "/watchlists": return {
|
|
717
|
+
status: 200,
|
|
718
|
+
payload: await bridge.watchlistRows()
|
|
719
|
+
};
|
|
720
|
+
case "/selection": return {
|
|
721
|
+
status: 200,
|
|
722
|
+
payload: await bridge.selection()
|
|
723
|
+
};
|
|
724
|
+
default: throw new BridgeProtocolError(404, `no such endpoint: ${pathname}`);
|
|
725
|
+
}
|
|
726
|
+
if (method === "DELETE") {
|
|
727
|
+
if (pathname === "/indicators/custom") {
|
|
728
|
+
const id = search.get("id") ?? "";
|
|
729
|
+
if (!id) throw new BridgeProtocolError(400, "delete custom indicator: id is required");
|
|
730
|
+
return {
|
|
731
|
+
status: 200,
|
|
732
|
+
payload: await bridge.deleteCustomIndicator(id)
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (pathname === "/strategies/custom") {
|
|
736
|
+
const id = search.get("id") ?? "";
|
|
737
|
+
if (!id) throw new BridgeProtocolError(400, "delete custom strategy: id is required");
|
|
738
|
+
return {
|
|
739
|
+
status: 200,
|
|
740
|
+
payload: await bridge.deleteCustomStrategy(id)
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
if (pathname === "/trade/order") {
|
|
744
|
+
const market = search.get("market") ?? "";
|
|
745
|
+
const orderId = search.get("id") ?? search.get("orderId") ?? "";
|
|
746
|
+
const symbol = search.get("symbol") ?? void 0;
|
|
747
|
+
if (!market) throw new BridgeProtocolError(400, "cancel order: market is required");
|
|
748
|
+
if (!orderId) throw new BridgeProtocolError(400, "cancel order: id is required");
|
|
749
|
+
return {
|
|
750
|
+
status: 200,
|
|
751
|
+
payload: await bridge.cancelOrderFromGui(market, orderId, symbol)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
if (pathname === "/watchlists") {
|
|
755
|
+
const market = search.get("market") ?? "";
|
|
756
|
+
const symbol = search.get("symbol") ?? "";
|
|
757
|
+
if (!market || !symbol) throw new BridgeProtocolError(400, "delete watchlist row: market and symbol are required");
|
|
758
|
+
return {
|
|
759
|
+
status: 200,
|
|
760
|
+
payload: await bridge.removeWatchlistRow(market, symbol)
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
throw new BridgeProtocolError(404, `no such endpoint: ${pathname}`);
|
|
764
|
+
}
|
|
765
|
+
if (method === "PUT") {
|
|
766
|
+
if (pathname === "/watchlists") return {
|
|
767
|
+
status: 200,
|
|
768
|
+
payload: await bridge.replaceWatchlists(body)
|
|
769
|
+
};
|
|
770
|
+
if (pathname === "/selection") return {
|
|
771
|
+
status: 200,
|
|
772
|
+
payload: await bridge.putSelection(body)
|
|
773
|
+
};
|
|
774
|
+
throw new BridgeProtocolError(404, `no such endpoint: ${pathname}`);
|
|
775
|
+
}
|
|
776
|
+
if (method === "POST") {
|
|
777
|
+
if (pathname === "/trade/order") return {
|
|
778
|
+
status: 200,
|
|
779
|
+
payload: await bridge.placeOrderFromGui(search.get("market") ?? "", body)
|
|
780
|
+
};
|
|
781
|
+
if (pathname === "/watchlists") return {
|
|
782
|
+
status: 200,
|
|
783
|
+
payload: await bridge.addWatchlistRow(body)
|
|
784
|
+
};
|
|
785
|
+
if (pathname === "/watchlists/import") return {
|
|
786
|
+
status: 200,
|
|
787
|
+
payload: await bridge.importWatchlists(body)
|
|
788
|
+
};
|
|
789
|
+
throw new BridgeProtocolError(404, `no such endpoint: ${pathname}`);
|
|
790
|
+
}
|
|
791
|
+
throw new BridgeProtocolError(405, "only GET/PUT/POST/DELETE are supported");
|
|
792
|
+
}
|
|
793
|
+
//#endregion
|
|
794
|
+
export { BridgeProtocolError, FUNDAMENTALS_CACHE_TTL_MS, MARKET_IDS, MARKET_SERVICE_KEYS, MAX_KLINE_LIMIT, SYMBOLS_CACHE_TTL_MS, TradingBridge, createBridgeHost, dispatchBridgeRequest, errorPayload };
|