Initial commit

This commit is contained in:
2025-10-11 11:55:25 +08:00
parent 467dad8449
commit 8107dee8d3
2879 changed files with 610575 additions and 0 deletions

View File

@@ -0,0 +1,495 @@
// src/services/marketService.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 Market 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(`Market API request failed: ${response.status} - ${errorText}`);
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(`Market API response from ${url}:`, data);
return data;
} catch (error) {
console.error(`Market API request failed for ${url}:`, error);
throw error;
}
};
export const marketService = {
/**
* 获取股票交易数据日K线
* @param {string} seccode - 股票代码
* @param {number} days - 获取天数
* @param {string} end_date - 截止日期
*/
getTradeData: async (seccode, days = 60, end_date = null) => {
let url = `/api/market/trade/${seccode}?days=${days}`;
if (end_date) {
url += `&end_date=${end_date}`;
}
return await apiRequest(url);
},
/**
* 获取融资融券数据
* @param {string} seccode - 股票代码
* @param {number} days - 获取天数
*/
getFundingData: async (seccode, days = 30) => {
return await apiRequest(`/api/market/funding/${seccode}?days=${days}`);
},
/**
* 获取大宗交易数据
* @param {string} seccode - 股票代码
* @param {number} days - 获取天数
*/
getBigDealData: async (seccode, days = 30) => {
return await apiRequest(`/api/market/bigdeal/${seccode}?days=${days}`);
},
/**
* 获取龙虎榜数据
* @param {string} seccode - 股票代码
* @param {number} days - 获取天数
*/
getUnusualData: async (seccode, days = 30) => {
return await apiRequest(`/api/market/unusual/${seccode}?days=${days}`);
},
/**
* 获取股权质押数据
* @param {string} seccode - 股票代码
*/
getPledgeData: async (seccode) => {
return await apiRequest(`/api/market/pledge/${seccode}`);
},
/**
* 获取市场汇总数据
* @param {string} seccode - 股票代码
*/
getMarketSummary: async (seccode) => {
return await apiRequest(`/api/market/summary/${seccode}`);
},
/**
* 批量获取多只股票数据
* @param {array} seccodes - 股票代码数组
*/
getBatchMarketData: async (seccodes) => {
const promises = seccodes.map(code => marketService.getMarketSummary(code));
const results = await Promise.allSettled(promises);
return results.map((result, index) => {
if (result.status === 'fulfilled' && result.value.success) {
return result.value.data;
}
return {
stock_code: seccodes[index],
error: true,
message: result.reason?.message || 'Failed to fetch data'
};
});
},
};
// 数据格式化工具函数与financialService保持一致的风格
export const marketFormatUtils = {
/**
* 格式化大数字(亿/万)
* @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);
}
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 {number} volume - 成交量
*/
formatVolume: (volume) => {
if (!volume) return '-';
if (volume >= 100000000) {
return (volume / 100000000).toFixed(2) + '亿股';
} else if (volume >= 10000) {
return (volume / 10000).toFixed(2) + '万股';
}
return volume.toFixed(0) + '股';
},
/**
* 格式化成交额
* @param {number} amount - 成交额
*/
formatAmount: (amount) => {
if (!amount) return '-';
return '¥' + marketFormatUtils.formatLargeNumber(amount);
},
/**
* 获取涨跌颜色(中国市场:红涨绿跌)
* @param {number} value - 涨跌值
*/
getPriceColor: (value) => {
if (value === null || value === undefined) return 'gray.500';
if (value > 0) return 'red.500';
if (value < 0) return 'green.500';
return 'gray.500';
},
/**
* 格式化涨跌幅
* @param {number} change - 涨跌幅
*/
formatChange: (change) => {
if (change === null || change === undefined) return '-';
const formatted = change.toFixed(2) + '%';
return change >= 0 ? '+' + formatted : formatted;
},
/**
* 格式化换手率
* @param {number} turnover - 换手率
*/
formatTurnover: (turnover) => {
if (!turnover) return '-';
return turnover.toFixed(2) + '%';
},
/**
* 获取融资融券状态
* @param {object} funding - 融资融券数据
*/
getFundingStatus: (funding) => {
if (!funding) return { status: 'unknown', label: '未知' };
const netFinancing = funding.financing.buy - funding.financing.repay;
const netSecurities = funding.securities.sell - funding.securities.repay;
if (netFinancing > 0 && netSecurities <= 0) {
return { status: 'bullish', label: '看多', color: 'red.500' };
} else if (netFinancing <= 0 && netSecurities > 0) {
return { status: 'bearish', label: '看空', color: 'green.500' };
} else if (netFinancing > 0 && netSecurities > 0) {
return { status: 'divergent', label: '分歧', color: 'yellow.500' };
} else {
return { status: 'neutral', label: '中性', color: 'gray.500' };
}
},
/**
* 计算技术指标
* @param {array} data - K线数据
*/
calculateTechnicalIndicators: (data) => {
// 计算MA均线
const calculateMA = (period) => {
return data.map((item, index) => {
if (index < period - 1) return null;
const sum = data.slice(index - period + 1, index + 1)
.reduce((acc, cur) => acc + cur.close, 0);
return sum / period;
});
};
// 计算RSI
const calculateRSI = (period = 14) => {
const changes = data.map((item, index) => {
if (index === 0) return null;
return item.close - data[index - 1].close;
});
let avgGain = 0;
let avgLoss = 0;
for (let i = 1; i <= period; i++) {
if (changes[i] > 0) avgGain += changes[i];
else avgLoss += Math.abs(changes[i]);
}
avgGain /= period;
avgLoss /= period;
const rsi = [null];
for (let i = 1; i < period; i++) {
rsi.push(null);
}
for (let i = period; i < data.length; i++) {
const change = changes[i];
if (change > 0) {
avgGain = (avgGain * (period - 1) + change) / period;
avgLoss = (avgLoss * (period - 1)) / period;
} else {
avgGain = (avgGain * (period - 1)) / period;
avgLoss = (avgLoss * (period - 1) + Math.abs(change)) / period;
}
const rs = avgGain / avgLoss;
rsi.push(100 - (100 / (1 + rs)));
}
return rsi;
};
return {
ma5: calculateMA(5),
ma10: calculateMA(10),
ma20: calculateMA(20),
ma60: calculateMA(60),
rsi: calculateRSI(14)
};
},
/**
* 获取大宗交易评级
* @param {number} amount - 交易金额
* @param {number} avgAmount - 平均交易金额
*/
getBigDealRating: (amount, avgAmount) => {
if (!amount || !avgAmount) return { level: 'normal', label: '普通' };
const ratio = amount / avgAmount;
if (ratio >= 3) return { level: 'huge', label: '超大', color: 'red.600' };
if (ratio >= 2) return { level: 'large', label: '大额', color: 'orange.500' };
if (ratio >= 1.5) return { level: 'medium', label: '中等', color: 'yellow.500' };
return { level: 'normal', label: '普通', color: 'gray.500' };
},
};
// 图表数据处理工具
export const marketChartUtils = {
/**
* 准备K线图数据
* @param {array} data - 原始交易数据
*/
prepareKLineData: (data) => {
return data.map(item => ({
date: item.date.substring(5, 10), // MM-DD格式
fullDate: item.date,
open: item.open,
high: item.high,
low: item.low,
close: item.close,
volume: item.volume,
amount: item.amount,
change: item.change_percent,
k: [item.open, item.close, item.low, item.high], // K线数据格式
color: item.change_percent >= 0 ? '#ef4444' : '#10b981' // 红涨绿跌
}));
},
/**
* 准备融资融券趋势图数据
* @param {array} data - 原始融资融券数据
*/
prepareFundingTrendData: (data) => {
return data.map(item => ({
date: item.date.substring(5, 10),
fullDate: item.date,
financing: item.financing.balance / 100000000, // 转换为亿
securities: item.securities.balance_amount / 100000000,
total: item.total_balance / 100000000,
netBuy: (item.financing.buy - item.financing.repay) / 10000, // 转换为万
netSell: (item.securities.sell - item.securities.repay) / 10000
}));
},
/**
* 准备龙虎榜资金流向图数据
* @param {array} data - 龙虎榜数据
*/
prepareUnusualFlowData: (data) => {
const flowData = {};
data.grouped_data.forEach(dayData => {
flowData[dayData.date] = {
date: dayData.date,
totalBuy: dayData.total_buy / 10000, // 转换为万
totalSell: dayData.total_sell / 10000,
netFlow: dayData.net_amount / 10000,
buyersCount: dayData.buyers.length,
sellersCount: dayData.sellers.length
};
});
return Object.values(flowData).sort((a, b) =>
new Date(a.date) - new Date(b.date)
);
},
/**
* 准备股权质押趋势图数据
* @param {array} data - 原始质押数据
*/
preparePledgeTrendData: (data) => {
return data.map(item => ({
date: item.end_date.substring(0, 10),
pledgeRatio: item.pledge_ratio,
pledgeCount: item.pledge_count,
totalPledge: item.total_pledge / 10000, // 转换为万股
unrestricted: item.unrestricted_pledge / 10000,
restricted: item.restricted_pledge / 10000
})).reverse(); // 按时间正序排列
},
/**
* 获取图表配色方案
* @param {string} theme - 主题名称
*/
getChartTheme: (theme = 'default') => {
const themes = {
default: {
upColor: '#ef4444', // 红色上涨
downColor: '#10b981', // 绿色下跌
volumeUpColor: 'rgba(239, 68, 68, 0.7)',
volumeDownColor: 'rgba(16, 185, 129, 0.7)',
ma5Color: '#3b82f6',
ma10Color: '#8b5cf6',
ma20Color: '#f59e0b',
ma60Color: '#ec4899',
gridColor: '#e5e7eb'
},
dark: {
upColor: '#ef4444',
downColor: '#10b981',
volumeUpColor: 'rgba(239, 68, 68, 0.5)',
volumeDownColor: 'rgba(16, 185, 129, 0.5)',
ma5Color: '#60a5fa',
ma10Color: '#a78bfa',
ma20Color: '#fbbf24',
ma60Color: '#f472b6',
gridColor: '#374151'
}
};
return themes[theme] || themes.default;
},
};
// 数据缓存管理
export const marketCacheManager = {
cache: new Map(),
/**
* 生成缓存键
* @param {string} stockCode - 股票代码
* @param {string} dataType - 数据类型
* @param {object} params - 查询参数
*/
generateKey: (stockCode, dataType, params = {}) => {
return `market_${stockCode}_${dataType}_${JSON.stringify(params)}`;
},
/**
* 获取缓存数据
* @param {string} key - 缓存键
* @param {number} maxAge - 最大缓存时间(毫秒)
*/
get: function(key, maxAge = 5 * 60 * 1000) {
const cached = this.cache.get(key);
if (cached) {
const { data, timestamp } = cached;
if (Date.now() - timestamp < maxAge) {
console.log(`Cache hit for key: ${key}`);
return data;
}
this.cache.delete(key);
}
return null;
},
/**
* 设置缓存数据
* @param {string} key - 缓存键
* @param {any} data - 要缓存的数据
*/
set: function(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
console.log(`Data cached for key: ${key}`);
},
/**
* 清除所有缓存
*/
clear: function() {
this.cache.clear();
console.log('All market cache cleared');
},
/**
* 清除特定股票的缓存
* @param {string} stockCode - 股票代码
*/
clearStock: function(stockCode) {
for (const key of this.cache.keys()) {
if (key.includes(`market_${stockCode}_`)) {
this.cache.delete(key);
}
}
console.log(`Cache cleared for stock: ${stockCode}`);
}
};
export default marketService;