@dshtrading/connector-tencent 0.6.1 → 0.6.2
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/README.md +51 -3
- package/lib/dataplane.d.ts +2 -2
- package/lib/dataplane.js +5 -5
- package/lib/index.d.ts +38 -3
- package/lib/index.js +45 -2
- package/lib/rest.d.ts +65 -2
- package/lib/rest.js +242 -18
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# @dshtrading/connector-tencent
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **状态:已实证(本出口可用)**。报价(`qt.gtimg.cn`)、日/周/月 K(`web.ifzq.gtimg.cn`
|
|
4
|
+
> `fqkline`/`hkfqkline`)、分钟 K(`ifzq.gtimg.cn` `mkline`)与批量报价均从本开发出口返回真实数据。
|
|
5
|
+
> 证据 `spikes/impl-cn-hk/REPORT.md`、`spikes/impl-tencent-minute/EVIDENCE.md`、
|
|
6
|
+
> `spikes/impl-tencent-quote-batch/EVIDENCE.md`、`spikes/impl-cn-datasource-bench/EVIDENCE.md`。
|
|
4
7
|
|
|
5
|
-
dsh-trading **cn+hk 双市场切片**市场连接器:经腾讯公共行情端点实现 `@dshtrading/api` 的
|
|
8
|
+
dsh-trading **cn+hk 双市场切片**市场连接器:经腾讯公共行情端点实现 `@dshtrading/api` 的
|
|
9
|
+
`MarketDataService` 契约,并提供 `<market>_get_ticker` / `<market>_get_klines` / `<market>_place_order`
|
|
10
|
+
三工具(下单三段闸门与 connector-binance/connector-stooq 同构)。
|
|
6
11
|
|
|
7
12
|
## 单包双市场(多实例模式)
|
|
8
13
|
|
|
@@ -13,13 +18,56 @@ dsh-trading **cn+hk 双市场切片**市场连接器:经腾讯公共行情端
|
|
|
13
18
|
- cn 实例 provide `tradingCnMarketData` + 注册 `cn_get_ticker/cn_get_klines/cn_place_order`;hk 实例 provide `tradingHkMarketData` + `hk_` 三件;
|
|
14
19
|
- isolate 组键 = 对应服务名(preset 挂载硬规则)。
|
|
15
20
|
|
|
21
|
+
## 配置
|
|
22
|
+
|
|
23
|
+
| 字段 | 默认 | 说明 |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `market` | 必填 | `cn` = A 股,`hk` = 港股(两个实例各写一行) |
|
|
26
|
+
| `dryRun` | `true` | 下单工具默认模拟 |
|
|
27
|
+
| `liveTrading` | `false` | 实盘总开关;base 审批闸门不得绕过 |
|
|
28
|
+
|
|
29
|
+
## 上游端点
|
|
30
|
+
|
|
31
|
+
| 用途 | 端点 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| 报价(单只与批量同端点) | `qt.gtimg.cn/q=<wire[,wire…]>`(`q=` 原生支持逗号列表) |
|
|
34
|
+
| 日/周/月 K(前复权) | `web.ifzq.gtimg.cn/appstock/app/{fqkline,hkfqkline}/get` |
|
|
35
|
+
| 分钟 K(**仅 cn**) | `ifzq.gtimg.cn/appstock/app/kline/mkline`(host **无 `www.`**,带则 301) |
|
|
36
|
+
| 名册检索 | `smartbox.gtimg.cn/s3/?t=all&q=` |
|
|
37
|
+
|
|
38
|
+
单次请求超时 10 s(`DEFAULT_TIMEOUT_MS`)。
|
|
39
|
+
|
|
40
|
+
## 周期与能力面
|
|
41
|
+
|
|
42
|
+
- 周期:cn `5m/30m/1d/1w/1M`(`mkline` / `fqkline` 前复权);**hk 仅 `1d/1w/1M`**,分钟周期抛
|
|
43
|
+
`TRADING_UNSUPPORTED_INTERVAL`(公开端点不通)。
|
|
44
|
+
- `getTickers` 一次上游请求取多只标的(键 = 入参原字符串,未收录的整行缺席故跳过,整批失败抛出);
|
|
45
|
+
实现不分片,标的数由调用方裁剪。
|
|
46
|
+
- cn 实例独有:`getActiveBuySnapshot`(同一报价行的外盘/内盘,「盘口推断」口径);
|
|
47
|
+
该方法在 hk 实例上**本身缺席**,消费方按「方法是否存在」判降级。
|
|
48
|
+
- `getOrderbook`(同一报价行五档)两个实例都声明,但 hk 调用抛 `TRADING_NOT_IMPLEMENTED`
|
|
49
|
+
——`r_hk` 行的档位字段恒 0,桥层据此显示「未提供」而不是空盘口。
|
|
50
|
+
- cn/hk 均有:`getTicker` / `getKlines` / `getFundamentals` / `listInstruments` /
|
|
51
|
+
`subscribeTicker`(快照轮询,最小 250 ms,默认 5 s)。
|
|
52
|
+
|
|
16
53
|
## 已知局限(实现时已内置)
|
|
17
54
|
|
|
18
55
|
- 报价响应为 **GBK 编码**,客户端用 `TextDecoder('gbk')` 解码(Node 全 ICU 内置);
|
|
19
56
|
- cn/hk 报价字段布局不同:cn 成交量单位是**手**(×100 归一到股)、hk 是**股**;hk 买卖档位字段全 0(`r_hk` 无实时档)→ bid/ask 缺省;
|
|
20
57
|
- hk K 线 wire 前缀是 `hk`(报价才是 `r_hk`);K 线行字段序是**开收高低量**,hk 行第 7 元素起为分红/回购附加对象(解析丢弃);
|
|
21
|
-
-
|
|
58
|
+
- **超限静默降级**(HTTP 仍 200、`code=0`):`mkline` 的 `count > 800` 降到 320 根;`fqkline` 的 `count ≥ 801` 降到 641 根。
|
|
59
|
+
实现侧 `Math.min(limit, 800)` 让这两个悬崖在调用面上不可达,代价是**日 K 最多只能取到 801 根(约 3.2 年)**,
|
|
60
|
+
需要更长历史须换 provider;
|
|
61
|
+
- 北交所不在符号词汇内(只认 `sh`/`sz` 前缀),裸 wire 也只回 09:00:00 / 零成交量的占位行。
|
|
22
62
|
|
|
23
63
|
## 合规记录(README 铁律 #5)
|
|
24
64
|
|
|
25
65
|
**腾讯公共行情端点:公开、无 key、无官方授权;个人使用边界自负**,以腾讯服务条款为准。本仓不缓存、不批量抓取、不再分发行情数据。腾讯不提供交易 API——live 下单路径恒为 `TRADING_NOT_IMPLEMENTED`(券商 API 是后续任务)。
|
|
66
|
+
|
|
67
|
+
## 出网验证
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
node spikes/impl-cn-hk/r3-real-network-verify.mjs # cn+hk 链路(报价 + K 线)
|
|
71
|
+
node spikes/impl-tencent-quote-batch/r0-live-probe.mjs # 批量报价(默认只打印,不落盘)
|
|
72
|
+
./node_modules/.bin/tsx spikes/impl-cn-datasource-bench/bench.mts # 时延 / 并发 / 批量 / 分钟 K 上限
|
|
73
|
+
```
|
package/lib/dataplane.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import { Config } from "./index.js";
|
|
|
2
2
|
import { Context } from "@deepseek-ai/cordis";
|
|
3
3
|
//#region src/dataplane.d.ts
|
|
4
4
|
declare const inject: string[];
|
|
5
|
-
declare function apply(ctx: Context, config
|
|
5
|
+
declare function apply(ctx: Context, config?: Config): void;
|
|
6
6
|
//#endregion
|
|
7
|
-
export { apply, inject };
|
|
7
|
+
export { Config, apply, inject };
|
package/lib/dataplane.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { TencentMarketDataService, marketDataKey } from "./index.js";
|
|
1
|
+
import { Config, ROUTER_PROVIDER, TencentMarketDataService, marketDataKey } from "./index.js";
|
|
2
2
|
//#region src/dataplane.ts
|
|
3
3
|
const inject = [];
|
|
4
|
+
/** schema 的运行时可调用形态(校验即补默认值);手工构造 ctx 不传 config 时按 schema 默认值处理。 */
|
|
5
|
+
const withDefaults = (value) => Config(value);
|
|
4
6
|
/** 解析注册表服务;老部署(base/router 未升级)返回 undefined → 调用方回退旧的直接 provide 路径。 */
|
|
5
7
|
function resolveMarketDataRegistry(ctx) {
|
|
6
8
|
const candidate = ctx.get?.("tradingMarketDataRegistry", false);
|
|
7
9
|
return candidate !== void 0 ? candidate : void 0;
|
|
8
10
|
}
|
|
9
|
-
|
|
10
|
-
const ROUTER_PROVIDER = "tencent";
|
|
11
|
-
function apply(ctx, config) {
|
|
11
|
+
function apply(ctx, config = withDefaults(void 0)) {
|
|
12
12
|
const market = config.market;
|
|
13
13
|
const key = marketDataKey(market);
|
|
14
14
|
const registry = resolveMarketDataRegistry(ctx);
|
|
@@ -21,4 +21,4 @@ function apply(ctx, config) {
|
|
|
21
21
|
ctx.effect(() => registry.register(market, ROUTER_PROVIDER, service));
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
|
-
export { apply, inject };
|
|
24
|
+
export { Config, apply, inject };
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { CnTickerExtra, HkTickerExtra, INTERVAL_VOCABULARY, TencentFundamentals, TencentIntervalDef, TencentKlineType, TencentMarket, TencentRestClient, TencentRestOptions, TencentTicker, TradingServiceError, klineDateToEpochMs, minuteKlineTimeToEpochMs, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnOrderbook, toCanonicalTencentSymbol, wallTimeToEpochMs } from "./rest.js";
|
|
1
|
+
import { ACTIVE_BUY_SOURCE, CnTickerExtra, HkTickerExtra, INTERVAL_VOCABULARY, TENCENT_KLINE_MAX_BARS, TencentFundamentals, TencentIntervalDef, TencentKlineType, TencentMarket, TencentRestClient, TencentRestOptions, TencentTicker, TradingServiceError, isoWithOffset, klineDateToEpochMs, minuteKlineTimeToEpochMs, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnActiveBuySnapshot, parseCnOrderbook, tencentKlineCapabilities, toCanonicalTencentSymbol, wallTimeToEpochMs } from "./rest.js";
|
|
2
2
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
3
3
|
import Schema from "@deepseek-ai/schemastery";
|
|
4
|
-
import { Disposable, Interval, Kline, MarketDataService, Orderbook, StockFundamentals, Ticker } from "@dshtrading/api";
|
|
4
|
+
import { ActiveBuySnapshot, Disposable, Interval, Kline, KlineCapability, MarketDataService, Orderbook, StockFundamentals, Ticker } from "@dshtrading/api";
|
|
5
5
|
//#region src/index.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Cordis 插件名:`dsh-trading-*` 命名空间(TEMPLATES §8)。**两个 preset 实例共享此
|
|
@@ -31,7 +31,16 @@ interface SubscribeTickerOptions {
|
|
|
31
31
|
declare class TencentMarketDataService extends Service implements MarketDataService {
|
|
32
32
|
private readonly client;
|
|
33
33
|
constructor(ctx: Context, market: MarketOption, options?: TencentRestOptions, name?: string);
|
|
34
|
+
/** 当日累计主动买盘快照;仅 cn 实例存在(hk 实例为 undefined)。 */
|
|
35
|
+
getActiveBuySnapshot?: (symbol: string) => Promise<ActiveBuySnapshot>;
|
|
34
36
|
getTicker(symbol: string): Promise<TencentTicker>;
|
|
37
|
+
/**
|
|
38
|
+
* 批量最新报价(MarketDataService 可选契约 2026-10-14):一次上游请求取多只标的,
|
|
39
|
+
* 自选栏「每拍 N 次请求」降成 1 次(cn / hk 实例同能力,均走同一报价行端点)。
|
|
40
|
+
* 键=入参原字符串、未知/字段不足的符号跳过、整批失败抛出、不自行分片——语义见
|
|
41
|
+
* TencentRestClient.getTickers。
|
|
42
|
+
*/
|
|
43
|
+
getTickers(symbols: string[]): Promise<Record<string, TencentTicker>>;
|
|
35
44
|
/** 基本面快照(同一报价行解析,MarketDataService 可选契约 2026-09-02)。 */
|
|
36
45
|
getFundamentals(symbol: string): Promise<StockFundamentals>;
|
|
37
46
|
/**
|
|
@@ -40,6 +49,11 @@ declare class TencentMarketDataService extends Service implements MarketDataServ
|
|
|
40
49
|
*/
|
|
41
50
|
getOrderbook(symbol: string): Promise<Orderbook>;
|
|
42
51
|
getKlines(symbol: string, interval: Interval, limit?: number): Promise<Kline[]>;
|
|
52
|
+
/**
|
|
53
|
+
* K 线能力声明(api 可选契约,2026-10-15 跨源 K 线路由):透传 REST 客户端的本市场口径
|
|
54
|
+
* (hk 实例不含 5m/30m;深度 = 单请求上限 800)。
|
|
55
|
+
*/
|
|
56
|
+
klineCapabilities(): readonly KlineCapability[];
|
|
43
57
|
listInstruments(query?: string): Promise<Array<{
|
|
44
58
|
symbol: string;
|
|
45
59
|
name: string;
|
|
@@ -86,6 +100,27 @@ interface PlaceOrderToolDeps {
|
|
|
86
100
|
* `tools/pre-execute` waterfall 统一 ask(S4:headless 下 ask=deny,fail-closed)。
|
|
87
101
|
*/
|
|
88
102
|
declare function createPlaceOrderTool(deps: PlaceOrderToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
103
|
+
/**
|
|
104
|
+
* 本连接器在路由层的 provider slug(cn/hk 两个实例共用同一个 slug;
|
|
105
|
+
* docs/guides/exchange-routing.md §2.2)。
|
|
106
|
+
*/
|
|
107
|
+
declare const ROUTER_PROVIDER = "tencent";
|
|
108
|
+
/**
|
|
109
|
+
* 路由闸门(互斥纪律):preset 平面的同市场连接器行挂在**同一个**
|
|
110
|
+
* `cordis:group + isolate` realm 里(`isolate: { tradingCnMarketData: true }` 的
|
|
111
|
+
* label=true → loader 给该组一个 LocalRealm、组内子行共用,见 cordis-plugin-loader
|
|
112
|
+
* 的 `isolate()`),因此任意时刻只能有一个 provider 真正 provide 市场服务键——每个
|
|
113
|
+
* provider 自己按路由让位。
|
|
114
|
+
*
|
|
115
|
+
* 缺这道闸门时:用户把 cn/hk 路由切到别家(如 eastmoney),腾讯实例仍无条件 provide →
|
|
116
|
+
* 后挂载的那行抛 `service "tradingCnMarketData" has been registered at
|
|
117
|
+
* <dsh-trading-tencent>` → 整个 agent preset 判 invalid,新建会话直接失败
|
|
118
|
+
* (2026-10-14 实测复现)。
|
|
119
|
+
*
|
|
120
|
+
* 无 router 服务的老部署(或手搓 ctx 的单测)→ 放行,保持旧的单 provider 行为;
|
|
121
|
+
* 与 eastmoney/xysz/qmt/tushare/akshare 的 `routeAllows` 同款语义。
|
|
122
|
+
*/
|
|
123
|
+
declare function routeAllows(ctx: Context, market: MarketOption): boolean;
|
|
89
124
|
declare function apply(ctx: Context, config: Config): void;
|
|
90
125
|
//#endregion
|
|
91
|
-
export { CnTickerExtra, Config, HkTickerExtra, INTERVAL_VOCABULARY, MarketOption, OrderGateVerdict, PlaceOrderArgs, PlaceOrderToolDeps, SubscribeTickerOptions, TRADING_CN_MARKET_DATA_KEY, TRADING_HK_MARKET_DATA_KEY, TencentFundamentals, TencentIntervalDef, TencentKlineType, TencentMarket, TencentMarketDataService, TencentRestClient, TencentRestOptions, TencentTicker, TradingServiceError, apply, buildDryRunReceipt, createPlaceOrderTool, evaluateOrderGate, inject, klineDateToEpochMs, marketDataKey, minuteKlineTimeToEpochMs, name, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnOrderbook, toCanonicalTencentSymbol, wallTimeToEpochMs };
|
|
126
|
+
export { ACTIVE_BUY_SOURCE, CnTickerExtra, Config, HkTickerExtra, INTERVAL_VOCABULARY, MarketOption, OrderGateVerdict, PlaceOrderArgs, PlaceOrderToolDeps, ROUTER_PROVIDER, SubscribeTickerOptions, TENCENT_KLINE_MAX_BARS, TRADING_CN_MARKET_DATA_KEY, TRADING_HK_MARKET_DATA_KEY, TencentFundamentals, TencentIntervalDef, TencentKlineType, TencentMarket, TencentMarketDataService, TencentRestClient, TencentRestOptions, TencentTicker, TradingServiceError, apply, buildDryRunReceipt, createPlaceOrderTool, evaluateOrderGate, inject, isoWithOffset, klineDateToEpochMs, marketDataKey, minuteKlineTimeToEpochMs, name, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnActiveBuySnapshot, parseCnOrderbook, routeAllows, tencentKlineCapabilities, toCanonicalTencentSymbol, wallTimeToEpochMs };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { INTERVAL_VOCABULARY, TencentRestClient, TradingServiceError, klineDateToEpochMs, minuteKlineTimeToEpochMs, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnOrderbook, toCanonicalTencentSymbol, wallTimeToEpochMs } from "./rest.js";
|
|
1
|
+
import { ACTIVE_BUY_SOURCE, INTERVAL_VOCABULARY, TENCENT_KLINE_MAX_BARS, TencentRestClient, TradingServiceError, isoWithOffset, klineDateToEpochMs, minuteKlineTimeToEpochMs, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnActiveBuySnapshot, parseCnOrderbook, tencentKlineCapabilities, toCanonicalTencentSymbol, wallTimeToEpochMs } from "./rest.js";
|
|
2
2
|
import { liveTradingEnabled } from "@dshtrading/authority";
|
|
3
3
|
import { Service } from "@deepseek-ai/cordis";
|
|
4
4
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
@@ -51,10 +51,20 @@ var TencentMarketDataService = class extends Service {
|
|
|
51
51
|
constructor(ctx, market, options = {}, name = TRADING_CN_MARKET_DATA_KEY) {
|
|
52
52
|
super(ctx, name);
|
|
53
53
|
this.client = new TencentRestClient(market, options);
|
|
54
|
+
if (market === "cn") this.getActiveBuySnapshot = (symbol) => this.client.getActiveBuySnapshot(symbol);
|
|
54
55
|
}
|
|
55
56
|
getTicker(symbol) {
|
|
56
57
|
return this.client.getTicker(symbol);
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* 批量最新报价(MarketDataService 可选契约 2026-10-14):一次上游请求取多只标的,
|
|
61
|
+
* 自选栏「每拍 N 次请求」降成 1 次(cn / hk 实例同能力,均走同一报价行端点)。
|
|
62
|
+
* 键=入参原字符串、未知/字段不足的符号跳过、整批失败抛出、不自行分片——语义见
|
|
63
|
+
* TencentRestClient.getTickers。
|
|
64
|
+
*/
|
|
65
|
+
getTickers(symbols) {
|
|
66
|
+
return this.client.getTickers(symbols);
|
|
67
|
+
}
|
|
58
68
|
/** 基本面快照(同一报价行解析,MarketDataService 可选契约 2026-09-02)。 */
|
|
59
69
|
getFundamentals(symbol) {
|
|
60
70
|
return this.client.getFundamentals(symbol);
|
|
@@ -69,6 +79,13 @@ var TencentMarketDataService = class extends Service {
|
|
|
69
79
|
getKlines(symbol, interval, limit) {
|
|
70
80
|
return this.client.getKlines(symbol, interval, limit);
|
|
71
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* K 线能力声明(api 可选契约,2026-10-15 跨源 K 线路由):透传 REST 客户端的本市场口径
|
|
84
|
+
* (hk 实例不含 5m/30m;深度 = 单请求上限 800)。
|
|
85
|
+
*/
|
|
86
|
+
klineCapabilities() {
|
|
87
|
+
return this.client.klineCapabilities();
|
|
88
|
+
}
|
|
72
89
|
listInstruments(query) {
|
|
73
90
|
return this.client.listInstruments(query);
|
|
74
91
|
}
|
|
@@ -211,8 +228,34 @@ function createPlaceOrderTool(deps) {
|
|
|
211
228
|
}
|
|
212
229
|
});
|
|
213
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* 本连接器在路由层的 provider slug(cn/hk 两个实例共用同一个 slug;
|
|
233
|
+
* docs/guides/exchange-routing.md §2.2)。
|
|
234
|
+
*/
|
|
235
|
+
const ROUTER_PROVIDER = "tencent";
|
|
236
|
+
/**
|
|
237
|
+
* 路由闸门(互斥纪律):preset 平面的同市场连接器行挂在**同一个**
|
|
238
|
+
* `cordis:group + isolate` realm 里(`isolate: { tradingCnMarketData: true }` 的
|
|
239
|
+
* label=true → loader 给该组一个 LocalRealm、组内子行共用,见 cordis-plugin-loader
|
|
240
|
+
* 的 `isolate()`),因此任意时刻只能有一个 provider 真正 provide 市场服务键——每个
|
|
241
|
+
* provider 自己按路由让位。
|
|
242
|
+
*
|
|
243
|
+
* 缺这道闸门时:用户把 cn/hk 路由切到别家(如 eastmoney),腾讯实例仍无条件 provide →
|
|
244
|
+
* 后挂载的那行抛 `service "tradingCnMarketData" has been registered at
|
|
245
|
+
* <dsh-trading-tencent>` → 整个 agent preset 判 invalid,新建会话直接失败
|
|
246
|
+
* (2026-10-14 实测复现)。
|
|
247
|
+
*
|
|
248
|
+
* 无 router 服务的老部署(或手搓 ctx 的单测)→ 放行,保持旧的单 provider 行为;
|
|
249
|
+
* 与 eastmoney/xysz/qmt/tushare/akshare 的 `routeAllows` 同款语义。
|
|
250
|
+
*/
|
|
251
|
+
function routeAllows(ctx, market) {
|
|
252
|
+
const router = ctx.get?.("tradingMarketRouter", false);
|
|
253
|
+
if (router === void 0) return true;
|
|
254
|
+
return router.activeProvider(market) === ROUTER_PROVIDER;
|
|
255
|
+
}
|
|
214
256
|
function apply(ctx, config) {
|
|
215
257
|
const market = config.market;
|
|
258
|
+
if (!routeAllows(ctx, market)) return;
|
|
216
259
|
const key = marketDataKey(market);
|
|
217
260
|
new TencentMarketDataService(ctx, market, {}, key);
|
|
218
261
|
ctx.inject([key], (ctx) => {
|
|
@@ -279,4 +322,4 @@ function apply(ctx, config) {
|
|
|
279
322
|
});
|
|
280
323
|
}
|
|
281
324
|
//#endregion
|
|
282
|
-
export { Config, INTERVAL_VOCABULARY, TRADING_CN_MARKET_DATA_KEY, TRADING_HK_MARKET_DATA_KEY, TencentMarketDataService, TencentRestClient, TradingServiceError, apply, buildDryRunReceipt, createPlaceOrderTool, evaluateOrderGate, inject, klineDateToEpochMs, marketDataKey, minuteKlineTimeToEpochMs, name, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnOrderbook, toCanonicalTencentSymbol, wallTimeToEpochMs };
|
|
325
|
+
export { ACTIVE_BUY_SOURCE, Config, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TENCENT_KLINE_MAX_BARS, TRADING_CN_MARKET_DATA_KEY, TRADING_HK_MARKET_DATA_KEY, TencentMarketDataService, TencentRestClient, TradingServiceError, apply, buildDryRunReceipt, createPlaceOrderTool, evaluateOrderGate, inject, isoWithOffset, klineDateToEpochMs, marketDataKey, minuteKlineTimeToEpochMs, name, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnActiveBuySnapshot, parseCnOrderbook, routeAllows, tencentKlineCapabilities, toCanonicalTencentSymbol, wallTimeToEpochMs };
|
package/lib/rest.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Interval, Kline, Orderbook, StockFundamentals, Ticker, TradingErrorCode } from "@dshtrading/api";
|
|
1
|
+
import { ActiveBuySnapshot, Interval, Kline, KlineCapability, Orderbook, StockFundamentals, Ticker, TradingErrorCode } from "@dshtrading/api";
|
|
2
2
|
//#region src/rest.d.ts
|
|
3
3
|
/** api 包 TradingError 契约的运行时 Error 实现。 */
|
|
4
4
|
declare class TradingServiceError extends Error {
|
|
@@ -33,6 +33,19 @@ interface TencentIntervalDef {
|
|
|
33
33
|
}
|
|
34
34
|
/** 工具 parameters enum 用:受支持 interval 词汇。 */
|
|
35
35
|
declare const INTERVAL_VOCABULARY: readonly string[];
|
|
36
|
+
/**
|
|
37
|
+
* 单请求能承接的根数上限 = 800(2026-10-14 实测,spikes/impl-five-day-1m-probe/EVIDENCE.md §1):
|
|
38
|
+
* mkline 端点 count ≤ 800 正常,**超过即静默降级到 320 根**(不是报错),无游标可翻页。
|
|
39
|
+
* 故 800 是本连接器能诚实承接的 limit 上限(getKlines 也按它 min 裁剪)。
|
|
40
|
+
*/
|
|
41
|
+
declare const TENCENT_KLINE_MAX_BARS = 800;
|
|
42
|
+
/**
|
|
43
|
+
* K 线能力声明(api 可选契约 klineCapabilities):本实例能诚实服务的 (周期, 请求深度)。
|
|
44
|
+
* - 周期集 = INTERVAL_TO_TENCENT 的键;**hk 实例不含 5m/30m**——mkline 端点对港股一律
|
|
45
|
+
* `msg: param error`(实测同上 §1),getKlines 也显式抛 TRADING_UNSUPPORTED_INTERVAL;
|
|
46
|
+
* - 深度 = TENCENT_KLINE_MAX_BARS(上限即截断点,超过就静默降级,不是「多给几根」)。
|
|
47
|
+
*/
|
|
48
|
+
declare function tencentKlineCapabilities(market: TencentMarket): KlineCapability[];
|
|
36
49
|
/**
|
|
37
50
|
* 分钟 K 线时间 `YYYYMMDDHHmm` → epoch ms(Asia/Shanghai 墙钟,12 位紧凑格式)。
|
|
38
51
|
*/
|
|
@@ -87,6 +100,31 @@ type TencentTicker = Ticker & (CnTickerExtra | HkTickerExtra);
|
|
|
87
100
|
declare function parseCnOrderbook(fields: string[], timestamp: number): Orderbook;
|
|
88
101
|
/** 基本面快照 = api 契约形(币种单位由市场决定:cn=CNY,hk=HKD,展示层标注)。 */
|
|
89
102
|
type TencentFundamentals = StockFundamentals;
|
|
103
|
+
/** ActiveBuySnapshot.source 的取值(与 api 契约同一事实,只在这里写一次)。 */
|
|
104
|
+
declare const ACTIVE_BUY_SOURCE = "qt.gtimg.cn";
|
|
105
|
+
/**
|
|
106
|
+
* cn 主动买盘快照解析(字段布局与 parseCnTicker 同一行报价;字段号 2026-10-13 实测
|
|
107
|
+
* spikes/impl-active-buy/r1-raw-sz002714.gbk):
|
|
108
|
+
* 6=成交量=当日总量(**手**) 7=外盘=主动买(手) 8=内盘=主动卖(手)
|
|
109
|
+
* 30=时间 YYYYMMDDHHMMSS(Asia/Shanghai) 47=涨停价 48=跌停价。
|
|
110
|
+
*
|
|
111
|
+
* 口径与失败语义(api ActiveBuySnapshot 契约):
|
|
112
|
+
* - 外盘/内盘是**行情源按盘口推断**的当日累计主动方向,非交易所官方逐笔方向;
|
|
113
|
+
* - 总量 <= 0、外盘/内盘字段缺失(空串也算缺失)、非有限或为负 → 抛
|
|
114
|
+
* TRADING_EXCHANGE_ERROR,**绝不补 0、绝不返回 NaN**(无读数 ≠ 读数 0);
|
|
115
|
+
* - 占比按「÷当日总量」口径;`total - outer - inner` 的残差是**未归向成交**(集合竞价与
|
|
116
|
+
* 价间成交计入总量但不计入外/内盘),实测 2026-10-13 为 002714 12/121808 手、
|
|
117
|
+
* 600519 45/7154 手——不是固定量级,且**不参与修正**(源读数原样返回,见 spike 的
|
|
118
|
+
* sumGap 对照);
|
|
119
|
+
* - 涨跌停价字段缺省(指数/ETF)时整字段省略,不用 0 占位。
|
|
120
|
+
*/
|
|
121
|
+
declare function parseCnActiveBuySnapshot(fields: string[], symbol: string, fetchedAt: string): ActiveBuySnapshot;
|
|
122
|
+
/**
|
|
123
|
+
* epoch ms → ISO 8601 **带偏移**(该时区的墙钟,形如 2026-10-13T09:53:12+08:00)。
|
|
124
|
+
* 用 Intl 求时区偏移(含夏令时,与 wallTimeToEpochMs 同一套机制);不用
|
|
125
|
+
* `Date#toISOString`——它是 UTC `Z` 形,读不出「本地」,且会让同一读数随宿主时区漂移。
|
|
126
|
+
*/
|
|
127
|
+
declare function isoWithOffset(epochMs: number, timeZone: string): string;
|
|
90
128
|
interface TencentRestOptions {
|
|
91
129
|
/** 覆盖报价 base(测试/反代用),末尾不带斜杠。 */
|
|
92
130
|
readonly quoteBaseUrl?: string;
|
|
@@ -100,6 +138,8 @@ interface TencentRestOptions {
|
|
|
100
138
|
readonly timeoutMs?: number;
|
|
101
139
|
/** 注入 fetch 实现;缺省用全局 fetch(Node 22+ 内置)。 */
|
|
102
140
|
readonly fetchImpl?: typeof fetch;
|
|
141
|
+
/** 时钟注入(单测固定 active-buy 快照的 fetchedAt 用;缺省 Date.now)。 */
|
|
142
|
+
readonly now?: () => number;
|
|
103
143
|
}
|
|
104
144
|
declare class TencentRestClient {
|
|
105
145
|
#private;
|
|
@@ -109,6 +149,19 @@ declare class TencentRestClient {
|
|
|
109
149
|
* 最新行情快照(同一报价行含基本面字段,getFundamentals 零额外请求成本地解析)。
|
|
110
150
|
*/
|
|
111
151
|
getTicker(symbol: string): Promise<TencentTicker>;
|
|
152
|
+
/**
|
|
153
|
+
* 批量最新报价(api 可选契约 getTickers,2026-10-14):自选栏「每拍 N 只标的 N 次请求」
|
|
154
|
+
* → **一次请求**(`q=` 原生支持逗号列表,实证见 #fetchQuoteRows)。**实现不分片**——
|
|
155
|
+
* 单次请求的标的数由调用方按 MAX_SYMBOLS 裁剪。
|
|
156
|
+
*
|
|
157
|
+
* 契约语义:
|
|
158
|
+
* - 键 = **入参原字符串**(调用方按它一一对应),值 Ticker.symbol 仍是市场规范形;
|
|
159
|
+
* - 只返回**确实拿到**的标的:入参不可规范化、上游整行缺席、行字段不足、行内时间读数
|
|
160
|
+
* 坏掉——一律跳过该符号(调用方回落 getTicker 拿它自己的错误码),绝不凑数造假行;
|
|
161
|
+
* - 整批 HTTP/网络失败 → 抛出(不做 N 次重试);
|
|
162
|
+
* - 空入参(或全部入参都不可规范化)→ `{}` 且**不发请求**。
|
|
163
|
+
*/
|
|
164
|
+
getTickers(symbols: string[]): Promise<Record<string, TencentTicker>>;
|
|
112
165
|
/**
|
|
113
166
|
* 基本面与估值快照:与 getTicker 同一行报价(字段布局见 parseCnFundamentals /
|
|
114
167
|
* parseHkFundamentals 注释),无额外端点。估值字段缺省(指数/ETF)时整字段省略。
|
|
@@ -121,6 +174,16 @@ declare class TencentRestClient {
|
|
|
121
174
|
* TRADING_NOT_IMPLEMENTED 上报,桥层转 ok:false(前端显示「未提供」而非空盘口)。
|
|
122
175
|
*/
|
|
123
176
|
getOrderbook(symbol: string): Promise<Orderbook>;
|
|
177
|
+
/**
|
|
178
|
+
* 当日累计主动买盘快照(api 可选契约 getActiveBuySnapshot,2026-10-13):与 getTicker
|
|
179
|
+
* 同一行报价的外盘/内盘字段(cn 布局 6/7/8/30/47/48,见 parseCnActiveBuySnapshot 注释),
|
|
180
|
+
* 每次调用仍独立发起一次报价请求,不复用 getTicker 的应答。**hk 结构性不支持**——r_hk 行的 7/8 位恒为 0(2026-10-13 实测
|
|
181
|
+
* spikes/impl-active-buy/r2-raw-hk00700.gbk),直接按 TRADING_NOT_IMPLEMENTED 上报;
|
|
182
|
+
* 服务层 hk 实例上该方法本身缺席(能力自然缺席,消费方按「方法是否存在」判降级)。
|
|
183
|
+
*/
|
|
184
|
+
getActiveBuySnapshot(symbol: string): Promise<ActiveBuySnapshot>;
|
|
185
|
+
/** K 线能力声明(api 可选契约):本实例市场的诚实 (周期, 请求深度) 清单,口径见 tencentKlineCapabilities。 */
|
|
186
|
+
klineCapabilities(): readonly KlineCapability[];
|
|
124
187
|
/**
|
|
125
188
|
* K 线(日/周/月走 fqkline 前权 qfq;5m/30m 分钟线走 mkline 端点)。
|
|
126
189
|
* 港股 mkline 端点不支持分钟线,请求 5m/30m 会抛出 TRADING_UNSUPPORTED_INTERVAL。
|
|
@@ -137,4 +200,4 @@ declare class TencentRestClient {
|
|
|
137
200
|
}>>;
|
|
138
201
|
}
|
|
139
202
|
//#endregion
|
|
140
|
-
export { CnTickerExtra, HkTickerExtra, INTERVAL_VOCABULARY, TencentFundamentals, TencentIntervalDef, TencentKlineType, TencentMarket, TencentRestClient, TencentRestOptions, TencentTicker, TradingServiceError, klineDateToEpochMs, minuteKlineTimeToEpochMs, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnOrderbook, toCanonicalTencentSymbol, wallTimeToEpochMs };
|
|
203
|
+
export { ACTIVE_BUY_SOURCE, CnTickerExtra, HkTickerExtra, INTERVAL_VOCABULARY, TENCENT_KLINE_MAX_BARS, TencentFundamentals, TencentIntervalDef, TencentKlineType, TencentMarket, TencentRestClient, TencentRestOptions, TencentTicker, TradingServiceError, isoWithOffset, klineDateToEpochMs, minuteKlineTimeToEpochMs, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnActiveBuySnapshot, parseCnOrderbook, tencentKlineCapabilities, toCanonicalTencentSymbol, wallTimeToEpochMs };
|
package/lib/rest.js
CHANGED
|
@@ -95,6 +95,24 @@ const INTERVAL_TO_TENCENT = /* @__PURE__ */ new Map([
|
|
|
95
95
|
/** 工具 parameters enum 用:受支持 interval 词汇。 */
|
|
96
96
|
const INTERVAL_VOCABULARY = [...INTERVAL_TO_TENCENT.keys()];
|
|
97
97
|
/**
|
|
98
|
+
* 单请求能承接的根数上限 = 800(2026-10-14 实测,spikes/impl-five-day-1m-probe/EVIDENCE.md §1):
|
|
99
|
+
* mkline 端点 count ≤ 800 正常,**超过即静默降级到 320 根**(不是报错),无游标可翻页。
|
|
100
|
+
* 故 800 是本连接器能诚实承接的 limit 上限(getKlines 也按它 min 裁剪)。
|
|
101
|
+
*/
|
|
102
|
+
const TENCENT_KLINE_MAX_BARS = 800;
|
|
103
|
+
/**
|
|
104
|
+
* K 线能力声明(api 可选契约 klineCapabilities):本实例能诚实服务的 (周期, 请求深度)。
|
|
105
|
+
* - 周期集 = INTERVAL_TO_TENCENT 的键;**hk 实例不含 5m/30m**——mkline 端点对港股一律
|
|
106
|
+
* `msg: param error`(实测同上 §1),getKlines 也显式抛 TRADING_UNSUPPORTED_INTERVAL;
|
|
107
|
+
* - 深度 = TENCENT_KLINE_MAX_BARS(上限即截断点,超过就静默降级,不是「多给几根」)。
|
|
108
|
+
*/
|
|
109
|
+
function tencentKlineCapabilities(market) {
|
|
110
|
+
return [...INTERVAL_TO_TENCENT].filter(([, def]) => !(def.type === "mkline" && market === "hk")).map(([interval]) => ({
|
|
111
|
+
interval,
|
|
112
|
+
maxBars: 800
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
98
116
|
* 分钟 K 线时间 `YYYYMMDDHHmm` → epoch ms(Asia/Shanghai 墙钟,12 位紧凑格式)。
|
|
99
117
|
*/
|
|
100
118
|
function minuteKlineTimeToEpochMs(value, timeZone = "Asia/Shanghai") {
|
|
@@ -305,6 +323,119 @@ function parseHkFundamentals(fields, timestamp) {
|
|
|
305
323
|
timestamp
|
|
306
324
|
};
|
|
307
325
|
}
|
|
326
|
+
/** ActiveBuySnapshot.source 的取值(与 api 契约同一事实,只在这里写一次)。 */
|
|
327
|
+
const ACTIVE_BUY_SOURCE = "qt.gtimg.cn";
|
|
328
|
+
/**
|
|
329
|
+
* 行的严格数值读数:空串/纯空白视为**缺失**。
|
|
330
|
+
* `Number('') === 0`——用宽松读数会把「字段缺席」伪装成 0 读数,而「主动买/卖为 0」
|
|
331
|
+
* 本身是有效事实,两者不能混同(本文件此前的 num() 保留给宽容字段用)。
|
|
332
|
+
*/
|
|
333
|
+
function strictNum(value) {
|
|
334
|
+
if (value === void 0) return void 0;
|
|
335
|
+
const trimmed = value.trim();
|
|
336
|
+
if (!trimmed) return void 0;
|
|
337
|
+
const n = Number(trimmed);
|
|
338
|
+
return Number.isFinite(n) ? n : void 0;
|
|
339
|
+
}
|
|
340
|
+
/** 占比归一:保留 2 位小数(-0 归一为 0,避免 wire 上出现 `-0`)。 */
|
|
341
|
+
function round2(value) {
|
|
342
|
+
const r = Math.round(value * 100) / 100;
|
|
343
|
+
return r === 0 ? 0 : r;
|
|
344
|
+
}
|
|
345
|
+
/** 源字段 30(`YYYYMMDDHHMMSS`,交易所本地墙钟)→ 契约的 date/time 两个字符串。 */
|
|
346
|
+
function splitQuoteTime(value, symbol) {
|
|
347
|
+
const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(value ?? "");
|
|
348
|
+
if (!m) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Tencent CN active-buy for ${symbol}: invalid quote time field ${JSON.stringify(value ?? null)}`);
|
|
349
|
+
return {
|
|
350
|
+
date: `${m[1]}-${m[2]}-${m[3]}`,
|
|
351
|
+
time: `${m[4]}:${m[5]}:${m[6]}`
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* cn 主动买盘快照解析(字段布局与 parseCnTicker 同一行报价;字段号 2026-10-13 实测
|
|
356
|
+
* spikes/impl-active-buy/r1-raw-sz002714.gbk):
|
|
357
|
+
* 6=成交量=当日总量(**手**) 7=外盘=主动买(手) 8=内盘=主动卖(手)
|
|
358
|
+
* 30=时间 YYYYMMDDHHMMSS(Asia/Shanghai) 47=涨停价 48=跌停价。
|
|
359
|
+
*
|
|
360
|
+
* 口径与失败语义(api ActiveBuySnapshot 契约):
|
|
361
|
+
* - 外盘/内盘是**行情源按盘口推断**的当日累计主动方向,非交易所官方逐笔方向;
|
|
362
|
+
* - 总量 <= 0、外盘/内盘字段缺失(空串也算缺失)、非有限或为负 → 抛
|
|
363
|
+
* TRADING_EXCHANGE_ERROR,**绝不补 0、绝不返回 NaN**(无读数 ≠ 读数 0);
|
|
364
|
+
* - 占比按「÷当日总量」口径;`total - outer - inner` 的残差是**未归向成交**(集合竞价与
|
|
365
|
+
* 价间成交计入总量但不计入外/内盘),实测 2026-10-13 为 002714 12/121808 手、
|
|
366
|
+
* 600519 45/7154 手——不是固定量级,且**不参与修正**(源读数原样返回,见 spike 的
|
|
367
|
+
* sumGap 对照);
|
|
368
|
+
* - 涨跌停价字段缺省(指数/ETF)时整字段省略,不用 0 占位。
|
|
369
|
+
*/
|
|
370
|
+
function parseCnActiveBuySnapshot(fields, symbol, fetchedAt) {
|
|
371
|
+
const totalLots = strictNum(fields[6]);
|
|
372
|
+
if (totalLots === void 0 || totalLots <= 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Tencent CN active-buy for ${symbol}: missing/invalid total volume field 6 ${JSON.stringify(fields[6] ?? null)}`);
|
|
373
|
+
const outerLots = strictNum(fields[7]);
|
|
374
|
+
const innerLots = strictNum(fields[8]);
|
|
375
|
+
if (outerLots === void 0 || innerLots === void 0 || outerLots < 0 || innerLots < 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Tencent CN active-buy for ${symbol}: missing/invalid outer/inner volume fields 7/8 ${JSON.stringify(fields[7] ?? null)}/${JSON.stringify(fields[8] ?? null)}`);
|
|
376
|
+
const { date, time } = splitQuoteTime(fields[30], symbol);
|
|
377
|
+
const limitUp = strictNum(fields[47]);
|
|
378
|
+
const limitDown = strictNum(fields[48]);
|
|
379
|
+
return {
|
|
380
|
+
symbol,
|
|
381
|
+
date,
|
|
382
|
+
time,
|
|
383
|
+
totalLots,
|
|
384
|
+
outerLots,
|
|
385
|
+
innerLots,
|
|
386
|
+
buySharePct: round2(outerLots / totalLots * 100),
|
|
387
|
+
netSharePct: round2((outerLots - innerLots) / totalLots * 100),
|
|
388
|
+
...limitUp !== void 0 ? { limitUp } : {},
|
|
389
|
+
...limitDown !== void 0 ? { limitDown } : {},
|
|
390
|
+
source: ACTIVE_BUY_SOURCE,
|
|
391
|
+
fetchedAt
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* epoch ms → ISO 8601 **带偏移**(该时区的墙钟,形如 2026-10-13T09:53:12+08:00)。
|
|
396
|
+
* 用 Intl 求时区偏移(含夏令时,与 wallTimeToEpochMs 同一套机制);不用
|
|
397
|
+
* `Date#toISOString`——它是 UTC `Z` 形,读不出「本地」,且会让同一读数随宿主时区漂移。
|
|
398
|
+
*/
|
|
399
|
+
function isoWithOffset(epochMs, timeZone) {
|
|
400
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
401
|
+
timeZone,
|
|
402
|
+
year: "numeric",
|
|
403
|
+
month: "2-digit",
|
|
404
|
+
day: "2-digit",
|
|
405
|
+
hour: "2-digit",
|
|
406
|
+
minute: "2-digit",
|
|
407
|
+
second: "2-digit",
|
|
408
|
+
hour12: false
|
|
409
|
+
}).formatToParts(new Date(epochMs));
|
|
410
|
+
const get = (type) => parts.find((p) => p.type === type)?.value ?? "00";
|
|
411
|
+
const hour = String(Number(get("hour")) % 24).padStart(2, "0");
|
|
412
|
+
const wall = Date.UTC(Number(get("year")), Number(get("month")) - 1, Number(get("day")), Number(hour), Number(get("minute")), Number(get("second")));
|
|
413
|
+
const offsetMinutes = Math.round((wall - epochMs) / 6e4);
|
|
414
|
+
const abs = Math.abs(offsetMinutes);
|
|
415
|
+
const offset = `${offsetMinutes < 0 ? "-" : "+"}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`;
|
|
416
|
+
return `${get("year")}-${get("month")}-${get("day")}T${hour}:${get("minute")}:${get("second")}${offset}`;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* 整段报价响应文本(**已 GBK 解码**)→ wire(小写归一)→字段数组。
|
|
420
|
+
*
|
|
421
|
+
* 每行形如 `v_<wire>="字段~字段~…";\n`——wire 原样可能是 `sh600519` / `r_hk00700` /
|
|
422
|
+
* `r_hkHSI`。**键统一小写归一**:hk 指数 wire 大小写敏感(`r_hkHSI` 有行、`r_hkhsi` 返回
|
|
423
|
+
* `v_pv_none_match="1";`,2026-10-14 实测),请求侧必须原样带大小写,只有查表键归一化;
|
|
424
|
+
* 规范形由调用方持有的 sym 还原,不用响应键(否则指数会变成 `hsi.HK`)。字段切分与单标的
|
|
425
|
+
* 路径同源,批量不另写解析器。
|
|
426
|
+
*
|
|
427
|
+
* 未知代码的两种上游形态都在调用侧处理:单标的短 body `v_pv_none="1"`(键也匹配不上)、
|
|
428
|
+
* 批量响应里**整行缺席**(2026-10-14 实证 `q=sh600519,sz000001,sz159869,sz999999` 只回
|
|
429
|
+
* 3 行,HTTP 200)。
|
|
430
|
+
*/
|
|
431
|
+
function parseQuoteRows(text) {
|
|
432
|
+
const rows = /* @__PURE__ */ new Map();
|
|
433
|
+
for (const m of text.matchAll(/v_([A-Za-z0-9_^]+)="([^"]*)"/g)) {
|
|
434
|
+
const wire = (m[1] ?? "").toLowerCase();
|
|
435
|
+
if (!rows.has(wire)) rows.set(wire, (m[2] ?? "").split("~"));
|
|
436
|
+
}
|
|
437
|
+
return rows;
|
|
438
|
+
}
|
|
308
439
|
const DEFAULT_QUOTE_BASE_URL = "https://qt.gtimg.cn";
|
|
309
440
|
const DEFAULT_KLINE_BASE_URL = "https://web.ifzq.gtimg.cn";
|
|
310
441
|
const DEFAULT_MKLINE_BASE_URL = "https://ifzq.gtimg.cn";
|
|
@@ -318,6 +449,7 @@ var TencentRestClient = class {
|
|
|
318
449
|
#searchBaseUrl;
|
|
319
450
|
#timeoutMs;
|
|
320
451
|
#fetchImpl;
|
|
452
|
+
#now;
|
|
321
453
|
constructor(market, options = {}) {
|
|
322
454
|
this.#market = market;
|
|
323
455
|
this.#quoteBaseUrl = options.quoteBaseUrl ?? DEFAULT_QUOTE_BASE_URL;
|
|
@@ -326,6 +458,7 @@ var TencentRestClient = class {
|
|
|
326
458
|
this.#searchBaseUrl = options.searchBaseUrl ?? DEFAULT_SEARCH_BASE_URL;
|
|
327
459
|
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
328
460
|
this.#fetchImpl = options.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
461
|
+
this.#now = options.now ?? (() => Date.now());
|
|
329
462
|
}
|
|
330
463
|
get market() {
|
|
331
464
|
return this.#market;
|
|
@@ -345,7 +478,11 @@ var TencentRestClient = class {
|
|
|
345
478
|
if (!res.ok) throw new TradingServiceError(res.status === 429 ? "TRADING_RATE_LIMITED" : "TRADING_EXCHANGE_ERROR", `Tencent ${url}: HTTP ${res.status} ${res.statusText}`);
|
|
346
479
|
return new Uint8Array(await res.arrayBuffer());
|
|
347
480
|
}
|
|
348
|
-
/**
|
|
481
|
+
/**
|
|
482
|
+
* 报价 wire 形态:cn=sh600519;hk=r_hk00700(hk K 线另用 hk 前缀,见文件头注)。
|
|
483
|
+
* **大小写原样进 URL**:hk 指数 wire 大小写敏感(`r_hkHSI` 有行、`r_hkhsi` 返回
|
|
484
|
+
* `v_pv_none_match="1";`,2026-10-14 实测),只有响应查表键才小写归一。
|
|
485
|
+
*/
|
|
349
486
|
#quoteWireCode(symbol) {
|
|
350
487
|
return this.#market === "hk" ? `r_hk${symbol}` : symbol;
|
|
351
488
|
}
|
|
@@ -354,22 +491,47 @@ var TencentRestClient = class {
|
|
|
354
491
|
return this.#market === "hk" ? `hk${symbol}` : symbol;
|
|
355
492
|
}
|
|
356
493
|
/**
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
494
|
+
* 报价批量取数:一次 `GET /q=<wire,wire,…>`(腾讯端点原生支持逗号列表,2026-10-14 实证
|
|
495
|
+
* 40 码单请求 HTTP 200 / 17954 B / 180 ms)→ wire(小写)→字段数组。wires 原样拼 URL
|
|
496
|
+
* (hk 指数大小写敏感,见 #quoteWireCode);整批失败(网络/超时/HTTP 非 2xx)在
|
|
497
|
+
* #requestArrayBuffer 里抛出,不在这里吞。
|
|
360
498
|
*/
|
|
361
|
-
async #
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
499
|
+
async #fetchQuoteRows(wires) {
|
|
500
|
+
const url = `${this.#quoteBaseUrl}/q=${wires.join(",")}`;
|
|
501
|
+
return parseQuoteRows(new TextDecoder("gbk").decode(await this.#requestArrayBuffer(url)));
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* 单行校验(getTicker 与 getTickers **共用**):字段数 < 35 = 未知/退市代码的短行
|
|
505
|
+
* (`v_pv_none="1"` 之类);字段 30 是行情时间(cn=Asia/Shanghai,hk=Asia/Hong_Kong)。
|
|
506
|
+
*/
|
|
507
|
+
#validateQuoteRow(fields, sym) {
|
|
369
508
|
if (fields.length < 35) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `Tencent ticker for ${sym}: payload has ${fields.length} fields, expected >= 35 (unknown/delisted symbol?)`);
|
|
370
509
|
return {
|
|
371
510
|
fields,
|
|
372
|
-
timestamp: this.#market === "hk" ? wallTimeToEpochMs(fields[30] ?? "", "Asia/Hong_Kong") : wallTimeToEpochMs(fields[30] ?? "", "Asia/Shanghai")
|
|
511
|
+
timestamp: this.#market === "hk" ? wallTimeToEpochMs(fields[30] ?? "", "Asia/Hong_Kong") : wallTimeToEpochMs(fields[30] ?? "", "Asia/Shanghai")
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
/** 已校验的行 → 契约快照(getTicker 与 getTickers 共用同一解析与规范形出口)。 */
|
|
515
|
+
#tickerFrom(fields, timestamp, sym) {
|
|
516
|
+
return {
|
|
517
|
+
...this.#market === "hk" ? parseHkTicker(fields, timestamp) : parseCnTicker(fields, timestamp),
|
|
518
|
+
symbol: toCanonicalTencentSymbol(this.#market, sym)
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* 单标的报价行取数(getTicker/getFundamentals/getOrderbook/getActiveBuySnapshot 共用):
|
|
523
|
+
* **GBK 解码**(响应 charset=GBK,UTF-8 直接乱码);未知代码返回 `v_pv_none="1"` 之类
|
|
524
|
+
* 短 body(行键匹配不上),按 TRADING_UNSUPPORTED_SYMBOL 上报。返回 `~` 分隔字段数组 +
|
|
525
|
+
* 行情时间戳 + 规范化符号。
|
|
526
|
+
*/
|
|
527
|
+
async #fetchQuoteFields(symbol) {
|
|
528
|
+
const sym = normalizeSymbol(this.#market, symbol);
|
|
529
|
+
const wire = this.#quoteWireCode(sym);
|
|
530
|
+
const rows = await this.#fetchQuoteRows([wire]);
|
|
531
|
+
const fields = rows.get(wire.toLowerCase());
|
|
532
|
+
if (fields === void 0) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `Tencent ticker for ${sym}: no row for wire ${wire} (${rows.size} quote rows in payload, unknown symbol?)`);
|
|
533
|
+
return {
|
|
534
|
+
...this.#validateQuoteRow(fields, sym),
|
|
373
535
|
sym
|
|
374
536
|
};
|
|
375
537
|
}
|
|
@@ -378,10 +540,56 @@ var TencentRestClient = class {
|
|
|
378
540
|
*/
|
|
379
541
|
async getTicker(symbol) {
|
|
380
542
|
const { fields, timestamp, sym } = await this.#fetchQuoteFields(symbol);
|
|
381
|
-
return
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
543
|
+
return this.#tickerFrom(fields, timestamp, sym);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* 批量最新报价(api 可选契约 getTickers,2026-10-14):自选栏「每拍 N 只标的 N 次请求」
|
|
547
|
+
* → **一次请求**(`q=` 原生支持逗号列表,实证见 #fetchQuoteRows)。**实现不分片**——
|
|
548
|
+
* 单次请求的标的数由调用方按 MAX_SYMBOLS 裁剪。
|
|
549
|
+
*
|
|
550
|
+
* 契约语义:
|
|
551
|
+
* - 键 = **入参原字符串**(调用方按它一一对应),值 Ticker.symbol 仍是市场规范形;
|
|
552
|
+
* - 只返回**确实拿到**的标的:入参不可规范化、上游整行缺席、行字段不足、行内时间读数
|
|
553
|
+
* 坏掉——一律跳过该符号(调用方回落 getTicker 拿它自己的错误码),绝不凑数造假行;
|
|
554
|
+
* - 整批 HTTP/网络失败 → 抛出(不做 N 次重试);
|
|
555
|
+
* - 空入参(或全部入参都不可规范化)→ `{}` 且**不发请求**。
|
|
556
|
+
*/
|
|
557
|
+
async getTickers(symbols) {
|
|
558
|
+
const tickers = {};
|
|
559
|
+
if (symbols.length === 0) return tickers;
|
|
560
|
+
const byWire = /* @__PURE__ */ new Map();
|
|
561
|
+
for (const symbol of symbols) {
|
|
562
|
+
let sym;
|
|
563
|
+
try {
|
|
564
|
+
sym = normalizeSymbol(this.#market, symbol);
|
|
565
|
+
} catch {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const wire = this.#quoteWireCode(sym);
|
|
569
|
+
const key = wire.toLowerCase();
|
|
570
|
+
const group = byWire.get(key);
|
|
571
|
+
if (group === void 0) byWire.set(key, {
|
|
572
|
+
wire,
|
|
573
|
+
sym,
|
|
574
|
+
inputs: [symbol]
|
|
575
|
+
});
|
|
576
|
+
else if (!group.inputs.includes(symbol)) group.inputs.push(symbol);
|
|
577
|
+
}
|
|
578
|
+
if (byWire.size === 0) return tickers;
|
|
579
|
+
const rows = await this.#fetchQuoteRows([...byWire.values()].map((group) => group.wire));
|
|
580
|
+
for (const [key, group] of byWire) {
|
|
581
|
+
const fields = rows.get(key);
|
|
582
|
+
if (fields === void 0) continue;
|
|
583
|
+
let ticker;
|
|
584
|
+
try {
|
|
585
|
+
const row = this.#validateQuoteRow(fields, group.sym);
|
|
586
|
+
ticker = this.#tickerFrom(row.fields, row.timestamp, group.sym);
|
|
587
|
+
} catch {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
for (const input of group.inputs) tickers[input] = ticker;
|
|
591
|
+
}
|
|
592
|
+
return tickers;
|
|
385
593
|
}
|
|
386
594
|
/**
|
|
387
595
|
* 基本面与估值快照:与 getTicker 同一行报价(字段布局见 parseCnFundamentals /
|
|
@@ -409,6 +617,22 @@ var TencentRestClient = class {
|
|
|
409
617
|
};
|
|
410
618
|
}
|
|
411
619
|
/**
|
|
620
|
+
* 当日累计主动买盘快照(api 可选契约 getActiveBuySnapshot,2026-10-13):与 getTicker
|
|
621
|
+
* 同一行报价的外盘/内盘字段(cn 布局 6/7/8/30/47/48,见 parseCnActiveBuySnapshot 注释),
|
|
622
|
+
* 每次调用仍独立发起一次报价请求,不复用 getTicker 的应答。**hk 结构性不支持**——r_hk 行的 7/8 位恒为 0(2026-10-13 实测
|
|
623
|
+
* spikes/impl-active-buy/r2-raw-hk00700.gbk),直接按 TRADING_NOT_IMPLEMENTED 上报;
|
|
624
|
+
* 服务层 hk 实例上该方法本身缺席(能力自然缺席,消费方按「方法是否存在」判降级)。
|
|
625
|
+
*/
|
|
626
|
+
async getActiveBuySnapshot(symbol) {
|
|
627
|
+
if (this.#market === "hk") throw new TradingServiceError("TRADING_NOT_IMPLEMENTED", "Tencent HK active-buy: r_hk quote row carries no outer/inner (active buy/sell) fields (7/8 always 0) — unavailable for hk");
|
|
628
|
+
const { fields, sym } = await this.#fetchQuoteFields(symbol);
|
|
629
|
+
return parseCnActiveBuySnapshot(fields, toCanonicalTencentSymbol("cn", sym), isoWithOffset(this.#now(), "Asia/Shanghai"));
|
|
630
|
+
}
|
|
631
|
+
/** K 线能力声明(api 可选契约):本实例市场的诚实 (周期, 请求深度) 清单,口径见 tencentKlineCapabilities。 */
|
|
632
|
+
klineCapabilities() {
|
|
633
|
+
return tencentKlineCapabilities(this.#market);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
412
636
|
* K 线(日/周/月走 fqkline 前权 qfq;5m/30m 分钟线走 mkline 端点)。
|
|
413
637
|
* 港股 mkline 端点不支持分钟线,请求 5m/30m 会抛出 TRADING_UNSUPPORTED_INTERVAL。
|
|
414
638
|
*/
|
|
@@ -504,4 +728,4 @@ var TencentRestClient = class {
|
|
|
504
728
|
}
|
|
505
729
|
};
|
|
506
730
|
//#endregion
|
|
507
|
-
export { INTERVAL_VOCABULARY, TencentRestClient, TradingServiceError, klineDateToEpochMs, minuteKlineTimeToEpochMs, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnOrderbook, toCanonicalTencentSymbol, wallTimeToEpochMs };
|
|
731
|
+
export { ACTIVE_BUY_SOURCE, INTERVAL_VOCABULARY, TENCENT_KLINE_MAX_BARS, TencentRestClient, TradingServiceError, isoWithOffset, klineDateToEpochMs, minuteKlineTimeToEpochMs, normalizeCnSymbol, normalizeHkSymbol, normalizeSymbol, parseCnActiveBuySnapshot, parseCnOrderbook, tencentKlineCapabilities, toCanonicalTencentSymbol, wallTimeToEpochMs };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshtrading/connector-tencent",
|
|
3
3
|
"description": "CN + HK dual-market connector plugin for dsh-trading: public market data from Tencent quote/kline endpoints implementing the @dshtrading/api contracts (single package, per-instance market split; no key, no official authorization — personal use boundary at your own discretion)",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
],
|
|
22
22
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@dshtrading/authority": "^0.6.
|
|
25
|
-
"@dshtrading/api": "^0.6.
|
|
24
|
+
"@dshtrading/authority": "^0.6.2",
|
|
25
|
+
"@dshtrading/api": "^0.6.2"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"@deepseek-ai/cordis": ">=4.0.0",
|
|
29
|
-
"@deepseek-ai/dsh-tools": ">=0.2.
|
|
29
|
+
"@deepseek-ai/dsh-tools": ">=0.2.1-rc.2",
|
|
30
30
|
"@deepseek-ai/schemastery": ">=3.18.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|