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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 市场数据服务
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;
};