|
| 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