// 股票数据获取工具:通过 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; turnoverRate: number; // 换手率% pe: number; // 市盈率 pb: number; // 市净率 totalMarketCap: number; // 总市值 circulatingMarketCap: number; // 流通市值 } export interface KLineData { date: string; open: number; close: number; high: number; low: number; volume: number; changePercent?: number; turnover?: number; amplitude?: number; turnoverRate?: 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 { 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 { 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 { 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> { const map = new Map(); 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 CompanyProfile { code: string; name: string; fullName: string; englishName: string; boardType: string; industry: string; exchange: string; industryDetail: string; chairman: string; generalManager: string; secretary: string; phone: string; email: string; fax: string; website: string; officeAddress: string; registeredAddress: string; region: string; postalCode: string; registeredCapital: string; unifiedCreditCode: string; employees: string; managementCount: string; lawFirm: string; auditor: string; businessScope: string; companyProfile: string; establishmentDate: string; listingDate: string; listingPrice: string; issuePriceEarningsRatio: string; issueAmount: string; netProceeds: string; issueMethod: string; } /** * 获取公司概况 */ export async function fetchCompanyProfile(code: string): Promise { const baseUrl = getApiBaseUrl(); const url = `${baseUrl}/api/stock/profile?code=${code}`; try { const resp = await fetch(url); if (!resp.ok) return null; const result = await resp.json(); return result.data || null; } catch (err) { console.error("[stock-api] 获取公司概况失败:", err); return null; } } // ---- 经营分析(收入构成)---- export interface BusinessSegmentItem { itemName: string; income: number | null; cost: number | null; profit: number | null; grossRatio: number | null; incomeRatio: number | null; profitRatio: number | null; costRatio: number | null; } export interface BusinessSegmentPeriod { reportName: string; items: BusinessSegmentItem[]; } export interface BusinessSegmentsResponse { data: BusinessSegmentPeriod[]; count: number; code: string; name: string; byType: string; } type SegmentType = "product" | "industry" | "region"; /** * 获取主营构成(收入构成) */ export async function fetchBusinessSegments(code: string, byType: SegmentType = "product", years: number = 3): Promise { const baseUrl = getApiBaseUrl(); const url = `${baseUrl}/api/stock/business-segments?code=${code}&by_type=${byType}&years=${years}`; try { const resp = await fetch(url); if (!resp.ok) return null; return await resp.json(); } catch (err) { console.error("[stock-api] 获取主营构成失败:", err); return null; } } // ---- 财务指标 ---- export interface FinancialReportItem { reportDate: string; reportType: string; reportDateName: string; noticeDate: string; indicators: Record; } export interface FinancialDataResponse { data: FinancialReportItem[]; count: number; code: string; name: string; } /** * 获取财务指标数据 */ export async function fetchFinancialData(code: string, years: number = 5): Promise { const baseUrl = getApiBaseUrl(); const url = `${baseUrl}/api/stock/financial?code=${code}&years=${years}`; try { const resp = await fetch(url); if (!resp.ok) return null; return await resp.json(); } catch (err) { console.error("[stock-api] 获取财务数据失败:", err); return null; } } // ---- 板块数据 ---- export interface SectorItem { code: string; name: string; level: number | null; changePercent: number | null; changeAmount: number | null; mainNetInflow: number; mainNetInflowPercent: number | null; superLargeInflow: number | null; superLargeInflowPercent: number | null; largeInflow: number | null; largeInflowPercent: number | null; mediumInflow: number | null; mediumInflowPercent: number | null; smallInflow: number | null; smallInflowPercent: number | null; turnover: 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, signal?: AbortSignal): Promise { const baseUrl = getApiBaseUrl(); const url = `${baseUrl}/api/sectors?type=${type}`; try { const resp = await fetch(url, { method: "GET", signal, cache: "no-store" }); if (!resp.ok) return []; const result: SectorResponse = await resp.json(); return result.data || []; } catch (err) { console.error("[stock-api] 获取板块数据失败:", err); return []; } }