Initial commit
This commit is contained in:
399
src/services/financialService.js
Normal file
399
src/services/financialService.js
Normal file
@@ -0,0 +1,399 @@
|
||||
// src/services/financialService.js
|
||||
/**
|
||||
* 完整的财务数据服务层
|
||||
* 对应Flask后端的所有财务API接口
|
||||
*/
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL;
|
||||
|
||||
const apiRequest = async (url, options = {}) => {
|
||||
try {
|
||||
console.log(`Making Financial API request to: ${API_BASE_URL}${url}`);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
credentials: 'include', // 包含 cookies,以便后端识别登录状态
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`Financial API request failed: ${response.status} - ${errorText}`);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`Financial API response from ${url}:`, data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Financial API request failed for ${url}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const financialService = {
|
||||
/**
|
||||
* 获取股票基本信息和最新财务摘要
|
||||
* @param {string} seccode - 股票代码
|
||||
*/
|
||||
getStockInfo: async (seccode) => {
|
||||
return await apiRequest(`/api/financial/stock-info/${seccode}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取完整的资产负债表数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} limit - 获取的报告期数量
|
||||
*/
|
||||
getBalanceSheet: async (seccode, limit = 12) => {
|
||||
return await apiRequest(`/api/financial/balance-sheet/${seccode}?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取完整的利润表数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} limit - 获取的报告期数量
|
||||
*/
|
||||
getIncomeStatement: async (seccode, limit = 12) => {
|
||||
return await apiRequest(`/api/financial/income-statement/${seccode}?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取完整的现金流量表数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} limit - 获取的报告期数量
|
||||
*/
|
||||
getCashflow: async (seccode, limit = 12) => {
|
||||
return await apiRequest(`/api/financial/cashflow/${seccode}?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取完整的财务指标数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} limit - 获取的报告期数量
|
||||
*/
|
||||
getFinancialMetrics: async (seccode, limit = 12) => {
|
||||
return await apiRequest(`/api/financial/financial-metrics/${seccode}?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取主营业务构成数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} periods - 获取的报告期数量
|
||||
*/
|
||||
getMainBusiness: async (seccode, periods = 4) => {
|
||||
return await apiRequest(`/api/financial/main-business/${seccode}?periods=${periods}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取业绩预告和预披露时间
|
||||
* @param {string} seccode - 股票代码
|
||||
*/
|
||||
getForecast: async (seccode) => {
|
||||
return await apiRequest(`/api/financial/forecast/${seccode}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取行业排名数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} limit - 获取的报告期数量
|
||||
*/
|
||||
getIndustryRank: async (seccode, limit = 4) => {
|
||||
return await apiRequest(`/api/financial/industry-rank/${seccode}?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取不同报告期的对比数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} periods - 对比的报告期数量
|
||||
*/
|
||||
getPeriodComparison: async (seccode, periods = 8) => {
|
||||
return await apiRequest(`/api/financial/comparison/${seccode}?periods=${periods}`);
|
||||
},
|
||||
};
|
||||
|
||||
// 数据格式化工具函数
|
||||
export const formatUtils = {
|
||||
/**
|
||||
* 格式化大数字(亿/万)
|
||||
* @param {number} num - 数字
|
||||
* @param {number} decimal - 小数位数
|
||||
*/
|
||||
formatLargeNumber: (num, decimal = 2) => {
|
||||
if (num === null || num === undefined) return '-';
|
||||
|
||||
const absNum = Math.abs(num);
|
||||
let result = '';
|
||||
|
||||
if (absNum >= 100000000) {
|
||||
result = (num / 100000000).toFixed(decimal) + '亿';
|
||||
} else if (absNum >= 10000) {
|
||||
result = (num / 10000).toFixed(decimal) + '万';
|
||||
} else if (absNum >= 1) {
|
||||
result = num.toFixed(decimal);
|
||||
} else {
|
||||
result = num.toFixed(4); // 小于1的数字显示更多小数位
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化百分比
|
||||
* @param {number} num - 数字
|
||||
* @param {number} decimal - 小数位数
|
||||
*/
|
||||
formatPercent: (num, decimal = 2) => {
|
||||
if (num === null || num === undefined) return '-';
|
||||
return num.toFixed(decimal) + '%';
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {string} dateStr - 日期字符串
|
||||
*/
|
||||
formatDate: (dateStr) => {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取报告期类型
|
||||
* @param {string} dateStr - 日期字符串
|
||||
*/
|
||||
getReportType: (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
const month = date.getMonth() + 1;
|
||||
const year = date.getFullYear();
|
||||
|
||||
switch(month) {
|
||||
case 3:
|
||||
return `${year}Q1`;
|
||||
case 6:
|
||||
return `${year}中报`;
|
||||
case 9:
|
||||
return `${year}Q3`;
|
||||
case 12:
|
||||
return `${year}年报`;
|
||||
default:
|
||||
return dateStr;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化增长率颜色(用于Chakra UI)
|
||||
* @param {number} value - 增长率数值
|
||||
*/
|
||||
getGrowthColor: (value) => {
|
||||
if (value === null || value === undefined) return 'gray.500';
|
||||
if (value > 0) return 'green.500';
|
||||
if (value < 0) return 'red.500';
|
||||
return 'gray.500';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取指标变化趋势图标
|
||||
* @param {number} current - 当前值
|
||||
* @param {number} previous - 之前的值
|
||||
*/
|
||||
getTrendIcon: (current, previous) => {
|
||||
if (!current || !previous) return 'stable';
|
||||
if (current > previous) return 'up';
|
||||
if (current < previous) return 'down';
|
||||
return 'stable';
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算同比增长率
|
||||
* @param {number} current - 当前值
|
||||
* @param {number} yearAgo - 去年同期值
|
||||
*/
|
||||
calculateYoY: (current, yearAgo) => {
|
||||
if (!current || !yearAgo) return null;
|
||||
return ((current - yearAgo) / Math.abs(yearAgo)) * 100;
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算环比增长率
|
||||
* @param {number} current - 当前值
|
||||
* @param {number} previous - 上期值
|
||||
*/
|
||||
calculateQoQ: (current, previous) => {
|
||||
if (!current || !previous) return null;
|
||||
return ((current - previous) / Math.abs(previous)) * 100;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取财务健康度评分
|
||||
* @param {object} metrics - 财务指标对象
|
||||
*/
|
||||
getFinancialHealthScore: (metrics) => {
|
||||
let score = 0;
|
||||
let factors = 0;
|
||||
|
||||
// ROE评分
|
||||
if (metrics.roe !== null && metrics.roe !== undefined) {
|
||||
factors++;
|
||||
if (metrics.roe > 20) score += 5;
|
||||
else if (metrics.roe > 15) score += 4;
|
||||
else if (metrics.roe > 10) score += 3;
|
||||
else if (metrics.roe > 5) score += 2;
|
||||
else if (metrics.roe > 0) score += 1;
|
||||
}
|
||||
|
||||
// 流动比率评分
|
||||
if (metrics.current_ratio !== null && metrics.current_ratio !== undefined) {
|
||||
factors++;
|
||||
if (metrics.current_ratio > 2) score += 5;
|
||||
else if (metrics.current_ratio > 1.5) score += 4;
|
||||
else if (metrics.current_ratio > 1) score += 3;
|
||||
else if (metrics.current_ratio > 0.75) score += 2;
|
||||
else score += 1;
|
||||
}
|
||||
|
||||
// 资产负债率评分
|
||||
if (metrics.debt_ratio !== null && metrics.debt_ratio !== undefined) {
|
||||
factors++;
|
||||
if (metrics.debt_ratio < 30) score += 5;
|
||||
else if (metrics.debt_ratio < 40) score += 4;
|
||||
else if (metrics.debt_ratio < 50) score += 3;
|
||||
else if (metrics.debt_ratio < 60) score += 2;
|
||||
else if (metrics.debt_ratio < 70) score += 1;
|
||||
}
|
||||
|
||||
// 营收增长率评分
|
||||
if (metrics.revenue_growth !== null && metrics.revenue_growth !== undefined) {
|
||||
factors++;
|
||||
if (metrics.revenue_growth > 30) score += 5;
|
||||
else if (metrics.revenue_growth > 20) score += 4;
|
||||
else if (metrics.revenue_growth > 10) score += 3;
|
||||
else if (metrics.revenue_growth > 5) score += 2;
|
||||
else if (metrics.revenue_growth > 0) score += 1;
|
||||
}
|
||||
|
||||
if (factors === 0) return null;
|
||||
|
||||
const avgScore = (score / factors) * 20; // 转换为100分制
|
||||
return {
|
||||
score: avgScore,
|
||||
level: avgScore >= 80 ? '优秀' : avgScore >= 60 ? '良好' : avgScore >= 40 ? '一般' : '较差',
|
||||
color: avgScore >= 80 ? 'green' : avgScore >= 60 ? 'blue' : avgScore >= 40 ? 'yellow' : 'red'
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化数据表格列配置
|
||||
* @param {string} type - 表格类型
|
||||
*/
|
||||
getTableColumns: (type) => {
|
||||
const columnConfigs = {
|
||||
balanceSheet: [
|
||||
{ key: 'period', label: '报告期', align: 'left' },
|
||||
{ key: 'total_assets', label: '总资产', align: 'right', format: 'largeNumber' },
|
||||
{ key: 'total_liabilities', label: '总负债', align: 'right', format: 'largeNumber' },
|
||||
{ key: 'total_equity', label: '股东权益', align: 'right', format: 'largeNumber' },
|
||||
{ key: 'current_ratio', label: '流动比率', align: 'right', format: 'decimal' },
|
||||
{ key: 'debt_ratio', label: '资产负债率', align: 'right', format: 'percent' },
|
||||
],
|
||||
incomeStatement: [
|
||||
{ key: 'period', label: '报告期', align: 'left' },
|
||||
{ key: 'revenue', label: '营业收入', align: 'right', format: 'largeNumber' },
|
||||
{ key: 'operating_profit', label: '营业利润', align: 'right', format: 'largeNumber' },
|
||||
{ key: 'net_profit', label: '净利润', align: 'right', format: 'largeNumber' },
|
||||
{ key: 'gross_margin', label: '毛利率', align: 'right', format: 'percent' },
|
||||
{ key: 'net_margin', label: '净利率', align: 'right', format: 'percent' },
|
||||
],
|
||||
cashflow: [
|
||||
{ key: 'period', label: '报告期', align: 'left' },
|
||||
{ key: 'operating_cashflow', label: '经营现金流', align: 'right', format: 'largeNumber' },
|
||||
{ key: 'investing_cashflow', label: '投资现金流', align: 'right', format: 'largeNumber' },
|
||||
{ key: 'financing_cashflow', label: '筹资现金流', align: 'right', format: 'largeNumber' },
|
||||
{ key: 'free_cashflow', label: '自由现金流', align: 'right', format: 'largeNumber' },
|
||||
],
|
||||
metrics: [
|
||||
{ key: 'period', label: '报告期', align: 'left' },
|
||||
{ key: 'roe', label: 'ROE', align: 'right', format: 'percent' },
|
||||
{ key: 'roa', label: 'ROA', align: 'right', format: 'percent' },
|
||||
{ key: 'eps', label: 'EPS', align: 'right', format: 'decimal' },
|
||||
{ key: 'pe', label: 'PE', align: 'right', format: 'decimal' },
|
||||
{ key: 'pb', label: 'PB', align: 'right', format: 'decimal' },
|
||||
]
|
||||
};
|
||||
|
||||
return columnConfigs[type] || [];
|
||||
}
|
||||
};
|
||||
|
||||
// 图表数据处理工具
|
||||
export const chartUtils = {
|
||||
/**
|
||||
* 准备趋势图数据
|
||||
* @param {array} data - 原始数据
|
||||
* @param {array} metrics - 要显示的指标
|
||||
*/
|
||||
prepareTrendData: (data, metrics) => {
|
||||
return data.map(item => {
|
||||
const point = {
|
||||
period: formatUtils.getReportType(item.period)
|
||||
};
|
||||
metrics.forEach(metric => {
|
||||
point[metric.key] = item[metric.dataPath];
|
||||
});
|
||||
return point;
|
||||
}).reverse(); // 按时间顺序排列
|
||||
},
|
||||
|
||||
/**
|
||||
* 准备饼图数据
|
||||
* @param {array} data - 原始数据
|
||||
* @param {string} valueKey - 值字段
|
||||
* @param {string} nameKey - 名称字段
|
||||
*/
|
||||
preparePieData: (data, valueKey, nameKey) => {
|
||||
const total = data.reduce((sum, item) => sum + (item[valueKey] || 0), 0);
|
||||
return data.map(item => ({
|
||||
name: item[nameKey],
|
||||
value: item[valueKey],
|
||||
percentage: total > 0 ? (item[valueKey] / total * 100).toFixed(2) : 0
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* 准备对比柱状图数据
|
||||
* @param {array} data - 原始数据
|
||||
* @param {array} periods - 要对比的期间
|
||||
* @param {array} metrics - 要对比的指标
|
||||
*/
|
||||
prepareComparisonData: (data, periods, metrics) => {
|
||||
return metrics.map(metric => ({
|
||||
metric: metric.label,
|
||||
...periods.reduce((acc, period, index) => {
|
||||
const periodData = data.find(d => d.period === period);
|
||||
acc[`period${index + 1}`] = periodData ? periodData[metric.key] : 0;
|
||||
return acc;
|
||||
}, {})
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取图表颜色配置
|
||||
* @param {string} theme - 主题类型
|
||||
*/
|
||||
getChartColors: (theme = 'default') => {
|
||||
const themes = {
|
||||
default: ['#3182CE', '#38A169', '#DD6B20', '#805AD5', '#D69E2E', '#E53E3E'],
|
||||
blue: ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D'],
|
||||
green: ['#38A169', '#48BB78', '#68D391', '#9AE6B4', '#C6F6D5', '#F0FFF4']
|
||||
};
|
||||
return themes[theme] || themes.default;
|
||||
}
|
||||
};
|
||||
|
||||
export default financialService;
|
||||
Reference in New Issue
Block a user