You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

783 lines
36 KiB

// 市场数据服务
import { DataSourceFactory, DataSourceType } from './datasource/DataSourceFactory';
import { futuresList, generateFuturesOverview, generateFutureData, generateKlineData, riskAlerts } from '../utils/mockData';
import { config } from '../config';
import { serviceImplementationClient } from './ServiceImplementationClient';
import { logger } from '../utils/logger';
import { cacheService } from './cacheService';
// 获取数据源配置
const getDataSourceConfig = () => {
// 从配置文件中读取数据源配置
return config.dataSource;
};
// 获取市场概览
export const fetchMarketOverview = async () => {
const cacheKey = 'market:overview';
const symbol = 'market';
const type = 'overview';
return cacheService.get(cacheKey, symbol, type, async () => {
try {
// 首先尝试使用 service_implementation API
try {
logger.log('尝试使用 service_implementation API 获取市场概览...');
// 先获取合约列表
logger.log('获取合约列表...');
const contractsResponse = await serviceImplementationClient.getContracts();
const allContracts = contractsResponse.data;
// 去重,按品种代码分组
const uniqueContracts = new Map();
for (const contract of allContracts) {
// 提取品种代码如从CU2603中提取CU
const code = contract.symbol.slice(0, 2);
if (!uniqueContracts.has(code)) {
uniqueContracts.set(code, {
code: code,
name: contract.name || code,
exchange: contract.exchange || ''
});
}
}
// 转换为数组
const contractList = Array.from(uniqueContracts.values());
logger.log(`获取到 ${contractList.length} 个独特品种,分别是: ${contractList.map(c => c.code).join(', ')}`);
// 使用获取到的合约列表
const overview = [];
for (const future of contractList) {
try {
// 构建合约符号(使用大写代码,因为 service_implementation API 期望大写)
const symbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}03`;
// 获取合约详情
logger.log(`获取合约${symbol}详情...`);
const contractsResponse = await serviceImplementationClient.getContracts(future.exchange, future.code);
const contract = contractsResponse.data.find((c: any) => c.symbol === symbol);
if (!contract) {
logger.warn(`合约${symbol}不存在,跳过`);
continue;
}
// 更新未来合约的中文名称
future.name = `${contract.product_name || future.name}`;
// 获取分析数据
logger.log(`分析合约${symbol}...`);
logger.log(`before analyzeMarket: symbol=${symbol}time=${new Date().toISOString()}`);
const analysisResponse = await serviceImplementationClient.analyzeMarket(symbol);
const analysis = analysisResponse.data;
logger.log(`after analyzeMarket: symbol=${symbol}, analysis=${JSON.stringify(analysis)}, time=${new Date().toISOString()}`);
overview.push({
code: future.code,
name: future.name,
currentPrice: analysis.current_price || 0,
changePercent: analysis.change_percent || 0,
winRate: analysis.probability ? Math.round(analysis.probability * 100) : Math.floor(Math.random() * 50) + 30, // 使用分析结果或模拟胜率
atr: analysis.atr || +(Math.random() * 5 + 0.5).toFixed(2), // 使用分析结果或模拟ATR
adx: analysis.adx || Math.floor(Math.random() * 60) + 10, // 使用分析结果或模拟ADX
adxStatus: (adx: number) => {
if (adx < 20) return '无趋势/震荡';
if (adx < 40) return '弱趋势';
return '强趋势';
},
trends: {
'5MIN': {
direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望',
status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡',
rsi: Math.floor(Math.random() * 80) + 10
},
'30MIN': {
direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望',
status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡',
rsi: Math.floor(Math.random() * 80) + 10
},
'1HOUR': {
direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望',
status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡',
rsi: Math.floor(Math.random() * 80) + 10
},
'1DAY': {
direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望',
status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡',
rsi: Math.floor(Math.random() * 80) + 10
}
},
tradingAdvice: {
entry: analysis.entry_price || 0,
stopLoss: analysis.stop_loss || 0,
target: analysis.target_price || 0,
resistance: analysis.resistance || 0,
support: analysis.support || 0
},
overallView: analysis.trend === 'up' ? '多头排列' : analysis.trend === 'down' ? '空头排列' : '震荡',
aiAnalysis: `趋势:${analysis.trend || '中性'} | 概率:${analysis.probability ? Math.round(analysis.probability * 100) : 50}% | 方向:${analysis.direction || '观望'}`
});
} catch (error) {
logger.error(`获取合约${future.code}行情失败:`, error);
// 跳过获取失败的合约
continue;
}
}
if (overview.length > 0) {
logger.log('使用 service_implementation API 获取市场概览成功');
return overview;
}
logger.warn('service_implementation API 未返回数据,尝试使用其他数据源');
} catch (error) {
logger.error('service_implementation API 获取失败:', error);
// service_implementation API 失败,尝试使用其他数据源
}
// 获取数据源配置
const dataSourceConfig = getDataSourceConfig();
logger.log('获取数据源配置:', dataSourceConfig);
// 检查是否有可用的数据源
const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
// 尝试使用TQSDK数据源
if (dataSourceConfig.tqsdk?.enabled) {
try {
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
// 使用用户指定的合约列表
const overview = [];
for (const future of futuresList) {
try {
// 构建合约符号使用小写代码因为TQAPI期望小写
const symbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`;
// 获取合约详情和实时行情
const tick = await dataSource.getTickData(symbol);
overview.push({
code: future.code,
name: future.name,
currentPrice: tick.last_price,
changePercent: tick.price_change / tick.pre_close * 100,
winRate: Math.floor(Math.random() * 50) + 30, // 模拟胜率
atr: +(Math.random() * 5 + 0.5).toFixed(2), // 模拟ATR
adx: Math.floor(Math.random() * 60) + 10, // 模拟ADX
adxStatus: (adx: number) => {
if (adx < 20) return '无趋势/震荡';
if (adx < 40) return '弱趋势';
return '强趋势';
},
trends: {
'5MIN': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'30MIN': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'1HOUR': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'1DAY': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
}
},
tradingAdvice: {
entry: tick.last_price,
stopLoss: tick.last_price * (1 - 0.02 * (Math.random() + 0.5)),
target: tick.last_price * (1 + 0.03 * (Math.random() + 0.5)),
resistance: tick.last_price * (1 + 0.05 * (Math.random() + 0.5)),
support: tick.last_price * (1 - 0.05 * (Math.random() + 0.5))
},
overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)],
aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近`
});
} catch (error) {
logger.error(`获取合约${future.code}行情失败:`, error);
// 跳过获取失败的合约
continue;
}
}
return overview;
} catch (error) {
logger.error('TQSDK数据源获取失败:', error);
// TQSDK数据源失败尝试使用测试数据源
if (dataSourceConfig.test?.enabled) {
logger.log('切换到测试数据源');
// 启用了测试数据源,使用测试数据
await new Promise(resolve => setTimeout(resolve, 300));
return generateFuturesOverview();
} else {
// 未启用测试数据源,返回友好的错误提示
throw new Error('获取市场概览失败,所有数据源均不可用');
}
}
} else if (dataSourceConfig.test?.enabled) {
// 直接使用测试数据源
logger.log('使用测试数据源');
await new Promise(resolve => setTimeout(resolve, 300));
return generateFuturesOverview();
} else {
// 无可用数据源
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
} catch (error) {
logger.error('获取市场概览失败:', error);
// 直接返回友好的错误提示
throw new Error(error instanceof Error ? error.message : '获取市场概览失败,请检查数据源配置');
}
}, { expireTime: 300 }); // 缓存5分钟
};
// 获取品种详情
export const fetchMarketDetail = async (symbol: string) => {
const cacheKey = `market:detail:${symbol}`;
const type = 'detail';
return cacheService.get(cacheKey, symbol, type, async () => {
try {
// 提取合约代码从SHFE.AU2605中提取AU
const code = symbol.includes('.') ? symbol.split('.')[1].slice(0, 2) : symbol;
// 查找合约信息
const future = futuresList.find(item => item.code === code);
if (!future) {
throw new Error('品种不存在');
}
// 首先尝试使用 service_implementation API
try {
logger.log('尝试使用 service_implementation API 获取品种详情...');
// 构建合约符号(使用大写代码,因为 service_implementation API 期望大写)
const contractSymbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}03`;
// 获取合约详情
logger.log(`获取合约${contractSymbol}详情...`);
const contractsResponse = await serviceImplementationClient.getContracts(future.exchange, future.code);
const contract = contractsResponse.data.find((c: any) => c.symbol === contractSymbol);
if (!contract) {
logger.warn(`合约${contractSymbol}不存在,尝试使用其他数据源`);
} else {
// 更新未来合约的中文名称
future.name = `${contract.product_name || future.name} - ${future.code}`;
// 获取分析数据
logger.log(`分析合约${contractSymbol}...`);
logger.log(`before analyzeMarket: symbol=${contractSymbol}time=${new Date().toISOString()}`);
const analysisResponse = await serviceImplementationClient.analyzeMarket(contractSymbol);
const analysis = analysisResponse.data;
logger.log(`after analyzeMarket: symbol=${contractSymbol}time=${new Date().toISOString()}`);
// 获取交易建议
logger.log(`获取合约${contractSymbol}交易建议...`);
const recommendationsResponse = await serviceImplementationClient.getRecommendations(contractSymbol);
const recommendation = recommendationsResponse.data[0];
// 转换为前端需要的格式
const result = {
code: future.code,
name: future.name,
fullName: `${future.name}-${future.code}605`,
currentPrice: analysis.current_price || 0,
changePercent: analysis.change_percent || 0,
winRate: analysis.probability ? Math.round(analysis.probability * 100) : Math.floor(Math.random() * 50) + 30,
atr: analysis.atr || +(Math.random() * 5 + 0.5).toFixed(2),
adx: analysis.adx || Math.floor(Math.random() * 60) + 10,
adxStatus: (adx: number) => {
if (adx < 20) return '无趋势/震荡';
if (adx < 40) return '弱趋势';
return '强趋势';
},
trends: {
'5MIN': {
direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望',
status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡',
rsi: Math.floor(Math.random() * 80) + 10
},
'30MIN': {
direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望',
status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡',
rsi: Math.floor(Math.random() * 80) + 10
},
'1HOUR': {
direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望',
status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡',
rsi: Math.floor(Math.random() * 80) + 10
},
'1DAY': {
direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望',
status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡',
rsi: Math.floor(Math.random() * 80) + 10
}
},
indicators: {
macd: analysis.trend === 'up' ? '金叉向上' : analysis.trend === 'down' ? '死叉向下' : '走平',
rsi: `${Math.floor(Math.random() * 80) + 10}(中性)`,
bollinger: ['触及上轨', '触及下轨', '中轨附近'][Math.floor(Math.random() * 3)],
kdj: analysis.trend === 'up' ? '金叉向上' : analysis.trend === 'down' ? '死叉向下' : '走平'
},
tradingAdvice: {
entry: analysis.entry_price || recommendation?.entry_price || 0,
stopLoss: analysis.stop_loss || recommendation?.stop_loss || 0,
target: analysis.target_price || recommendation?.target_price || 0,
resistance: analysis.resistance || 0,
support: analysis.support || 0
},
riskLevel: analysis.risk_level || ['低', '中等', '高'][Math.floor(Math.random() * 3)],
volatility: ['低', '中等', '高'][Math.floor(Math.random() * 3)],
overallView: analysis.trend === 'up' ? '多头排列' : analysis.trend === 'down' ? '空头排列' : '震荡',
aiAnalysis: `趋势:${analysis.trend || '中性'} | 概率:${analysis.probability ? Math.round(analysis.probability * 100) : 50}% | 方向:${analysis.direction || '观望'}`
};
logger.log('使用 service_implementation API 获取品种详情成功');
return result;
}
logger.warn('service_implementation API 未返回数据,尝试使用其他数据源');
} catch (error) {
logger.error('service_implementation API 获取失败:', error);
// service_implementation API 失败,尝试使用其他数据源
}
// 获取数据源配置
const dataSourceConfig = getDataSourceConfig();
// 检查是否有可用的数据源
const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
// 尝试使用TQSDK数据源
if (dataSourceConfig.tqsdk?.enabled) {
try {
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
// 构建合约符号使用小写代码因为TQAPI期望小写
const contractSymbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`;
// 获取合约详情和实时行情
const tick = await dataSource.getTickData(contractSymbol);
// 转换为前端需要的格式
return {
code: future.code,
name: future.name,
fullName: `${future.name}-${future.code}605`,
currentPrice: tick.last_price,
changePercent: tick.price_change / tick.pre_close * 100,
winRate: Math.floor(Math.random() * 50) + 30,
atr: +(Math.random() * 5 + 0.5).toFixed(2),
adx: Math.floor(Math.random() * 60) + 10,
adxStatus: (adx: number) => {
if (adx < 20) return '无趋势/震荡';
if (adx < 40) return '弱趋势';
return '强趋势';
},
trends: {
'5MIN': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'30MIN': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'1HOUR': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'1DAY': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
}
},
indicators: {
macd: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)],
rsi: `${Math.floor(Math.random() * 80) + 10}(中性)`,
bollinger: ['触及上轨', '触及下轨', '中轨附近'][Math.floor(Math.random() * 3)],
kdj: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)]
},
tradingAdvice: {
entry: tick.last_price,
stopLoss: tick.last_price * (1 - 0.02 * (Math.random() + 0.5)),
target: tick.last_price * (1 + 0.03 * (Math.random() + 0.5)),
resistance: tick.last_price * (1 + 0.05 * (Math.random() + 0.5)),
support: tick.last_price * (1 - 0.05 * (Math.random() + 0.5))
},
riskLevel: ['低', '中等', '高'][Math.floor(Math.random() * 3)],
volatility: ['低', '中等', '高'][Math.floor(Math.random() * 3)],
overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)],
aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近`
};
} catch (error) {
logger.error('TQSDK数据源获取失败:', error);
// TQSDK数据源失败尝试使用测试数据源
if (dataSourceConfig.test?.enabled) {
logger.log('切换到测试数据源');
// 启用了测试数据源,使用测试数据
await new Promise(resolve => setTimeout(resolve, 200));
return generateFutureData(symbol, future.name);
} else {
// 未启用测试数据源,返回友好的错误提示
throw new Error('获取品种详情失败,所有数据源均不可用');
}
}
} else if (dataSourceConfig.test?.enabled) {
// 直接使用测试数据源
logger.log('使用测试数据源');
await new Promise(resolve => setTimeout(resolve, 200));
return generateFutureData(symbol, future.name);
} else {
// 无可用数据源
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
} catch (error) {
logger.error(`获取品种${symbol}详情失败:`, error);
// 直接返回友好的错误提示
throw new Error(error instanceof Error ? error.message : '获取品种详情失败,请检查数据源配置');
}
}, { expireTime: 300 }); // 缓存5分钟
};
// 获取K线数据
export const fetchKlineData = async (symbol: string, period: string) => {
const cacheKey = `market:kline:${symbol}:${period}`;
const type = 'kline';
return cacheService.get(cacheKey, symbol, type, async () => {
try {
// 查找合约信息
const future = futuresList.find(item => item.code === symbol);
if (!future) {
throw new Error('品种不存在');
}
// 首先尝试使用 service_implementation API
try {
logger.log('尝试使用 service_implementation API 获取K线数据...');
// 构建合约符号(使用大写代码,因为 service_implementation API 期望大写)
const contractSymbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}3`;
// 转换周期格式
let duration = period;
switch (period) {
case '1M':
duration = '1m';
break;
case '5M':
duration = '5m';
break;
case '15M':
duration = '15m';
break;
case '30M':
duration = '30m';
break;
case '1H':
duration = '1h';
break;
case '4H':
duration = '4h';
break;
case '1D':
duration = '1d';
break;
case '1W':
duration = '1w';
break;
default:
duration = '1m';
}
// 获取K线数据
logger.log(`获取合约${contractSymbol}K线数据周期: ${duration}...`);
const klineResponse = await serviceImplementationClient.getKlineData(contractSymbol, duration, 30);
const klineData = klineResponse.data;
if (klineData.length > 0) {
// 转换为前端需要的格式
const result = klineData.map((item: any) => ({
timestamp: new Date(item.datetime).getTime() / 1000, // 转换为秒
open: item.open,
high: item.high,
low: item.low,
close: item.close,
volume: item.volume
}));
logger.log('使用 service_implementation API 获取K线数据成功');
return result;
}
logger.warn('service_implementation API 未返回K线数据尝试使用其他数据源');
} catch (error) {
logger.error('service_implementation API 获取K线数据失败:', error);
// service_implementation API 失败,尝试使用其他数据源
}
// 获取数据源配置
const dataSourceConfig = getDataSourceConfig();
// 检查是否有可用的数据源
const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
// 尝试使用TQSDK数据源
if (dataSourceConfig.tqsdk?.enabled) {
try {
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
// 构建合约符号使用小写代码因为TQAPI期望小写
const contractSymbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`;
// 获取K线数据
const klineData = await dataSource.getKlineData(contractSymbol, period, 30);
// 转换为前端需要的格式
return klineData.map(item => ({
timestamp: item.datetime / 1000000000, // 转换为秒
open: item.open,
high: item.high,
low: item.low,
close: item.close,
volume: item.volume
}));
} catch (error) {
logger.error('TQSDK数据源获取失败:', error);
// TQSDK数据源失败尝试使用测试数据源
if (dataSourceConfig.test?.enabled) {
logger.log('切换到测试数据源');
// 启用了测试数据源,使用测试数据
await new Promise(resolve => setTimeout(resolve, 200));
return generateKlineData(30);
} else {
// 未启用测试数据源,返回友好的错误提示
throw new Error('获取K线数据失败所有数据源均不可用');
}
}
} else if (dataSourceConfig.test?.enabled) {
// 直接使用测试数据源
logger.log('使用测试数据源');
await new Promise(resolve => setTimeout(resolve, 200));
return generateKlineData(30);
} else {
// 无可用数据源
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
} catch (error) {
logger.error(`获取合约${symbol}K线数据失败:`, error);
// 直接返回友好的错误提示
throw new Error(error instanceof Error ? error.message : '获取K线数据失败请检查数据源配置');
}
}, { expireTime: 600 }); // 缓存10分钟
};
// 获取市场热点
export const fetchMarketHotspots = async () => {
const cacheKey = 'market:hotspots';
const symbol = 'market';
const type = 'hotspots';
return cacheService.get(cacheKey, symbol, type, async () => {
try {
// 首先尝试使用 service_implementation API
try {
logger.log('尝试使用 service_implementation API 获取市场热点...');
// 先获取合约列表
logger.log('获取合约列表...');
const contractsResponse = await serviceImplementationClient.getContracts();
const allContracts = contractsResponse.data;
// 去重,按品种代码分组
const uniqueContracts = new Map();
for (const contract of allContracts) {
// 提取品种代码如从CU2603中提取CU
const code = contract.symbol.slice(0, 2);
if (!uniqueContracts.has(code)) {
uniqueContracts.set(code, {
code: code,
name: contract.name || code,
exchange: contract.exchange || ''
});
}
}
// 转换为数组
const contractList = Array.from(uniqueContracts.values());
logger.log(`获取到 ${contractList.length} 个独特品种`);
// 使用获取到的合约列表
const hotspots = [];
for (const future of contractList) {
try {
// 构建合约符号(使用大写代码,因为 service_implementation API 期望大写)
const symbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}03`;
// 获取合约详情
logger.log(`获取合约${symbol}详情...`);
const contractsResponse = await serviceImplementationClient.getContracts(future.exchange, future.code);
const contract = contractsResponse.data.find((c: any) => c.symbol === symbol);
if (!contract) {
logger.warn(`合约${symbol}不存在,跳过`);
continue;
}
// 更新未来合约的中文名称
future.name = `${contract.product_name || future.name} - ${future.code}`;
// 获取分析数据
logger.log(`分析合约${symbol}...`);
const analysisResponse = await serviceImplementationClient.analyzeMarket(symbol);
const analysis = analysisResponse.data;
hotspots.push({
symbol: future.code,
name: future.name,
change: analysis.change_percent || 0,
volume: analysis.volume || Math.floor(Math.random() * 1000000) + 100000
});
} catch (error) {
logger.error(`获取合约${future.code}行情失败:`, error);
// 跳过获取失败的合约
continue;
}
}
if (hotspots.length > 0) {
// 按涨跌幅排序返回前10个
const sortedHotspots = hotspots
.sort((a, b) => Math.abs(b.change) - Math.abs(a.change))
.slice(0, 10);
logger.log('使用 service_implementation API 获取市场热点成功');
return sortedHotspots;
}
logger.warn('service_implementation API 未返回市场热点数据,尝试使用其他数据源');
} catch (error) {
logger.error('service_implementation API 获取市场热点失败:', error);
// service_implementation API 失败,尝试使用其他数据源
}
// 获取数据源配置
const dataSourceConfig = getDataSourceConfig();
// 检查是否有可用的数据源
const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
// 尝试使用TQSDK数据源
if (dataSourceConfig.tqsdk?.enabled) {
try {
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
// 使用用户指定的合约列表
const hotspots = [];
for (const future of futuresList) {
try {
// 构建合约符号使用小写代码因为TQAPI期望小写
const symbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`;
// 获取合约详情和实时行情
const tick = await dataSource.getTickData(symbol);
hotspots.push({
symbol: future.code,
name: future.name,
change: tick.price_change / tick.pre_close * 100,
volume: tick.volume
});
} catch (error) {
logger.error(`获取合约${future.code}行情失败:`, error);
// 跳过获取失败的合约
continue;
}
}
// 按涨跌幅排序返回前10个
return hotspots
.sort((a, b) => Math.abs(b.change) - Math.abs(a.change))
.slice(0, 10);
} catch (error) {
logger.error('TQSDK数据源获取失败:', error);
// TQSDK数据源失败尝试使用测试数据源
if (dataSourceConfig.test?.enabled) {
logger.log('切换到测试数据源');
// 启用了测试数据源,使用测试数据
await new Promise(resolve => setTimeout(resolve, 200));
const overview = generateFuturesOverview();
// 按涨跌幅排序返回前10个
return overview
.sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent))
.slice(0, 10)
.map(item => ({
symbol: item.code,
name: item.name,
change: item.changePercent,
volume: Math.floor(Math.random() * 1000000) + 100000
}));
} else {
// 未启用测试数据源,返回友好的错误提示
throw new Error('获取市场热点失败,所有数据源均不可用');
}
}
} else if (dataSourceConfig.test?.enabled) {
// 直接使用测试数据源
logger.log('使用测试数据源');
await new Promise(resolve => setTimeout(resolve, 200));
const overview = generateFuturesOverview();
// 按涨跌幅排序返回前10个
return overview
.sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent))
.slice(0, 10)
.map(item => ({
symbol: item.code,
name: item.name,
change: item.changePercent,
volume: Math.floor(Math.random() * 1000000) + 100000
}));
} else {
// 无可用数据源
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
} catch (error) {
logger.error('获取市场热点失败:', error);
// 直接返回友好的错误提示
throw new Error(error instanceof Error ? error.message : '获取市场热点失败,请检查数据源配置');
}
}, { expireTime: 300 }); // 缓存5分钟
};
// 获取风险预警
export const fetchRiskAlerts = async () => {
// 直接返回模拟数据
await new Promise(resolve => setTimeout(resolve, 100));
return riskAlerts;
};