Initial commit
This commit is contained in:
495
src/services/marketService.js
Normal file
495
src/services/marketService.js
Normal 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;
|
||||
Reference in New Issue
Block a user