Files
auv/src/lib/stock-api.ts
T

480 lines
13 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 股票数据获取工具:通过 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开头:创业板(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 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<CompanyProfile | null> {
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<BusinessSegmentsResponse | null> {
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<string, number | string | boolean>;
}
export interface FinancialDataResponse {
data: FinancialReportItem[];
count: number;
code: string;
name: string;
}
/**
* 获取财务指标数据
*/
export async function fetchFinancialData(code: string, years: number = 5): Promise<FinancialDataResponse | null> {
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<SectorItem[]> {
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 [];
}
}