Skip to content

Commit 28481bc

Browse files
authored
feat: support features (#21)
**期货行情** - 新增国内期货历史 K 线接口 `getFuturesKline`,支持全部国内期货品种(上期所、大商所、郑商所、上海国际能源交易中心、中金所、广期所),支持主连合约(如 `RBM`)和具体合约(如 `rb2510`),支持日/周/月 K 线 - 新增全球期货实时行情接口 `getGlobalFuturesSpot`,覆盖 COMEX、NYMEX、CBOT、LME 等主要国际期货交易所,支持 600+ 个品种 - 新增全球期货历史 K 线接口 `getGlobalFuturesKline`,支持日/周/月 K 线 - 新增期货库存品种列表接口 `getFuturesInventorySymbols` - 新增期货库存数据接口 `getFuturesInventory`,支持查询国内各期货品种的历史库存数据 - 新增 COMEX 黄金/白银库存接口 `getComexInventory`
1 parent 9e81d06 commit 28481bc

File tree

21 files changed

+2084
-0
lines changed

21 files changed

+2084
-0
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
-**A 股、港股、美股、公募基金**实时行情
5656
-**历史 K 线**(日/周/月)、**分钟 K 线**(1/5/15/30/60 分钟)和**当日分时走势**数据
5757
-**技术指标**:内置 MA、MACD、BOLL、KDJ、RSI、WR 等常用指标计算
58+
-**期货行情**:国内期货 K 线、全球期货实时行情与 K 线、期货库存数据
5859
-**资金流向****盘口大单**等扩展数据
5960
- ✅ 获取全部 **A 股代码列表**(5000+ 只股票)和批量获取**全市场行情**(内置并发控制)
6061

@@ -161,6 +162,17 @@ console.log(`共获取 ${allQuotes.length} 只股票`);
161162
| `getConceptKline` | 概念板块历史 K 线(日/周/月) |
162163
| `getConceptMinuteKline` | 概念板块分时行情(1/5/15/30/60 分钟) |
163164

165+
### 期货行情
166+
167+
| 方法 | 说明 |
168+
|------|------|
169+
| `getFuturesKline` | 国内期货历史 K 线(日/周/月) |
170+
| `getGlobalFuturesSpot` | 全球期货实时行情 |
171+
| `getGlobalFuturesKline` | 全球期货历史 K 线(日/周/月) |
172+
| `getFuturesInventorySymbols` | 期货库存品种列表 |
173+
| `getFuturesInventory` | 期货库存数据 |
174+
| `getComexInventory` | COMEX 黄金/白银库存 |
175+
164176
### 扩展数据
165177

166178
| 方法 | 说明 |

README_EN.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ If you're a frontend engineer, you may have encountered these problems:
5555
- ✅ Real-time quotes for **A-shares, HK stocks, US stocks, mutual funds**
5656
-**Historical K-line** (daily/weekly/monthly), **minute K-line** (1/5/15/30/60 minutes), and **today's timeline** data
5757
-**Technical indicators**: Built-in MA, MACD, BOLL, KDJ, RSI, WR, BIAS, CCI, ATR and more
58+
-**Futures data**: Domestic futures K-line, global futures real-time quotes & K-line, futures inventory data
5859
- ✅ Extended data such as **fund flow**, **large order ratio**
5960
- ✅ Get full **A-share code list** (5000+ stocks) and batch fetch **whole-market quotes** (with built-in concurrency control)
6061

@@ -161,6 +162,17 @@ console.log(`Fetched ${allQuotes.length} stocks`);
161162
| `getConceptKline` | Concept sector historical K-line (daily/weekly/monthly) |
162163
| `getConceptMinuteKline` | Concept sector minute K-line (1/5/15/30/60 minutes) |
163164

165+
### Futures
166+
167+
| Method | Description |
168+
|--------|-------------|
169+
| `getFuturesKline` | Domestic futures historical K-line (daily/weekly/monthly) |
170+
| `getGlobalFuturesSpot` | Global futures real-time quotes |
171+
| `getGlobalFuturesKline` | Global futures historical K-line (daily/weekly/monthly) |
172+
| `getFuturesInventorySymbols` | Futures inventory symbol list |
173+
| `getFuturesInventory` | Futures inventory data |
174+
| `getComexInventory` | COMEX gold/silver inventory |
175+
164176
### Extended Data
165177

166178
| Method | Description |

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"@types/node": "^24.10.2",
6767
"@vitest/coverage-v8": "^4.0.16",
6868
"msw": "^2.10.4",
69+
"playwright": "^1.58.2",
6970
"tsup": "^8.3.5",
7071
"typescript": "^5.9.3",
7172
"vitepress": "^1.6.4",

src/core/constants.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,63 @@ export const EM_CONCEPT_TRENDS_URL = 'https://push2his.eastmoney.com/api/qt/stoc
4141
// 东方财富数据中心 API
4242
export const EM_DATACENTER_URL = 'https://datacenter-web.eastmoney.com/api/data/v1/get';
4343

44+
// 东方财富期货 API
45+
export const EM_FUTURES_KLINE_URL = 'https://push2his.eastmoney.com/api/qt/stock/kline/get';
46+
export const EM_FUTURES_GLOBAL_SPOT_URL =
47+
'https://futsseapi.eastmoney.com/list/COMEX,NYMEX,COBOT,SGX,NYBOT,LME,MDEX,TOCOM,IPE';
48+
export const EM_FUTURES_GLOBAL_SPOT_TOKEN = '58b2fa8f54638b60b87d69b31969089c';
49+
50+
/**
51+
* 国内期货交易所 market code 映射
52+
* 来源:futsse-static.eastmoney.com/redis?msgid=gnweb 验证
53+
*/
54+
export const FUTURES_EXCHANGE_MAP: Record<string, number> = {
55+
SHFE: 113,
56+
DCE: 114,
57+
CZCE: 115,
58+
INE: 142,
59+
CFFEX: 220,
60+
GFEX: 225,
61+
};
62+
63+
/**
64+
* 品种代码 -> 交易所映射(内置,免去动态获取的 CORS 问题)
65+
*/
66+
export const FUTURES_VARIETY_EXCHANGE: Record<string, string> = {
67+
// SHFE 上海期货交易所
68+
cu: 'SHFE', al: 'SHFE', zn: 'SHFE', pb: 'SHFE', au: 'SHFE', ag: 'SHFE',
69+
rb: 'SHFE', wr: 'SHFE', fu: 'SHFE', ru: 'SHFE', bu: 'SHFE', hc: 'SHFE',
70+
ni: 'SHFE', sn: 'SHFE', sp: 'SHFE', ss: 'SHFE', ao: 'SHFE', br: 'SHFE',
71+
// DCE 大连商品交易所
72+
c: 'DCE', a: 'DCE', b: 'DCE', m: 'DCE', y: 'DCE', p: 'DCE',
73+
l: 'DCE', v: 'DCE', j: 'DCE', jm: 'DCE', i: 'DCE', jd: 'DCE',
74+
pp: 'DCE', cs: 'DCE', eg: 'DCE', eb: 'DCE', pg: 'DCE', lh: 'DCE',
75+
// CZCE 郑州商品交易所
76+
WH: 'CZCE', CF: 'CZCE', SR: 'CZCE', TA: 'CZCE', OI: 'CZCE', MA: 'CZCE',
77+
FG: 'CZCE', RM: 'CZCE', SF: 'CZCE', SM: 'CZCE', ZC: 'CZCE', AP: 'CZCE',
78+
CJ: 'CZCE', UR: 'CZCE', SA: 'CZCE', PF: 'CZCE', PK: 'CZCE', PX: 'CZCE',
79+
SH: 'CZCE',
80+
// INE 上海国际能源交易中心
81+
sc: 'INE', nr: 'INE', lu: 'INE', bc: 'INE', ec: 'INE',
82+
// CFFEX 中国金融期货交易所
83+
IF: 'CFFEX', IC: 'CFFEX', IH: 'CFFEX', IM: 'CFFEX',
84+
TS: 'CFFEX', TF: 'CFFEX', T: 'CFFEX', TL: 'CFFEX',
85+
// GFEX 广州期货交易所
86+
si: 'GFEX', lc: 'GFEX', ps: 'GFEX', pt: 'GFEX', pd: 'GFEX',
87+
};
88+
89+
/**
90+
* 全球期货市场代码映射(来自 akshare futures_hf_em.py)
91+
*/
92+
export const GLOBAL_FUTURES_MARKET: Record<string, number> = {
93+
HG: 101, GC: 101, SI: 101, QI: 101, QO: 101, MGC: 101,
94+
CL: 102, NG: 102, RB: 102, HO: 102, PA: 102, PL: 102,
95+
ZW: 103, ZM: 103, ZS: 103, ZC: 103, ZL: 103, ZR: 103,
96+
YM: 103, NQ: 103, ES: 103,
97+
SB: 108, CT: 108,
98+
LCPT: 109, LZNT: 109, LALT: 109,
99+
};
100+
44101
// 默认配置
45102
export const DEFAULT_TIMEOUT = 30000;
46103
export const DEFAULT_BATCH_SIZE = 500;

src/core/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ export {
5656
EM_CONCEPT_KLINE_URL,
5757
EM_CONCEPT_TRENDS_URL,
5858
EM_DATACENTER_URL,
59+
EM_FUTURES_KLINE_URL,
60+
EM_FUTURES_GLOBAL_SPOT_URL,
61+
EM_FUTURES_GLOBAL_SPOT_TOKEN,
62+
FUTURES_EXCHANGE_MAP,
63+
FUTURES_VARIETY_EXCHANGE,
64+
GLOBAL_FUTURES_MARKET,
5965
DEFAULT_TIMEOUT,
6066
DEFAULT_BATCH_SIZE,
6167
MAX_BATCH_SIZE,

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,9 @@ export type {
8383
IndustryBoardMinuteKlineOptions,
8484
ConceptBoardKlineOptions,
8585
ConceptBoardMinuteKlineOptions,
86+
FuturesKlineOptions,
87+
GlobalFuturesSpotOptions,
88+
GlobalFuturesKlineOptions,
89+
FuturesInventoryOptions,
90+
ComexInventoryOptions,
8691
} from './providers/eastmoney';
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
* 东方财富 - 全球期货行情 + K 线
3+
*/
4+
import {
5+
RequestClient,
6+
EM_FUTURES_KLINE_URL,
7+
EM_FUTURES_GLOBAL_SPOT_URL,
8+
EM_FUTURES_GLOBAL_SPOT_TOKEN,
9+
GLOBAL_FUTURES_MARKET,
10+
assertKlinePeriod,
11+
getPeriodCode,
12+
toNumber,
13+
} from '../../core';
14+
import type { GlobalFuturesQuote, FuturesKline } from '../../types';
15+
import { fetchEmHistoryKline, parseEmKlineCsv } from './utils';
16+
17+
export interface GlobalFuturesSpotOptions {
18+
/** 每页条数,默认 20 */
19+
pageSize?: number;
20+
}
21+
22+
export interface GlobalFuturesKlineOptions {
23+
/** K 线周期 */
24+
period?: 'daily' | 'weekly' | 'monthly';
25+
/** 开始日期 YYYYMMDD */
26+
startDate?: string;
27+
/** 结束日期 YYYYMMDD */
28+
endDate?: string;
29+
/** 东方财富市场代码(用于未内置的品种,可从 GLOBAL_FUTURES_MARKET 查表) */
30+
marketCode?: number;
31+
}
32+
33+
interface FutsseApiItem {
34+
dm: string;
35+
name: string;
36+
p: number;
37+
zde: number;
38+
zdf: number;
39+
o: number;
40+
h: number;
41+
l: number;
42+
zjsj: number;
43+
vol: number;
44+
wp: number;
45+
np: number;
46+
ccl: number;
47+
sc: number;
48+
zsjd: number;
49+
}
50+
51+
interface FutsseApiResponse {
52+
list: FutsseApiItem[];
53+
total: number;
54+
}
55+
56+
/**
57+
* 获取全球期货实时行情(自动分页拉取全部)
58+
* @param client - 请求客户端
59+
* @param options - 配置选项
60+
* @returns 全球期货行情数组
61+
*/
62+
export async function getGlobalFuturesSpot(
63+
client: RequestClient,
64+
options: GlobalFuturesSpotOptions = {}
65+
): Promise<GlobalFuturesQuote[]> {
66+
const pageSize = options.pageSize ?? 20;
67+
const allData: GlobalFuturesQuote[] = [];
68+
let pageIndex = 0;
69+
let total = 0;
70+
71+
do {
72+
const params = new URLSearchParams({
73+
orderBy: 'dm',
74+
sort: 'desc',
75+
pageSize: String(pageSize),
76+
pageIndex: String(pageIndex),
77+
token: EM_FUTURES_GLOBAL_SPOT_TOKEN,
78+
field: 'dm,sc,name,p,zsjd,zde,zdf,f152,o,h,l,zjsj,vol,wp,np,ccl',
79+
blockName: 'callback',
80+
});
81+
82+
const url = `${EM_FUTURES_GLOBAL_SPOT_URL}?${params.toString()}`;
83+
const json = await client.get<FutsseApiResponse>(url, {
84+
responseType: 'json',
85+
});
86+
87+
if (!json || !Array.isArray(json.list)) {
88+
break;
89+
}
90+
91+
if (pageIndex === 0) {
92+
total = json.total ?? 0;
93+
}
94+
95+
const items = json.list.map(mapFutsseItem);
96+
allData.push(...items);
97+
pageIndex++;
98+
} while (allData.length < total);
99+
100+
return allData;
101+
}
102+
103+
function mapFutsseItem(item: FutsseApiItem): GlobalFuturesQuote {
104+
return {
105+
code: item.dm || '',
106+
name: item.name || '',
107+
price: toNumber(String(item.p)),
108+
change: toNumber(String(item.zde)),
109+
changePercent: toNumber(String(item.zdf)),
110+
open: toNumber(String(item.o)),
111+
high: toNumber(String(item.h)),
112+
low: toNumber(String(item.l)),
113+
prevSettle: toNumber(String(item.zjsj)),
114+
volume: toNumber(String(item.vol)),
115+
buyVolume: toNumber(String(item.wp)),
116+
sellVolume: toNumber(String(item.np)),
117+
openInterest: toNumber(String(item.ccl)),
118+
};
119+
}
120+
121+
/**
122+
* 解析全球期货 K 线 CSV(14 列),提取持仓量
123+
*/
124+
function parseGlobalFuturesKlineCsv(
125+
line: string
126+
): Omit<FuturesKline, 'code' | 'name'> {
127+
const base = parseEmKlineCsv(line);
128+
const parts = line.split(',');
129+
return {
130+
...base,
131+
openInterest: parts.length > 12 ? toNumber(parts[12]) : null,
132+
};
133+
}
134+
135+
/**
136+
* 从合约代码中提取全球期货品种前缀
137+
* 如 HG00Y -> HG, CL2507 -> CL, LCPT -> LCPT
138+
*/
139+
function extractGlobalVariety(symbol: string): string {
140+
const match = symbol.match(/^([A-Z]+)/);
141+
if (!match) {
142+
throw new RangeError(
143+
`Invalid global futures symbol: "${symbol}". Expected format like HG00Y, CL2507`
144+
);
145+
}
146+
return match[1];
147+
}
148+
149+
/**
150+
* 获取全球期货历史 K 线(日/周/月)
151+
* @param client - 请求客户端
152+
* @param symbol - 合约代码,如 'HG00Y'(COMEX铜连续)
153+
* @param options - 配置选项
154+
* @returns 期货 K 线数据数组
155+
*/
156+
export async function getGlobalFuturesKline(
157+
client: RequestClient,
158+
symbol: string,
159+
options: GlobalFuturesKlineOptions = {}
160+
): Promise<FuturesKline[]> {
161+
const {
162+
period = 'daily',
163+
startDate = '19700101',
164+
endDate = '20500101',
165+
} = options;
166+
assertKlinePeriod(period);
167+
168+
let mktCode = options.marketCode;
169+
if (mktCode === undefined) {
170+
const variety = extractGlobalVariety(symbol);
171+
mktCode = GLOBAL_FUTURES_MARKET[variety];
172+
if (mktCode === undefined) {
173+
const supported = Object.keys(GLOBAL_FUTURES_MARKET).join(', ');
174+
throw new RangeError(
175+
`Unknown global futures variety: "${variety}". Supported: ${supported}. ` +
176+
`Or specify marketCode manually via options.`
177+
);
178+
}
179+
}
180+
181+
const secid = `${mktCode}.${symbol}`;
182+
183+
const params = new URLSearchParams({
184+
fields1: 'f1,f2,f3,f4,f5,f6',
185+
fields2: 'f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61,f62,f63,f64',
186+
ut: '7eea3edcaed734bea9cbfc24409ed989',
187+
klt: getPeriodCode(period),
188+
fqt: '0',
189+
secid,
190+
beg: startDate,
191+
end: endDate,
192+
});
193+
194+
const { klines, name, code } = await fetchEmHistoryKline(
195+
client,
196+
EM_FUTURES_KLINE_URL,
197+
params
198+
);
199+
200+
if (klines.length === 0) {
201+
return [];
202+
}
203+
204+
return klines.map((line) => {
205+
const item = parseGlobalFuturesKlineCsv(line);
206+
return {
207+
...item,
208+
code: code || symbol,
209+
name: name || '',
210+
};
211+
});
212+
}

0 commit comments

Comments
 (0)