@dshtrading/connector-eastmoney 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dataplane.js +7 -6
- package/lib/index.d.ts +7 -3
- package/lib/index.js +19 -11
- package/lib/rest.d.ts +32 -6
- package/lib/rest.js +108 -31
- package/package.json +2 -2
package/lib/dataplane.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EastmoneyMarketDataService,
|
|
1
|
+
import { EastmoneyMarketDataService, marketDataKey } from "./index.js";
|
|
2
2
|
//#region src/dataplane.ts
|
|
3
3
|
const inject = [];
|
|
4
4
|
function resolveMarketDataRegistry(ctx) {
|
|
@@ -6,17 +6,18 @@ function resolveMarketDataRegistry(ctx) {
|
|
|
6
6
|
return candidate !== void 0 ? candidate : void 0;
|
|
7
7
|
}
|
|
8
8
|
const ROUTER_PROVIDER = "eastmoney";
|
|
9
|
-
const MARKET = "cn";
|
|
10
9
|
function apply(ctx, config) {
|
|
11
10
|
if (!config.enabled) return;
|
|
11
|
+
const market = config.market ?? "cn";
|
|
12
|
+
const key = marketDataKey(market);
|
|
12
13
|
const registry = resolveMarketDataRegistry(ctx);
|
|
13
14
|
if (registry === void 0) {
|
|
14
|
-
new EastmoneyMarketDataService(ctx);
|
|
15
|
+
new EastmoneyMarketDataService(ctx, { market }, key);
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
|
-
const inner = ctx.isolate(
|
|
18
|
-
const service = new EastmoneyMarketDataService(inner);
|
|
19
|
-
ctx.effect(() => registry.register(
|
|
18
|
+
const inner = ctx.isolate(key);
|
|
19
|
+
const service = new EastmoneyMarketDataService(inner, { market }, key);
|
|
20
|
+
ctx.effect(() => registry.register(market, ROUTER_PROVIDER, service));
|
|
20
21
|
}
|
|
21
22
|
//#endregion
|
|
22
23
|
export { apply, inject };
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EastmoneyRestClient, EastmoneyRestOptions, INTERVAL_VOCABULARY, Interval, Kline, Ticker, TradingServiceError, mapIntervalToKlt, parseIntervalMs, toEastmoneySecid } from "./rest.js";
|
|
1
|
+
import { EastmoneyMarket, EastmoneyRestClient, EastmoneyRestOptions, INTERVAL_VOCABULARY, Interval, Kline, Ticker, TradingServiceError, eastmoneyPriceScale, mapIntervalToKlt, parseIntervalMs, toEastmoneySecid, utc8WallTimeToEpochMs } from "./rest.js";
|
|
2
2
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
3
3
|
import Schema from "@deepseek-ai/schemastery";
|
|
4
4
|
import { Disposable, Interval as Interval$1, Kline as Kline$1, MarketDataService, Ticker as Ticker$1 } from "@dshtrading/api";
|
|
@@ -6,9 +6,13 @@ import { Disposable, Interval as Interval$1, Kline as Kline$1, MarketDataService
|
|
|
6
6
|
declare const name = "dsh-trading-cn-connector-eastmoney";
|
|
7
7
|
interface Config {
|
|
8
8
|
enabled: boolean;
|
|
9
|
+
/** 市场分流(单包双市场,照抄 connector-tencent 模式):cn=A 股(缺省),hk=港股(trends2 分钟线实证)。 */
|
|
10
|
+
market?: 'cn' | 'hk';
|
|
9
11
|
}
|
|
10
12
|
declare const Config: Schema<Config>;
|
|
11
13
|
declare const TRADING_CN_MARKET_DATA_KEY = "tradingCnMarketData";
|
|
14
|
+
declare const TRADING_HK_MARKET_DATA_KEY = "tradingHkMarketData";
|
|
15
|
+
declare function marketDataKey(market: 'cn' | 'hk'): string;
|
|
12
16
|
declare class EastmoneyMarketDataService extends Service implements MarketDataService {
|
|
13
17
|
private readonly client;
|
|
14
18
|
constructor(ctx: Context, options?: EastmoneyRestOptions, serviceName?: string);
|
|
@@ -23,7 +27,7 @@ declare class EastmoneyMarketDataService extends Service implements MarketDataSe
|
|
|
23
27
|
}): Disposable;
|
|
24
28
|
}
|
|
25
29
|
declare const ROUTER_PROVIDER = "eastmoney";
|
|
26
|
-
declare function routeAllows(ctx: Context, config: Config, market:
|
|
30
|
+
declare function routeAllows(ctx: Context, config: Config, market: 'cn' | 'hk'): boolean;
|
|
27
31
|
declare function apply(ctx: Context, config: Config): void;
|
|
28
32
|
//#endregion
|
|
29
|
-
export { Config, EastmoneyMarketDataService, EastmoneyRestClient, EastmoneyRestOptions, INTERVAL_VOCABULARY, type Interval, type Kline, ROUTER_PROVIDER, TRADING_CN_MARKET_DATA_KEY, type Ticker, TradingServiceError, apply, mapIntervalToKlt, name, parseIntervalMs, routeAllows, toEastmoneySecid };
|
|
33
|
+
export { Config, EastmoneyMarket, EastmoneyMarketDataService, EastmoneyRestClient, EastmoneyRestOptions, INTERVAL_VOCABULARY, type Interval, type Kline, ROUTER_PROVIDER, TRADING_CN_MARKET_DATA_KEY, TRADING_HK_MARKET_DATA_KEY, type Ticker, TradingServiceError, apply, eastmoneyPriceScale, mapIntervalToKlt, marketDataKey, name, parseIntervalMs, routeAllows, toEastmoneySecid, utc8WallTimeToEpochMs };
|
package/lib/index.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { EastmoneyRestClient, INTERVAL_VOCABULARY, TradingServiceError, mapIntervalToKlt, parseIntervalMs, toEastmoneySecid } from "./rest.js";
|
|
1
|
+
import { EastmoneyRestClient, INTERVAL_VOCABULARY, TradingServiceError, eastmoneyPriceScale, mapIntervalToKlt, parseIntervalMs, toEastmoneySecid, utc8WallTimeToEpochMs } from "./rest.js";
|
|
2
2
|
import { Service } from "@deepseek-ai/cordis";
|
|
3
3
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
4
|
import Schema from "@deepseek-ai/schemastery";
|
|
5
5
|
//#region src/index.ts
|
|
6
6
|
const name = "dsh-trading-cn-connector-eastmoney";
|
|
7
|
-
const Config = Schema.object({
|
|
7
|
+
const Config = Schema.object({
|
|
8
|
+
enabled: Schema.boolean().default(true).description("是否激活东财连接器"),
|
|
9
|
+
market: Schema.union(["cn", "hk"]).default("cn").description("市场:cn=A 股,hk=港股")
|
|
10
|
+
});
|
|
8
11
|
const TRADING_CN_MARKET_DATA_KEY = "tradingCnMarketData";
|
|
12
|
+
const TRADING_HK_MARKET_DATA_KEY = "tradingHkMarketData";
|
|
13
|
+
function marketDataKey(market) {
|
|
14
|
+
return market === "hk" ? TRADING_HK_MARKET_DATA_KEY : TRADING_CN_MARKET_DATA_KEY;
|
|
15
|
+
}
|
|
9
16
|
var EastmoneyMarketDataService = class extends Service {
|
|
10
17
|
client;
|
|
11
18
|
constructor(ctx, options = {}, serviceName = TRADING_CN_MARKET_DATA_KEY) {
|
|
@@ -40,20 +47,21 @@ function routeAllows(ctx, config, market) {
|
|
|
40
47
|
}
|
|
41
48
|
function apply(ctx, config) {
|
|
42
49
|
if (!config.enabled) return;
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
const market = config.market ?? "cn";
|
|
51
|
+
if (!routeAllows(ctx, config, market)) return;
|
|
52
|
+
const marketData = new EastmoneyMarketDataService(ctx, { market }, marketDataKey(market));
|
|
45
53
|
ctx.inject(["tools"], (ctx) => {
|
|
46
54
|
const tools = ctx.tools;
|
|
47
55
|
const register = (t) => {
|
|
48
56
|
if (tools.get(t.name) === void 0) tools.register(t);
|
|
49
57
|
};
|
|
50
58
|
register(defineTool({
|
|
51
|
-
name:
|
|
52
|
-
description: "Get the latest trade price and quote for an A-share stock via Eastmoney API.",
|
|
59
|
+
name: `${market}_get_ticker`,
|
|
60
|
+
description: market === "hk" ? "Get the latest trade price and quote for a HK stock via Eastmoney API." : "Get the latest trade price and quote for an A-share stock via Eastmoney API.",
|
|
53
61
|
parameters: { symbol: {
|
|
54
62
|
type: "string",
|
|
55
63
|
required: true,
|
|
56
|
-
description: "A-share stock symbol, e.g. 600519.SH"
|
|
64
|
+
description: market === "hk" ? "HK stock symbol, e.g. 00700.HK" : "A-share stock symbol, e.g. 600519.SH"
|
|
57
65
|
} },
|
|
58
66
|
output: {
|
|
59
67
|
schema: { type: "string" },
|
|
@@ -67,13 +75,13 @@ function apply(ctx, config) {
|
|
|
67
75
|
}
|
|
68
76
|
}));
|
|
69
77
|
register(defineTool({
|
|
70
|
-
name:
|
|
71
|
-
description: "Get recent public klines for an A-share stock via Eastmoney API. Supports 1m/5m/15m/30m/1h/1d/1w/1M.",
|
|
78
|
+
name: `${market}_get_klines`,
|
|
79
|
+
description: market === "hk" ? "Get recent public klines for a HK stock via Eastmoney API. 1m = 当日分时(trends2);5m/15m/30m/1h/1d/1w/1M 走 kline。" : "Get recent public klines for an A-share stock via Eastmoney API. Supports 1m/5m/15m/30m/1h/1d/1w/1M.",
|
|
72
80
|
parameters: {
|
|
73
81
|
symbol: {
|
|
74
82
|
type: "string",
|
|
75
83
|
required: true,
|
|
76
|
-
description: "A-share stock symbol, e.g. 600519.SH"
|
|
84
|
+
description: market === "hk" ? "HK stock symbol, e.g. 00700.HK" : "A-share stock symbol, e.g. 600519.SH"
|
|
77
85
|
},
|
|
78
86
|
interval: {
|
|
79
87
|
type: "string",
|
|
@@ -102,4 +110,4 @@ function apply(ctx, config) {
|
|
|
102
110
|
});
|
|
103
111
|
}
|
|
104
112
|
//#endregion
|
|
105
|
-
export { Config, EastmoneyMarketDataService, EastmoneyRestClient, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TRADING_CN_MARKET_DATA_KEY, TradingServiceError, apply, mapIntervalToKlt, name, parseIntervalMs, routeAllows, toEastmoneySecid };
|
|
113
|
+
export { Config, EastmoneyMarketDataService, EastmoneyRestClient, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TRADING_CN_MARKET_DATA_KEY, TRADING_HK_MARKET_DATA_KEY, TradingServiceError, apply, eastmoneyPriceScale, mapIntervalToKlt, marketDataKey, name, parseIntervalMs, routeAllows, toEastmoneySecid, utc8WallTimeToEpochMs };
|
package/lib/rest.d.ts
CHANGED
|
@@ -7,35 +7,61 @@ declare class TradingServiceError extends Error {
|
|
|
7
7
|
declare const INTERVAL_VOCABULARY: readonly ["1m", "5m", "15m", "30m", "1h", "1d", "1w", "1M"];
|
|
8
8
|
declare function mapIntervalToKlt(interval: Interval$1): string;
|
|
9
9
|
declare function parseIntervalMs(interval: Interval$1): number;
|
|
10
|
+
/** 连接器服务的市场(cn=A 股;hk=港股,2026-09-08 扩展,实证见 spikes/impl-eastmoney-hk/)。 */
|
|
11
|
+
type EastmoneyMarket = 'cn' | 'hk';
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* `YYYY-MM-DD[ HH:MM[:SS]]`(UTC+8 墙钟;日 K 只有日期段)→ epoch ms。
|
|
14
|
+
* CN/HK 均为永久 UTC+8,故用固定偏移锚定,与运行机器时区无关(审查 M1:
|
|
15
|
+
* kline/get 此前用 `new Date('YYYY/MM/DD')` 按宿主本地时区解析,TZ≠UTC+8 时
|
|
16
|
+
* 与同市场 1m 序列相差数小时)。
|
|
15
17
|
*/
|
|
16
|
-
declare function
|
|
18
|
+
declare function utc8WallTimeToEpochMs(value: string): number;
|
|
19
|
+
/** 东财价格字段的分精度倍率:cn ×100(2 位小数),hk ×1000(3 位小数,响应 decimal=3)。 */
|
|
20
|
+
declare function eastmoneyPriceScale(market: EastmoneyMarket): number;
|
|
21
|
+
/**
|
|
22
|
+
* 将标准代码转为东财 secid。**按连接器市场分流**(2026-09-08 审查 H3/L1):同一个
|
|
23
|
+
* 东财服务实例可能服务 cn 或 hk,形态解析必须知道自己是哪个市场——否则港股宽容
|
|
24
|
+
* 形 `700` 会落进 CN 兜底(secid 0.700 / canonical 700.SZ),查询到另一个市场
|
|
25
|
+
* 的证券且回带错误规范形。
|
|
26
|
+
*
|
|
27
|
+
* - `market='hk'`:`00700.HK` / `HK.00700`(Futu 原生形)/ `HK00700` / 裸 1-5 位
|
|
28
|
+
* 数字 → secid 116.xxxxx;CN 形态(6 位数字 / .SH/.SZ/.BJ)显式拒绝。
|
|
29
|
+
* - `market='cn'`:6 位数字(可带 .SH/.SZ/.BJ)→ 1.xxxxxx(沪)/ 0.xxxxxx(深、
|
|
30
|
+
* 北);HK 形态显式拒绝。
|
|
31
|
+
*/
|
|
32
|
+
declare function toEastmoneySecid(symbol: string, market?: EastmoneyMarket): {
|
|
17
33
|
secid: string;
|
|
18
34
|
canonical: string;
|
|
35
|
+
market: EastmoneyMarket;
|
|
19
36
|
};
|
|
20
37
|
interface EastmoneyRestOptions {
|
|
21
38
|
baseUrl?: string;
|
|
22
39
|
historyBaseUrl?: string;
|
|
23
40
|
searchBaseUrl?: string;
|
|
24
41
|
fetchImpl?: typeof fetch;
|
|
42
|
+
/** 本实例服务的市场(形态解析与价格倍率都按它分流;缺省 cn)。 */
|
|
43
|
+
market?: EastmoneyMarket;
|
|
25
44
|
}
|
|
26
45
|
declare class EastmoneyRestClient {
|
|
27
46
|
readonly baseUrl: string;
|
|
28
47
|
readonly historyBaseUrl: string;
|
|
29
48
|
readonly searchBaseUrl: string;
|
|
49
|
+
/** 本实例的市场(cn/hk 单包双实例;形态解析必须与注册市场一致)。 */
|
|
50
|
+
readonly market: EastmoneyMarket;
|
|
30
51
|
private readonly fetchImpl;
|
|
31
52
|
constructor(options?: EastmoneyRestOptions);
|
|
32
53
|
private requestJson;
|
|
33
54
|
getTicker(symbol: string): Promise<Ticker$1>;
|
|
34
55
|
getKlines(symbol: string, interval?: Interval$1, limit?: number): Promise<Kline$1[]>;
|
|
56
|
+
/**
|
|
57
|
+
* 港股当日分时(trends2,ndays=1 = 最近一个交易日全天分钟序列,非交易日自然回落)。
|
|
58
|
+
* 行格式同 kline/get:`时间,开,收,高,低,量,额,均价`;时间 'YYYY-MM-DD HH:MM' 为 UTC+8 墙钟。
|
|
59
|
+
*/
|
|
60
|
+
private getHkIntradayTrends;
|
|
35
61
|
listInstruments(query?: string): Promise<Array<{
|
|
36
62
|
symbol: string;
|
|
37
63
|
name: string;
|
|
38
64
|
}>>;
|
|
39
65
|
}
|
|
40
66
|
//#endregion
|
|
41
|
-
export { EastmoneyRestClient, EastmoneyRestOptions, INTERVAL_VOCABULARY, type Interval$1 as Interval, type Kline$1 as Kline, type Ticker$1 as Ticker, TradingServiceError, mapIntervalToKlt, parseIntervalMs, toEastmoneySecid };
|
|
67
|
+
export { EastmoneyMarket, EastmoneyRestClient, EastmoneyRestOptions, INTERVAL_VOCABULARY, type Interval$1 as Interval, type Kline$1 as Kline, type Ticker$1 as Ticker, TradingServiceError, eastmoneyPriceScale, mapIntervalToKlt, parseIntervalMs, toEastmoneySecid, utc8WallTimeToEpochMs };
|
package/lib/rest.js
CHANGED
|
@@ -47,13 +47,16 @@ function parseIntervalMs(interval) {
|
|
|
47
47
|
* 上游分精度整数(×100,如昨收 f60=129740 → 1297.40)→ 数值;
|
|
48
48
|
* '-'/‘−’ 停牌占位或缺失返回 undefined。
|
|
49
49
|
*/
|
|
50
|
-
function
|
|
51
|
-
if (typeof value === "number") return Number.isFinite(value) ? value /
|
|
50
|
+
function parseScaled(value, scale) {
|
|
51
|
+
if (typeof value === "number") return Number.isFinite(value) ? value / scale : void 0;
|
|
52
52
|
if (typeof value === "string" && value !== "-" && value !== "−") {
|
|
53
53
|
const parsed = parseFloat(value);
|
|
54
|
-
return Number.isNaN(parsed) ? void 0 : parsed /
|
|
54
|
+
return Number.isNaN(parsed) ? void 0 : parsed / scale;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
function parseScaledHundred(value) {
|
|
58
|
+
return parseScaled(value, 100);
|
|
59
|
+
}
|
|
57
60
|
const KNOWN_SH_INDICES = /* @__PURE__ */ new Set([
|
|
58
61
|
"000688",
|
|
59
62
|
"000300",
|
|
@@ -61,45 +64,82 @@ const KNOWN_SH_INDICES = /* @__PURE__ */ new Set([
|
|
|
61
64
|
"000905",
|
|
62
65
|
"000852"
|
|
63
66
|
]);
|
|
67
|
+
/** CN/HK 均为永久 UTC+8(无夏令时)——墙钟时间用固定偏移锚定,与运行机器时区无关。 */
|
|
68
|
+
const UTC8_MS = 288e5;
|
|
64
69
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
70
|
+
* `YYYY-MM-DD[ HH:MM[:SS]]`(UTC+8 墙钟;日 K 只有日期段)→ epoch ms。
|
|
71
|
+
* CN/HK 均为永久 UTC+8,故用固定偏移锚定,与运行机器时区无关(审查 M1:
|
|
72
|
+
* kline/get 此前用 `new Date('YYYY/MM/DD')` 按宿主本地时区解析,TZ≠UTC+8 时
|
|
73
|
+
* 与同市场 1m 序列相差数小时)。
|
|
69
74
|
*/
|
|
70
|
-
function
|
|
75
|
+
function utc8WallTimeToEpochMs(value) {
|
|
76
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})(?: (\d{2}):(\d{2})(?::(\d{2}))?)?$/.exec(value);
|
|
77
|
+
if (!m) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `invalid eastmoney wall time ${JSON.stringify(value)}`);
|
|
78
|
+
return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4] ?? 0), Number(m[5] ?? 0), Number(m[6] ?? 0)) - UTC8_MS;
|
|
79
|
+
}
|
|
80
|
+
/** 东财价格字段的分精度倍率:cn ×100(2 位小数),hk ×1000(3 位小数,响应 decimal=3)。 */
|
|
81
|
+
function eastmoneyPriceScale(market) {
|
|
82
|
+
return market === "hk" ? 1e3 : 100;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 将标准代码转为东财 secid。**按连接器市场分流**(2026-09-08 审查 H3/L1):同一个
|
|
86
|
+
* 东财服务实例可能服务 cn 或 hk,形态解析必须知道自己是哪个市场——否则港股宽容
|
|
87
|
+
* 形 `700` 会落进 CN 兜底(secid 0.700 / canonical 700.SZ),查询到另一个市场
|
|
88
|
+
* 的证券且回带错误规范形。
|
|
89
|
+
*
|
|
90
|
+
* - `market='hk'`:`00700.HK` / `HK.00700`(Futu 原生形)/ `HK00700` / 裸 1-5 位
|
|
91
|
+
* 数字 → secid 116.xxxxx;CN 形态(6 位数字 / .SH/.SZ/.BJ)显式拒绝。
|
|
92
|
+
* - `market='cn'`:6 位数字(可带 .SH/.SZ/.BJ)→ 1.xxxxxx(沪)/ 0.xxxxxx(深、
|
|
93
|
+
* 北);HK 形态显式拒绝。
|
|
94
|
+
*/
|
|
95
|
+
function toEastmoneySecid(symbol, market = "cn") {
|
|
71
96
|
const clean = symbol.trim().toUpperCase();
|
|
97
|
+
if (market === "hk") {
|
|
98
|
+
const hkMatch = /^(\d{1,5})\.HK$/.exec(clean) ?? /^HK\.?(\d{1,5})$/.exec(clean) ?? /^(\d{1,5})$/.exec(clean);
|
|
99
|
+
if (hkMatch === null) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `Eastmoney hk: malformed HK symbol ${JSON.stringify(symbol)}`);
|
|
100
|
+
const hkCode = hkMatch[1].padStart(5, "0");
|
|
101
|
+
return {
|
|
102
|
+
secid: `116.${hkCode}`,
|
|
103
|
+
canonical: `${hkCode}.HK`,
|
|
104
|
+
market: "hk"
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (/^\d{1,5}$/.test(clean) || /\.HK$/.test(clean) || /^HK/.test(clean)) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `Eastmoney cn: not an A-share symbol ${JSON.stringify(symbol)}`);
|
|
72
108
|
let code = clean;
|
|
73
|
-
let
|
|
109
|
+
let exchange = "";
|
|
74
110
|
if (clean.includes(".")) {
|
|
75
111
|
const parts = clean.split(".");
|
|
76
112
|
code = parts[0];
|
|
77
|
-
|
|
113
|
+
exchange = parts[1];
|
|
78
114
|
} else if (/^\d{6}$/.test(clean)) {
|
|
79
115
|
code = clean;
|
|
80
|
-
if (KNOWN_SH_INDICES.has(code) || code.startsWith("6") || code.startsWith("5") || code.startsWith("9"))
|
|
81
|
-
else if (code.startsWith("0") || code.startsWith("3") || code.startsWith("1"))
|
|
82
|
-
else if (code.startsWith("8") || code.startsWith("4") || code.startsWith("92"))
|
|
116
|
+
if (KNOWN_SH_INDICES.has(code) || code.startsWith("6") || code.startsWith("5") || code.startsWith("9")) exchange = "SH";
|
|
117
|
+
else if (code.startsWith("0") || code.startsWith("3") || code.startsWith("1")) exchange = "SZ";
|
|
118
|
+
else if (code.startsWith("8") || code.startsWith("4") || code.startsWith("92")) exchange = "BJ";
|
|
83
119
|
}
|
|
84
|
-
if (!
|
|
85
|
-
if (KNOWN_SH_INDICES.has(code) || code.startsWith("6") || code.startsWith("5"))
|
|
86
|
-
else
|
|
120
|
+
if (!exchange) {
|
|
121
|
+
if (KNOWN_SH_INDICES.has(code) || code.startsWith("6") || code.startsWith("5")) exchange = "SH";
|
|
122
|
+
else exchange = "SZ";
|
|
87
123
|
}
|
|
88
124
|
return {
|
|
89
|
-
secid: `${
|
|
90
|
-
canonical: `${code}.${
|
|
125
|
+
secid: `${exchange === "SH" ? "1" : "0"}.${code}`,
|
|
126
|
+
canonical: `${code}.${exchange}`,
|
|
127
|
+
market: "cn"
|
|
91
128
|
};
|
|
92
129
|
}
|
|
93
130
|
var EastmoneyRestClient = class {
|
|
94
131
|
baseUrl;
|
|
95
132
|
historyBaseUrl;
|
|
96
133
|
searchBaseUrl;
|
|
134
|
+
/** 本实例的市场(cn/hk 单包双实例;形态解析必须与注册市场一致)。 */
|
|
135
|
+
market;
|
|
97
136
|
fetchImpl;
|
|
98
137
|
constructor(options = {}) {
|
|
99
138
|
this.baseUrl = options.baseUrl ?? "https://push2.eastmoney.com";
|
|
100
139
|
this.historyBaseUrl = options.historyBaseUrl ?? "https://push2his.eastmoney.com";
|
|
101
140
|
this.searchBaseUrl = options.searchBaseUrl ?? "https://searchapi.eastmoney.com";
|
|
102
141
|
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
142
|
+
this.market = options.market ?? "cn";
|
|
103
143
|
}
|
|
104
144
|
async requestJson(url) {
|
|
105
145
|
try {
|
|
@@ -115,21 +155,22 @@ var EastmoneyRestClient = class {
|
|
|
115
155
|
}
|
|
116
156
|
}
|
|
117
157
|
async getTicker(symbol) {
|
|
118
|
-
const { secid, canonical } = toEastmoneySecid(symbol);
|
|
158
|
+
const { secid, canonical, market } = toEastmoneySecid(symbol, this.market);
|
|
159
|
+
const scale = eastmoneyPriceScale(market);
|
|
119
160
|
const url = `${this.baseUrl}/api/qt/stock/get?secid=${secid}&fields=f43,f44,f45,f46,f47,f48,f57,f58,f60,f86,f169,f170`;
|
|
120
161
|
const res = await this.requestJson(url);
|
|
121
162
|
if (!res.data) throw new TradingServiceError("TRADING_SYMBOL_NOT_FOUND", `Eastmoney symbol not found: ${symbol}`);
|
|
122
163
|
const d = res.data;
|
|
123
164
|
let rawPrice = 0;
|
|
124
|
-
if (typeof d.f43 === "number") rawPrice = d.f43 /
|
|
165
|
+
if (typeof d.f43 === "number") rawPrice = d.f43 / scale;
|
|
125
166
|
else if (typeof d.f43 === "string" && d.f43 !== "-" && d.f43 !== "−") {
|
|
126
167
|
const parsed = parseFloat(d.f43);
|
|
127
|
-
rawPrice = Number.isNaN(parsed) ? 0 : parsed /
|
|
168
|
+
rawPrice = Number.isNaN(parsed) ? 0 : parsed / scale;
|
|
128
169
|
}
|
|
129
170
|
const price = rawPrice > 0 ? rawPrice : 0;
|
|
130
171
|
const volume = typeof d.f47 === "number" ? d.f47 : typeof d.f47 === "string" ? parseFloat(d.f47) : 0;
|
|
131
172
|
const timestamp = typeof d.f86 === "number" ? d.f86 * 1e3 : Date.now();
|
|
132
|
-
const prevClose = parseScaledHundred(d.f60);
|
|
173
|
+
const prevClose = market === "hk" ? parseScaled(d.f60, scale) : parseScaledHundred(d.f60);
|
|
133
174
|
const changePercent = parseScaledHundred(d.f170);
|
|
134
175
|
const name = typeof d.f58 === "string" && d.f58.trim() ? d.f58.trim() : void 0;
|
|
135
176
|
return {
|
|
@@ -143,7 +184,8 @@ var EastmoneyRestClient = class {
|
|
|
143
184
|
};
|
|
144
185
|
}
|
|
145
186
|
async getKlines(symbol, interval = "1d", limit = 100) {
|
|
146
|
-
const { secid } = toEastmoneySecid(symbol);
|
|
187
|
+
const { secid, market } = toEastmoneySecid(symbol, this.market);
|
|
188
|
+
if (market === "hk" && interval === "1m") return this.getHkIntradayTrends(secid, limit);
|
|
147
189
|
const klt = mapIntervalToKlt(interval);
|
|
148
190
|
const stepMs = parseIntervalMs(interval);
|
|
149
191
|
const url = `${this.historyBaseUrl}/api/qt/stock/kline/get?secid=${secid}&klt=${klt}&fqt=1&lmt=${limit}&end=20500101&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58`;
|
|
@@ -159,8 +201,12 @@ var EastmoneyRestClient = class {
|
|
|
159
201
|
const high = parseFloat(parts[3]);
|
|
160
202
|
const low = parseFloat(parts[4]);
|
|
161
203
|
const volume = parseFloat(parts[5]);
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
let openTime;
|
|
205
|
+
try {
|
|
206
|
+
openTime = utc8WallTimeToEpochMs(timeStr);
|
|
207
|
+
} catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
164
210
|
klines.push({
|
|
165
211
|
openTime,
|
|
166
212
|
open,
|
|
@@ -173,17 +219,48 @@ var EastmoneyRestClient = class {
|
|
|
173
219
|
}
|
|
174
220
|
return klines;
|
|
175
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* 港股当日分时(trends2,ndays=1 = 最近一个交易日全天分钟序列,非交易日自然回落)。
|
|
224
|
+
* 行格式同 kline/get:`时间,开,收,高,低,量,额,均价`;时间 'YYYY-MM-DD HH:MM' 为 UTC+8 墙钟。
|
|
225
|
+
*/
|
|
226
|
+
async getHkIntradayTrends(secid, limit) {
|
|
227
|
+
const url = `${this.historyBaseUrl}/api/qt/stock/trends2/get?secid=${secid}&fields1=f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11&fields2=f51,f52,f53,f54,f55,f56,f57,f58&ndays=1&iscr=0`;
|
|
228
|
+
const trends = (await this.requestJson(url)).data?.trends;
|
|
229
|
+
if (!Array.isArray(trends)) return [];
|
|
230
|
+
const stepMs = parseIntervalMs("1m");
|
|
231
|
+
const klines = [];
|
|
232
|
+
for (const line of trends) {
|
|
233
|
+
const parts = line.split(",");
|
|
234
|
+
if (parts.length < 6) continue;
|
|
235
|
+
const openTime = utc8WallTimeToEpochMs(parts[0] ?? "");
|
|
236
|
+
klines.push({
|
|
237
|
+
openTime,
|
|
238
|
+
open: parseFloat(parts[1] ?? ""),
|
|
239
|
+
high: parseFloat(parts[3] ?? ""),
|
|
240
|
+
low: parseFloat(parts[4] ?? ""),
|
|
241
|
+
close: parseFloat(parts[2] ?? ""),
|
|
242
|
+
volume: parseFloat(parts[5] ?? ""),
|
|
243
|
+
closeTime: openTime + stepMs - 1
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return klines.slice(-Math.max(limit, 1));
|
|
247
|
+
}
|
|
176
248
|
async listInstruments(query) {
|
|
177
249
|
if (!query) return [];
|
|
178
250
|
const url = `${this.searchBaseUrl}/api/suggest/get?input=${encodeURIComponent(query)}&type=14`;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
251
|
+
const items = (await this.requestJson(url)).QuotationCodeTable?.Data ?? [];
|
|
252
|
+
const out = [];
|
|
253
|
+
for (const item of items) try {
|
|
254
|
+
const { canonical } = toEastmoneySecid(item.Code, this.market);
|
|
255
|
+
out.push({
|
|
182
256
|
symbol: canonical,
|
|
183
257
|
name: item.Name
|
|
184
|
-
};
|
|
185
|
-
}
|
|
258
|
+
});
|
|
259
|
+
} catch {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
186
263
|
}
|
|
187
264
|
};
|
|
188
265
|
//#endregion
|
|
189
|
-
export { EastmoneyRestClient, INTERVAL_VOCABULARY, TradingServiceError, mapIntervalToKlt, parseIntervalMs, toEastmoneySecid };
|
|
266
|
+
export { EastmoneyRestClient, INTERVAL_VOCABULARY, TradingServiceError, eastmoneyPriceScale, mapIntervalToKlt, parseIntervalMs, toEastmoneySecid, utc8WallTimeToEpochMs };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshtrading/connector-eastmoney",
|
|
3
3
|
"description": "【模板】交易所连接器插件脚手架:Eastmoney(eastmoney)v2 插件骨架 —— 市场数据服务 + 交易服务 + 三态闸门 + BYOK 凭证 ref。生成器:node scripts/new-connector.mjs --slug <exchange>",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@dshtrading/api": "^0.
|
|
24
|
+
"@dshtrading/api": "^0.2.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"@deepseek-ai/cordis": ">=4.0.0",
|