api.ts•6.67 kB
import fetch from "node-fetch";
import type { Response } from "node-fetch";
import { COMMON_HEADERS, SEARCH_API, QUOTE_API, HISTORY_API, INDEX_API } from "./constants.js";
import type {
SearchResponse,
QuoteResponse,
HistoryResponse,
IndexResponse,
StockData,
HistoryDataPoint,
IndexData,
} from "./types.js";
import { getStockMarket } from "./utils.js";
async function makeRequest(
url: string,
params?: Record<string, string>
): Promise<Response> {
const finalUrl = params ? `${url}?${new URLSearchParams(params)}` : url;
const options = {
headers: COMMON_HEADERS,
};
try {
const response = await fetch(finalUrl, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
console.error(`Error making request to ${url}:`, error);
throw error;
}
}
export async function searchStock(keyword: string): Promise<[string, string] | null> {
try {
const params = {
input: keyword,
type: "14",
token: "D43BF722C8E33BDC906FB84D85E326E8",
count: "5",
};
const response = await makeRequest(SEARCH_API, params);
const data = (await response.json()) as SearchResponse;
if (data.QuotationCodeTable.Status === 0 && data.QuotationCodeTable.Data) {
for (const item of data.QuotationCodeTable.Data) {
if (
item.SecurityTypeName === "沪A" ||
item.SecurityTypeName === "深A"
) {
return [item.Code, item.Name];
}
}
}
return null;
} catch (error) {
console.error("Error searching stock:", error);
return null;
}
}
export async function fetchStockHistory(
stockInput: string,
period: string = "101",
days: number = 30
): Promise<HistoryDataPoint[] | null> {
try {
let stockCode = stockInput;
if (!stockInput.match(/^\d{6}$/)) {
const searchResult = await searchStock(stockInput);
if (!searchResult) {
console.error(`未找到股票:${stockInput}`);
return null;
}
[stockCode] = searchResult;
}
const [market, fullCode] = getStockMarket(stockCode);
const params = {
secid: fullCode,
klt: period,
fqt: "1",
lmt: days.toString(),
end: "20500101",
iscca: "1",
fields1: "f1,f2,f3,f4,f5,f6",
fields2: "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61",
};
const response = await makeRequest(HISTORY_API, params);
const data = (await response.json()) as HistoryResponse;
if (data.rc !== 0 || !data.data || !data.data.klines) {
throw new Error(data.msg || "获取历史数据失败");
}
const historyData: HistoryDataPoint[] = data.data.klines.map((kline) => {
const parts = kline.split(",");
return {
date: parts[0],
open: parseFloat(parts[1]),
close: parseFloat(parts[2]),
high: parseFloat(parts[3]),
low: parseFloat(parts[4]),
volume: parseInt(parts[5]) / 100,
amount: parseFloat(parts[6]) / 10000,
changePercent: parseFloat(parts[8]),
};
});
return historyData.reverse();
} catch (error) {
console.error("Error fetching stock history:", error);
return null;
}
}
export async function fetchMarketIndex(): Promise<IndexData[] | null> {
try {
const params = {
fltt: "2",
invt: "2",
fields: "f1,f2,f3,f4,f12,f13,f14",
secids: "1.000001,0.399001",
};
const response = await makeRequest(INDEX_API, params);
const data = (await response.json()) as IndexResponse;
if (data.rc !== 0 || !data.data || !data.data.diff) {
throw new Error(data.msg || "获取指数数据失败");
}
const indexData: IndexData[] = data.data.diff.map((item) => ({
code: item.f12,
name: item.f14,
price: item.f2,
changePercent: item.f3,
change: item.f4,
volume: 0,
amount: 0,
time: new Date().toLocaleTimeString(),
}));
return indexData;
} catch (error) {
console.error("Error fetching market index:", error);
return null;
}
}
export async function fetchStockData(stockInput: string): Promise<StockData | null> {
try {
let stockCode = stockInput;
let stockName = "";
if (!stockInput.match(/^\d{6}$/)) {
const searchResult = await searchStock(stockInput);
if (!searchResult) {
console.error(`未找到股票:${stockInput}`);
return null;
}
[stockCode, stockName] = searchResult;
console.error(`找到股票:${stockName}(${stockCode})`);
}
const [market, fullCode] = getStockMarket(stockCode);
const params = {
secid: fullCode,
fields:
"f43,f57,f58,f169,f170,f46,f44,f51,f168,f47,f164,f163,f116,f60,f45,f52,f50,f48,f167,f117,f71,f161,f49,f530,f135,f136,f137,f138,f139,f141,f142,f144,f145,f147,f148,f140,f143,f146,f149,f55,f62,f162,f92,f173,f104,f105,f84,f85,f183,f184,f185,f186,f187,f188,f189,f190,f191,f192,f206,f207,f208,f209,f210,f211,f212,f213,f214,f215,f86,f107,f111,f86,f177,f78,f110",
};
const response = await makeRequest(QUOTE_API, params);
const data = (await response.json()) as QuoteResponse;
if (data.rc !== 0 || !data.data) {
throw new Error(data.msg || "获取数据失败");
}
const quote = data.data;
const buyOrders = [];
const sellOrders = [];
for (let i = 1; i <= 5; i++) {
const buyPrice = quote[`f${18 + i * 2}`];
const buyVolume = quote[`f${17 + i * 2}`];
const sellPrice = quote[`f${10 + i * 2}`];
const sellVolume = quote[`f${9 + i * 2}`];
if (typeof buyPrice === "number" && typeof buyVolume === "number") {
buyOrders.push({
price: buyPrice / 100,
volume: buyVolume / 100,
});
}
if (typeof sellPrice === "number" && typeof sellVolume === "number") {
sellOrders.push({
price: sellPrice / 100,
volume: sellVolume / 100,
});
}
}
return {
code: stockCode,
name: quote.f58,
price: quote.f43 / 100,
change: quote.f169 / 100,
changePercent: quote.f170 / 100,
volume: quote.f47 / 100,
amount: quote.f48 / 10000,
high: quote.f44 / 100,
low: quote.f45 / 100,
open: quote.f46 / 100,
lastClose: quote.f60 / 100,
turnoverRate: quote.f168 / 100,
peRatio: quote.f162,
amplitude: quote.f171 ? quote.f171 / 100 : 0,
time: new Date().toLocaleTimeString(),
buyOrders,
sellOrders,
};
} catch (error) {
console.error("Error fetching stock data:", error);
return null;
}
}