up
This commit is contained in:
Executable
+332
@@ -0,0 +1,332 @@
|
||||
// 股票数据获取工具:通过 Python 后端代理调用腾讯财经API
|
||||
import { getApiBaseUrl } from "@/lib/api-client";
|
||||
|
||||
export interface StockQuote {
|
||||
code: string;
|
||||
market: string;
|
||||
name: string;
|
||||
todayOpen: number;
|
||||
yesterdayClose: number;
|
||||
currentPrice: number;
|
||||
high: number;
|
||||
low: number;
|
||||
volume: number;
|
||||
amount: number;
|
||||
outerDisk: number; // 外盘(手)
|
||||
innerDisk: number; // 内盘(手)
|
||||
date: string;
|
||||
time: string;
|
||||
change: number;
|
||||
changePercent: number;
|
||||
}
|
||||
|
||||
export interface KLineData {
|
||||
date: string;
|
||||
open: number;
|
||||
close: number;
|
||||
high: number;
|
||||
low: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export interface StockSearchResult {
|
||||
code: string;
|
||||
name: string;
|
||||
market: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface FundFlowData {
|
||||
date: string;
|
||||
mainNetInflow: number; // 主力净流入
|
||||
superLargeInflow: number; // 超大单流入
|
||||
superLargeOutflow: number; // 超大单流出
|
||||
largeInflow: number; // 大单流入
|
||||
largeOutflow: number; // 大单流出
|
||||
mediumInflow: number; // 中单流入
|
||||
mediumOutflow: number; // 中单流出
|
||||
smallInflow: number; // 小单流入
|
||||
smallOutflow: number; // 小单流出
|
||||
mainNetInflowPercent: number; // 主力净流入占比
|
||||
superLargeInflowPercent: number; // 超大单流入占比
|
||||
superLargeOutflowPercent: number; // 超大单流出占比
|
||||
largeInflowPercent: number; // 大单流入占比
|
||||
largeOutflowPercent: number; // 大单流出占比
|
||||
mediumInflowPercent: number; // 中单流入占比
|
||||
mediumOutflowPercent: number; // 中单流出占比
|
||||
smallInflowPercent: number; // 小单流入占比
|
||||
smallOutflowPercent: number; // 小单流出占比
|
||||
turnover: number; // 成交额
|
||||
// 计算字段
|
||||
mainForceNet: number; // 主力净额(超大单+大单净流入)
|
||||
cumulativeMainNet: number; // 累计主力净流入
|
||||
changePercent: number; // 当日涨幅%
|
||||
closePrice: number; // 收盘价
|
||||
}
|
||||
|
||||
export interface FundFlowSummary {
|
||||
totalMainNet: number; // 期间主力总净流入
|
||||
totalTurnover: number; // 期间总成交额
|
||||
totalLargeInflow?: number; // 期间大单流入资金(≈主买资金)
|
||||
avgDailyMainNet: number; // 日均主力净流入
|
||||
positiveDays: number; // 主力净流入为正的天数
|
||||
negativeDays: number; // 主力净流入为负的天数
|
||||
}
|
||||
|
||||
export type StockBoard = "main" | "cyb" | "kcb" | "bjb";
|
||||
|
||||
export interface BoardInfo {
|
||||
board: StockBoard;
|
||||
label: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据股票代码判断所属板块
|
||||
* - 688开头:科创板(KCB)
|
||||
* - 300/301开头:创业板(CYB,301为注册制)
|
||||
* - 8/4/920开头:北交所(BJB)
|
||||
* - 其他:主板
|
||||
*/
|
||||
export function getStockBoard(code: string): BoardInfo {
|
||||
if (code.startsWith("688")) {
|
||||
return { board: "kcb", label: "科", className: "bg-red-500/10 text-red-500 border-red-500/30" };
|
||||
}
|
||||
// 300/301 开头均为创业板(301 为创业板注册制)
|
||||
if (code.startsWith("300") || code.startsWith("301")) {
|
||||
return { board: "cyb", label: "创", className: "bg-purple-500/10 text-purple-500 border-purple-500/30" };
|
||||
}
|
||||
if (code.startsWith("8") || code.startsWith("4") || code.startsWith("920")) {
|
||||
return { board: "bjb", label: "北", className: "bg-orange-500/10 text-orange-500 border-orange-500/30" };
|
||||
}
|
||||
return { board: "main", label: "", className: "" };
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索股票(支持名称/代码/拼音模糊匹配)
|
||||
*/
|
||||
export async function fetchStockSearch(keyword: string): Promise<StockSearchResult[]> {
|
||||
const trimmed = keyword.trim();
|
||||
if (!trimmed) return [];
|
||||
|
||||
const baseUrl = getApiBaseUrl();
|
||||
const url = `${baseUrl}/api/stock/search?keyword=${encodeURIComponent(trimmed)}`;
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, { method: "GET" });
|
||||
if (!resp.ok) {
|
||||
console.error("[stock-api] 搜索失败:", resp.status);
|
||||
return [];
|
||||
}
|
||||
const result = await resp.json();
|
||||
if (!result.data || !Array.isArray(result.data)) return [];
|
||||
return result.data as StockSearchResult[];
|
||||
} catch (err) {
|
||||
console.error("[stock-api] 搜索股票失败:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单只股票实时行情
|
||||
*/
|
||||
export async function fetchStockQuote(code: string): Promise<StockQuote | null> {
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
throw new Error("股票代码格式错误,需为6位数字");
|
||||
}
|
||||
|
||||
const baseUrl = getApiBaseUrl();
|
||||
const url = `${baseUrl}/api/stock/quote?code=${code}`;
|
||||
|
||||
console.log("[stock-api] 请求URL:", url);
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, { method: "GET" });
|
||||
|
||||
console.log("[stock-api] 响应状态:", resp.status);
|
||||
|
||||
if (!resp.ok) {
|
||||
let errorMsg = `请求失败 (${resp.status})`;
|
||||
try {
|
||||
const errData = await resp.json();
|
||||
errorMsg = errData.error || errorMsg;
|
||||
} catch {
|
||||
// 忽略 JSON 解析错误
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const result = await resp.json();
|
||||
console.log("[stock-api] 响应数据:", result);
|
||||
|
||||
if (!result.data) {
|
||||
throw new Error(result.error || "未获取到股票数据");
|
||||
}
|
||||
|
||||
return result.data as StockQuote;
|
||||
} catch (err) {
|
||||
console.error("[stock-api] 获取股票数据失败:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取股票历史K线数据(通过Edge Function代理腾讯财经API)
|
||||
* @param code 股票代码(6位)
|
||||
* @param days 天数(默认90天)
|
||||
*/
|
||||
export async function fetchStockHistory(code: string, days: number = 90): Promise<KLineData[]> {
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
throw new Error("股票代码格式错误,需为6位数字");
|
||||
}
|
||||
|
||||
const baseUrl = getApiBaseUrl();
|
||||
const url = `${baseUrl}/api/stock/history?code=${code}&days=${days}`;
|
||||
|
||||
console.log("[stock-api] 请求历史K线URL:", url);
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, { method: "GET" });
|
||||
|
||||
console.log("[stock-api] 历史K线响应状态:", resp.status);
|
||||
|
||||
if (!resp.ok) {
|
||||
let errorMsg = `请求失败 (${resp.status})`;
|
||||
try {
|
||||
const errData = await resp.json();
|
||||
errorMsg = errData.error || errorMsg;
|
||||
} catch {
|
||||
// 忽略 JSON 解析错误
|
||||
}
|
||||
console.error("[stock-api] 历史K线错误:", errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const result = await resp.json();
|
||||
console.log("[stock-api] 历史K线数据条数:", result.count);
|
||||
|
||||
if (!result.data || !Array.isArray(result.data)) {
|
||||
throw new Error(result.error || "未获取到历史数据");
|
||||
}
|
||||
|
||||
return result.data as KLineData[];
|
||||
} catch (err) {
|
||||
console.error("[stock-api] 获取历史K线数据失败:", err);
|
||||
// 降级方案:返回空数组,让前端使用模拟数据
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取多只股票行情(并行调用)
|
||||
*/
|
||||
export async function fetchStockQuotes(codes: string[]): Promise<Map<string, StockQuote>> {
|
||||
const map = new Map<string, StockQuote>();
|
||||
const results = await Promise.allSettled(
|
||||
codes.map((code) => fetchStockQuote(code))
|
||||
);
|
||||
|
||||
results.forEach((res, idx) => {
|
||||
if (res.status === "fulfilled" && res.value) {
|
||||
map.set(codes[idx], res.value);
|
||||
}
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取股票资金流向数据
|
||||
* @param code 股票代码(6位)
|
||||
* @param name 股票名称(可选,用于MX API查询)
|
||||
* @param days 天数(默认21个交易日)
|
||||
*/
|
||||
export async function fetchStockFundFlow(code: string, name?: string, days: number = 21): Promise<{ data: FundFlowData[]; summary: FundFlowSummary | null; count: number }> {
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
throw new Error("股票代码格式错误,需为6位数字");
|
||||
}
|
||||
|
||||
const baseUrl = getApiBaseUrl();
|
||||
const nameParam = name ? `&name=${encodeURIComponent(name)}` : '';
|
||||
const url = `${baseUrl}/api/stock/fund-flow?code=${code}${nameParam}&days=${days}`;
|
||||
|
||||
console.log("[stock-api] 请求资金流向URL:", url);
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, { method: "GET" });
|
||||
|
||||
console.log("[stock-api] 资金流向响应状态:", resp.status);
|
||||
|
||||
if (!resp.ok) {
|
||||
let errorMsg = `请求失败 (${resp.status})`;
|
||||
try {
|
||||
const errData = await resp.json();
|
||||
errorMsg = errData.error || errorMsg;
|
||||
} catch {
|
||||
// 忽略 JSON 解析错误
|
||||
}
|
||||
console.error("[stock-api] 资金流向错误:", errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const result = await resp.json();
|
||||
console.log("[stock-api] 资金流向响应:", JSON.stringify(result).substring(0, 500));
|
||||
console.log("[stock-api] 资金流向数据条数:", result.count);
|
||||
|
||||
if (!result.data || !Array.isArray(result.data)) {
|
||||
throw new Error(result.error || "未获取到资金流向数据");
|
||||
}
|
||||
|
||||
return {
|
||||
data: result.data as FundFlowData[],
|
||||
summary: result.summary || null,
|
||||
count: result.count || 0,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("[stock-api] 获取资金流向数据失败:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 板块数据 ----
|
||||
|
||||
export interface SectorItem {
|
||||
code: string;
|
||||
name: string;
|
||||
level: number | null;
|
||||
changePercent: number | null;
|
||||
changeAmount: number | null;
|
||||
mainNetInflow: number;
|
||||
mainNetInflowPercent: number;
|
||||
superLargeInflow: number;
|
||||
superLargeInflowPercent: number;
|
||||
turnover: number;
|
||||
smallNetInflow: number;
|
||||
}
|
||||
|
||||
export type SectorType = "industry" | "concept";
|
||||
|
||||
export interface SectorResponse {
|
||||
data: SectorItem[];
|
||||
count: number;
|
||||
type: SectorType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取东方财富板块列表(按主力净流入排序)
|
||||
* @param type industry=行业板块, concept=概念板块
|
||||
*/
|
||||
export async function fetchSectors(type: SectorType): Promise<SectorItem[]> {
|
||||
const baseUrl = getApiBaseUrl();
|
||||
const url = `${baseUrl}/api/sectors?type=${type}`;
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, { method: "GET" });
|
||||
if (!resp.ok) return [];
|
||||
const result: SectorResponse = await resp.json();
|
||||
return result.data || [];
|
||||
} catch (err) {
|
||||
console.error("[stock-api] 获取板块数据失败:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user