Fundamental_Analysis/frontend/src/lib/financial-utils.ts

326 lines
9.1 KiB
TypeScript
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.

/**
* 财务数据处理工具函数
* 包含股票代码规范化、数据格式化等通用功能
*/
import type { MarketType, CompanyInfo, FinancialMetricConfig } from '@/types';
// ============================================================================
// 股票代码处理
// ============================================================================
/**
* 规范化A股代码为ts_code格式
* @param input - 输入的股票代码
* @returns 规范化后的ts_code如果无效则返回null
*
* @example
* ```typescript
* normalizeTsCode('600519') // '600519.SH'
* normalizeTsCode('000001') // '000001.SZ'
* normalizeTsCode('600519.SH') // '600519.SH'
* normalizeTsCode('invalid') // null
* ```
*/
export function normalizeTsCode(input: string): string | null {
const code = (input || "").trim();
if (!code) return null;
// 仅支持数字代码的简易规则6开头上证0/3开头深证
if (/^\d{6}$/.test(code)) {
if (code.startsWith("6")) return `${code}.SH`;
if (code.startsWith("0") || code.startsWith("3")) return `${code}.SZ`;
}
// 若已含交易所后缀则直接返回
if (/^\d{6}\.(SH|SZ)$/i.test(code)) return code.toUpperCase();
return null;
}
/**
* 验证股票代码是否有效
* @param code - 股票代码
* @returns 是否为有效的股票代码
*/
export function isValidTsCode(code: string): boolean {
return normalizeTsCode(code) !== null;
}
/**
* 从输入中提取股票代码
* @param input - 可能包含股票代码和公司名的输入
* @returns 提取的股票代码如果没有找到则返回null
*/
export function extractTsCode(input: string): string | null {
const trimmed = input.trim();
// 尝试从输入开头提取代码
const codeMatch = trimmed.match(/^(\d{6}(?:\.[A-Z]{2})?)/);
if (codeMatch) {
return normalizeTsCode(codeMatch[1]);
}
return null;
}
// ============================================================================
// 数据格式化
// ============================================================================
/**
* 格式化财务数值显示
* @param value - 原始数值
* @param group - 指标分组
* @param api - API接口名
* @param metricKey - 指标键名
* @returns 格式化后的字符串
*/
export function formatFinancialValue(
value: number | null | undefined,
group?: string,
api?: string,
metricKey?: string
): string {
if (value === null || typeof value === "undefined") return "-";
const num = Number(value);
if (Number.isNaN(num)) return "-";
const nf1 = new Intl.NumberFormat("zh-CN", {
minimumFractionDigits: 1,
maximumFractionDigits: 1
});
const nf0 = new Intl.NumberFormat("zh-CN", {
maximumFractionDigits: 0
});
// 报表类统一按亿元显示1位小数千分位
if (group === "income" || group === "balancesheet" || group === "cashflow" || metricKey === "revenue") {
const scaled = num / 1e8;
return nf1.format(scaled);
}
// 市值 total_mvdaily_basic万元-> 亿元,整数(千分位)
if (api === "daily_basic" && metricKey === "total_mv") {
const scaledYi = num / 1e4;
return nf0.format(Math.round(scaledYi));
}
// 员工数 / 股东数 -> 整数(千分位),不做单位换算
if (metricKey === "employees" || metricKey === "holder_num") {
return nf0.format(Math.round(num));
}
// 其他数值 -> 1位小数 + 千分位
return nf1.format(num);
}
/**
* 获取指标单位文本
* @param group - 指标分组
* @param api - API接口名
* @param metricKey - 指标键名
* @returns 单位文本,如果不需要显示单位则返回空字符串
*/
export function getMetricUnit(group?: string, api?: string, metricKey?: string): string {
// 报表类和市值显示亿元单位
if (
group === "income" ||
group === "balancesheet" ||
group === "cashflow" ||
metricKey === "total_mv"
) {
return "(亿元)";
}
return "";
}
// ============================================================================
// 配置处理
// ============================================================================
/**
* 扁平化API分组配置
* @param apiGroups - API分组配置对象
* @param groupOrder - 分组顺序数组
* @returns 扁平化的配置项数组和映射对象
*/
export function flattenApiGroups(
apiGroups: Record<string, FinancialMetricConfig[]>,
groupOrder: string[] = ["income", "fina_indicator", "balancesheet", "cashflow", "daily_basic", "daily", "unknown"]
): {
items: FinancialMetricConfig[];
groupMap: Record<string, string>;
apiMap: Record<string, string>;
} {
const items: FinancialMetricConfig[] = [];
const groupMap: Record<string, string> = {};
const apiMap: Record<string, string> = {};
for (const group of groupOrder) {
const arr = Array.isArray(apiGroups[group]) ? apiGroups[group] : [];
for (const item of arr) {
const param = (item?.tushareParam || "").trim();
if (!param) continue;
const api = (item?.api || "").trim();
items.push({
displayText: item.displayText,
tushareParam: param,
api,
group
});
groupMap[param] = group;
apiMap[param] = api;
}
}
return { items, groupMap, apiMap };
}
// ============================================================================
// 错误处理
// ============================================================================
/**
* 增强错误信息
* @param error - 原始错误
* @returns 增强后的错误信息
*/
export function enhanceErrorMessage(error: unknown): string {
const errorMsg = error instanceof Error ? error.message : "未知错误";
// 网络相关错误
if (errorMsg.includes('fetch') || errorMsg.includes('NetworkError')) {
return "网络连接失败,请检查后端服务是否已启动或网络连接是否正常。";
}
if (errorMsg.includes('timeout')) {
return "请求超时,请稍后重试。";
}
// 数据相关错误
if (errorMsg.includes('未查询到数据')) {
return "未查询到数据,请确认代码或稍后重试。";
}
// 服务相关错误
if (errorMsg.includes('500') || errorMsg.includes('Internal Server Error')) {
return "服务器内部错误,请稍后重试或联系管理员。";
}
if (errorMsg.includes('404') || errorMsg.includes('Not Found')) {
return "请求的资源不存在,请检查输入参数。";
}
// 默认错误处理
if (!errorMsg || errorMsg === "未知错误") {
return "查询失败,请检查后端服务是否已启动。";
}
return errorMsg;
}
/**
* 判断错误是否可重试
* @param error - 错误对象或错误信息
* @returns 是否可重试
*/
export function isRetryableError(error: unknown): boolean {
const errorMsg = error instanceof Error ? error.message : String(error);
return (
errorMsg.includes('网络') ||
errorMsg.includes('连接') ||
errorMsg.includes('超时') ||
errorMsg.includes('timeout') ||
errorMsg.includes('fetch') ||
errorMsg.includes('NetworkError')
);
}
// ============================================================================
// 数据验证
// ============================================================================
/**
* 验证市场类型是否支持
* @param market - 市场类型
* @returns 是否支持该市场
*/
export function isSupportedMarket(market: string): market is MarketType {
return ['cn', 'us', 'hk', 'jp', 'other'].includes(market);
}
/**
* 验证公司信息是否有效
* @param company - 公司信息对象
* @returns 是否为有效的公司信息
*/
export function isValidCompanyInfo(company: unknown): company is CompanyInfo {
return (
typeof company === 'object' &&
company !== null &&
typeof (company as CompanyInfo).ts_code === 'string' &&
isValidTsCode((company as CompanyInfo).ts_code)
);
}
// ============================================================================
// 本地存储工具
// ============================================================================
/**
* 检查localStorage是否可用
* @returns localStorage是否可用
*/
export function isLocalStorageAvailable(): boolean {
try {
const testKey = '__localStorage_test__';
localStorage.setItem(testKey, 'test');
localStorage.removeItem(testKey);
return true;
} catch {
return false;
}
}
/**
* 安全地从localStorage获取数据
* @param key - 存储键
* @param defaultValue - 默认值
* @returns 存储的数据或默认值
*/
export function safeGetFromStorage<T>(key: string, defaultValue: T): T {
if (!isLocalStorageAvailable()) {
return defaultValue;
}
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
} catch {
return defaultValue;
}
}
/**
* 安全地向localStorage保存数据
* @param key - 存储键
* @param value - 要保存的数据
* @returns 是否保存成功
*/
export function safeSetToStorage(key: string, value: unknown): boolean {
if (!isLocalStorageAvailable()) {
return false;
}
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch {
return false;
}
}