This commit is contained in:
Sakurasan
2026-07-05 21:29:17 +08:00
commit 013ebdaffe
150 changed files with 15614 additions and 0 deletions
+332
View File
@@ -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开头:创业板(CYB301为注册制)
* - 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 [];
}
}