import prisma from '../config/database'; import { cache } from '../config/redis'; import config from '../config'; import { Sector, SectorMomentumHistory, MomentumStock } from '../types'; import { calculateSectorMomentum } from '../utils/maCalculator'; import logger from '../utils/logger'; export class SectorService { // 获取版块列表(带动量排名) async getSectorsWithMomentum(): Promise { const cacheKey = 'sectors:momentum'; const cached = await cache.get(cacheKey); if (cached) { return cached; } try { const sectors = await prisma.sector.findMany({ include: { quotes: { orderBy: { quoteTime: 'desc' }, take: 2, }, }, }); // 计算动量分数和排名 const sectorsWithMomentum: Sector[] = sectors.map((sector) => { const latestQuote = sector.quotes[0]; const previousQuote = sector.quotes[1]; const volume = latestQuote?.volume ? Number(latestQuote.volume) : 0; const avgVolume = previousQuote?.volume ? Number(previousQuote.volume) : volume; const momentumScore = calculateSectorMomentum( latestQuote?.changePercent || 0, volume, avgVolume ); return { name: sector.name, code: sector.code, change: latestQuote?.change || 0, changePercent: latestQuote?.changePercent || 0, volume, turnover: latestQuote?.turnover ? Number(latestQuote.turnover) : 0, leadingStock: sector.name, // 可以从关联数据中获取 momentumScore, rank: 0, // 稍后计算 previousRank: previousQuote?.rank || 0, rankChange: 0, }; }); // 按动量分数排序并分配排名 sectorsWithMomentum.sort((a, b) => (b.momentumScore || 0) - (a.momentumScore || 0)); sectorsWithMomentum.forEach((sector, index) => { sector.rank = index + 1; sector.rankChange = (sector.previousRank || 0) - sector.rank; }); await cache.set(cacheKey, sectorsWithMomentum, config.cacheTtl.sectors); return sectorsWithMomentum; } catch (error) { logger.error('Failed to get sectors with momentum:', error); return this.getDefaultSectors(); } } // 获取默认版块数据 private getDefaultSectors(): Sector[] { const sectors = [ '半导体', '新能源', '医药生物', '白酒', '银行', '证券', '保险', '房地产', '汽车', '电子', '计算机', '通信', '传媒', '军工', '有色金属', '钢铁', '煤炭', '化工', '建筑材料', '机械设备', ]; return sectors.map((name, index) => ({ name, code: `880${String(index + 1).padStart(3, '0')}`, change: Math.random() * 20 - 10, changePercent: Math.random() * 5 - 2, volume: Math.floor(Math.random() * 90000000) + 10000000, turnover: Math.floor(Math.random() * 900000000) + 100000000, leadingStock: `${name}龙头`, momentumScore: Math.random() * 60 + 30, rank: index + 1, previousRank: Math.floor(Math.random() * 20) + 1, rankChange: Math.floor(Math.random() * 10) - 5, })); } // 获取版块详情 async getSectorDetail(sectorCode: string): Promise { const cacheKey = `sector:${sectorCode}:detail`; const cached = await cache.get(cacheKey); if (cached) { return cached; } try { const sector = await prisma.sector.findUnique({ where: { code: sectorCode }, include: { quotes: { orderBy: { quoteTime: 'desc' }, take: 2, }, }, }); if (!sector) { return null; } const latestQuote = sector.quotes[0]; const previousQuote = sector.quotes[1]; const result: Sector = { name: sector.name, code: sector.code, change: latestQuote?.change || 0, changePercent: latestQuote?.changePercent || 0, volume: latestQuote?.volume ? Number(latestQuote.volume) : 0, turnover: latestQuote?.turnover ? Number(latestQuote.turnover) : 0, momentumScore: latestQuote?.momentumScore || 50, rank: latestQuote?.rank || 0, previousRank: previousQuote?.rank || 0, rankChange: (previousQuote?.rank || 0) - (latestQuote?.rank || 0), }; await cache.set(cacheKey, result, config.cacheTtl.sectorDetail); return result; } catch (error) { logger.error(`Failed to get sector detail ${sectorCode}:`, error); return null; } } // 获取版块历史排名 async getSectorRankHistory(sectorCode: string, days: number = 30): Promise { const cacheKey = `sector:${sectorCode}:rank:history:${days}`; const cached = await cache.get(cacheKey); if (cached) { return cached; } try { const history = await prisma.sectorQuote.findMany({ where: { sectorCode, quoteTime: { gte: new Date(Date.now() - days * 24 * 60 * 60 * 1000), }, }, orderBy: { quoteTime: 'asc' }, select: { quoteTime: true, rank: true, momentumScore: true, }, }); const result: SectorMomentumHistory[] = history.map((h) => ({ date: h.quoteTime.toISOString().split('T')[0], rank: h.rank, momentumScore: h.momentumScore, topStock: '', // 可以从其他表获取 })); await cache.set(cacheKey, result, config.cacheTtl.sectorDetail); return result; } catch (error) { logger.error(`Failed to get sector rank history ${sectorCode}:`, error); return this.generateMockRankHistory(days); } } // 生成模拟历史数据 private generateMockRankHistory(days: number): SectorMomentumHistory[] { const history: SectorMomentumHistory[] = []; const today = new Date(); let currentRank = Math.floor(Math.random() * 20) + 1; let currentScore = Math.random() * 40 + 50; for (let i = days; i >= 0; i--) { const date = new Date(today); date.setDate(date.getDate() - i); const rankChange = Math.floor(Math.random() * 7) - 3; currentRank = Math.max(1, Math.min(20, currentRank + rankChange)); const scoreChange = Math.random() * 10 - 5; currentScore = Math.max(30, Math.min(100, currentScore + scoreChange)); history.push({ date: date.toISOString().split('T')[0], rank: currentRank, momentumScore: Number(currentScore.toFixed(2)), topStock: `股票${Math.floor(Math.random() * 100)}`, }); } return history; } // 获取版块内股票 async getSectorStocks(sectorCode: string, limit: number = 20): Promise { try { const stocks = await prisma.stock.findMany({ where: { sectorCode }, include: { quotes: { orderBy: { quoteTime: 'desc' }, take: 1, }, }, take: limit, }); return stocks.map((stock) => ({ code: stock.code, name: stock.name, price: stock.quotes[0]?.price || 0, change: stock.quotes[0]?.change || 0, changePercent: stock.quotes[0]?.changePercent || 0, volume: stock.quotes[0]?.volume ? Number(stock.quotes[0].volume) : 0, turnover: stock.quotes[0]?.turnover ? Number(stock.quotes[0].turnover) : 0, marketCap: stock.marketCap ? Number(stock.marketCap) : 0, pe: stock.pe, pb: stock.pb, industry: stock.sector?.name, })); } catch (error) { logger.error(`Failed to get sector stocks ${sectorCode}:`, error); return []; } } // 获取版块内动量个股 async getSectorMomentumStocks(sectorCode: string): Promise { try { const stocks = await prisma.stock.findMany({ where: { sectorCode }, include: { quotes: { orderBy: { quoteTime: 'desc' }, take: 2, }, momentumRecords: { orderBy: { date: 'desc' }, take: 1, }, }, }); const tags = ['强势突破', '量价齐升', '趋势反转', '资金流入', '技术金叉']; return stocks .map((stock) => { const quote = stock.quotes[0]; const momentumRecord = stock.momentumRecords[0]; return { code: stock.code, name: stock.name, price: quote?.price || 0, change: quote?.change || 0, changePercent: quote?.changePercent || 0, volume: quote?.volume ? Number(quote.volume) : 0, turnover: quote?.turnover ? Number(quote.turnover) : 0, industry: stock.sector?.name || '', momentumScore: momentumRecord?.momentumScore || Math.floor(Math.random() * 50) + 50, tags: momentumRecord?.tags ? JSON.parse(momentumRecord.tags) : [tags[Math.floor(Math.random() * tags.length)]], volumeRatio: momentumRecord?.volumeRatio || Math.random() * 6 + 1.5, breakThrough: momentumRecord?.breakThrough || Math.random() > 0.6, }; }) .sort((a, b) => b.momentumScore - a.momentumScore); } catch (error) { logger.error(`Failed to get sector momentum stocks ${sectorCode}:`, error); return []; } } // 更新版块排名 async updateSectorRankings(): Promise { try { const sectors = await this.getSectorsWithMomentum(); // 批量更新排名 await Promise.all( sectors.map((sector, index) => prisma.sectorQuote.updateMany({ where: { sectorCode: sector.code, }, data: { rank: index + 1, momentumScore: sector.momentumScore || 50, }, }) ) ); // 清除缓存 await cache.delPattern('sectors:*'); logger.info('Sector rankings updated successfully'); } catch (error) { logger.error('Failed to update sector rankings:', error); } } } export const sectorService = new SectorService();