From 46b1f2452fed6a6ee5dd9a3e8c3f20263fc35426 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Thu, 8 Jan 2026 17:45:28 +0800 Subject: [PATCH] =?UTF-8?q?community=E5=A2=9E=E5=8A=A0=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 182 +++++++++ .../components/EventEffectivenessStats.js | 362 ++++++++++++++++++ 2 files changed, 544 insertions(+) create mode 100644 src/views/Community/components/EventEffectivenessStats.js diff --git a/app.py b/app.py index e0d18b5a..54d19a92 100755 --- a/app.py +++ b/app.py @@ -10998,6 +10998,188 @@ def get_zt_theme_scatter(): }), 500 +@app.route('/api/v1/events/effectiveness-stats', methods=['GET']) +def get_events_effectiveness_stats(): + """获取事件有效性统计数据 + + 按交易日统计事件数据,展示事件预测的有效性 + + 参数: + date: 指定日期(YYYY-MM-DD格式,可选,默认今天) + days: 统计天数(默认7天) + + 返回: + { + success: true, + data: { + currentDate: "2026-01-08", + summary: { + totalEvents: 150, // 总事件数 + avgChg: 2.35, // 平均涨幅 + maxChg: 15.8, // 最大涨幅 + positiveRate: 68.5, // 正收益率 + avgInvestScore: 65, // 平均投资评分 + avgSurpriseScore: 58 // 平均超预期得分 + }, + dailyStats: [ + { + date: "2026-01-08", + eventCount: 25, + avgChg: 1.85, + maxChg: 8.5, + positiveCount: 18, + topEvents: [...] // 当日表现最好的事件 + }, + ... + ], + topPerformers: [...] // 近期表现最好的事件 + } + } + """ + try: + date_str = request.args.get('date', '') + days = request.args.get('days', 7, type=int) + days = min(max(days, 1), 30) # 限制1-30天 + + # 确定查询日期范围 + if date_str: + try: + end_date = datetime.strptime(date_str, '%Y-%m-%d') + except ValueError: + end_date = datetime.now() + else: + end_date = datetime.now() + + start_date = end_date - timedelta(days=days) + + # 查询事件数据 + events_query = db.session.query(Event).filter( + Event.created_at >= start_date, + Event.created_at <= end_date + timedelta(days=1), + Event.status == 'active' + ).order_by(Event.created_at.desc()).all() + + if not events_query: + return jsonify({ + 'success': True, + 'data': { + 'currentDate': end_date.strftime('%Y-%m-%d'), + 'summary': { + 'totalEvents': 0, + 'avgChg': 0, + 'maxChg': 0, + 'positiveRate': 0, + 'avgInvestScore': 0, + 'avgSurpriseScore': 0 + }, + 'dailyStats': [], + 'topPerformers': [] + } + }) + + # 计算汇总统计 + total_events = len(events_query) + avg_chg_list = [e.related_avg_chg for e in events_query if e.related_avg_chg is not None] + max_chg_list = [e.related_max_chg for e in events_query if e.related_max_chg is not None] + invest_scores = [e.invest_score for e in events_query if e.invest_score is not None] + surprise_scores = [e.expectation_surprise_score for e in events_query if e.expectation_surprise_score is not None] + + positive_count = sum(1 for chg in avg_chg_list if chg > 0) + + summary = { + 'totalEvents': total_events, + 'avgChg': round(sum(avg_chg_list) / len(avg_chg_list), 2) if avg_chg_list else 0, + 'maxChg': round(max(max_chg_list), 2) if max_chg_list else 0, + 'positiveRate': round(positive_count / len(avg_chg_list) * 100, 1) if avg_chg_list else 0, + 'avgInvestScore': round(sum(invest_scores) / len(invest_scores)) if invest_scores else 0, + 'avgSurpriseScore': round(sum(surprise_scores) / len(surprise_scores)) if surprise_scores else 0 + } + + # 按日期分组统计 + daily_data = {} + for event in events_query: + date_key = event.created_at.strftime('%Y-%m-%d') + if date_key not in daily_data: + daily_data[date_key] = { + 'date': date_key, + 'events': [], + 'avgChgList': [], + 'maxChgList': [] + } + + daily_data[date_key]['events'].append({ + 'id': event.id, + 'title': event.title[:50] if event.title else '', + 'eventType': event.event_type, + 'avgChg': event.related_avg_chg, + 'maxChg': event.related_max_chg, + 'investScore': event.invest_score, + 'surpriseScore': event.expectation_surprise_score, + 'indType': event.ind_type, + }) + + if event.related_avg_chg is not None: + daily_data[date_key]['avgChgList'].append(event.related_avg_chg) + if event.related_max_chg is not None: + daily_data[date_key]['maxChgList'].append(event.related_max_chg) + + # 处理每日统计 + daily_stats = [] + for date_key in sorted(daily_data.keys(), reverse=True): + day = daily_data[date_key] + avg_list = day['avgChgList'] + max_list = day['maxChgList'] + + # 找出当日表现最好的事件(按最大涨幅排序) + top_events = sorted(day['events'], key=lambda x: x['maxChg'] or 0, reverse=True)[:3] + + daily_stats.append({ + 'date': date_key, + 'eventCount': len(day['events']), + 'avgChg': round(sum(avg_list) / len(avg_list), 2) if avg_list else 0, + 'maxChg': round(max(max_list), 2) if max_list else 0, + 'positiveCount': sum(1 for chg in avg_list if chg > 0), + 'topEvents': top_events + }) + + # 找出表现最好的事件(全局) + top_performers = sorted( + [e for e in events_query if e.related_max_chg is not None], + key=lambda x: x.related_max_chg, + reverse=True + )[:10] + + top_performers_list = [{ + 'id': e.id, + 'title': e.title[:60] if e.title else '', + 'eventType': e.event_type, + 'avgChg': e.related_avg_chg, + 'maxChg': e.related_max_chg, + 'investScore': e.invest_score, + 'surpriseScore': e.expectation_surprise_score, + 'indType': e.ind_type, + 'createdAt': e.created_at.strftime('%Y-%m-%d %H:%M') if e.created_at else '', + } for e in top_performers] + + return jsonify({ + 'success': True, + 'data': { + 'currentDate': end_date.strftime('%Y-%m-%d'), + 'summary': summary, + 'dailyStats': daily_stats, + 'topPerformers': top_performers_list + } + }) + + except Exception as e: + import traceback + traceback.print_exc() + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + @app.route('/api/v1/calendar/events', methods=['GET']) def get_calendar_events(): """获取指定日期的事件列表""" diff --git a/src/views/Community/components/EventEffectivenessStats.js b/src/views/Community/components/EventEffectivenessStats.js new file mode 100644 index 00000000..8ab0327f --- /dev/null +++ b/src/views/Community/components/EventEffectivenessStats.js @@ -0,0 +1,362 @@ +/** + * EventEffectivenessStats - 事件有效性统计 + * 展示事件中心的事件有效性数据,证明系统推荐价值 + */ +import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import { + Box, + Text, + VStack, + HStack, + Spinner, + Center, + useToast, + Stat, + StatLabel, + StatNumber, + StatHelpText, + StatArrow, + Progress, + Badge, + Divider, + Tooltip, + Icon, +} from '@chakra-ui/react'; +import { + TrophyOutlined, + RiseOutlined, + FireOutlined, + CheckCircleOutlined, + ThunderboltOutlined, + StarOutlined, +} from '@ant-design/icons'; +import { getApiBase } from '@utils/apiConfig'; + +/** + * 格式化涨跌幅 + */ +const formatChg = (val) => { + if (val === null || val === undefined) return '-'; + const num = parseFloat(val); + if (isNaN(num)) return '-'; + return (num >= 0 ? '+' : '') + num.toFixed(2) + '%'; +}; + +/** + * 获取涨跌幅颜色 + */ +const getChgColor = (val) => { + if (val === null || val === undefined) return 'gray.400'; + const num = parseFloat(val); + if (isNaN(num)) return 'gray.400'; + if (num > 0) return '#FF4D4F'; + if (num < 0) return '#52C41A'; + return 'gray.400'; +}; + +/** + * 数据卡片组件 + */ +const StatCard = ({ label, value, icon, color = '#FFD700', subText, trend, progress }) => ( + + + + {icon} + + + {label} + + + + {value} + + {subText && ( + + {subText} + + )} + {trend !== undefined && ( + + = 0 ? 'increase' : 'decrease'} /> + = 0 ? '#FF4D4F' : '#52C41A'}> + {Math.abs(trend).toFixed(1)}% + + + )} + {progress !== undefined && ( + = 60 ? 'green' : progress >= 40 ? 'yellow' : 'red'} + mt={2} + borderRadius="full" + bg="rgba(255,255,255,0.1)" + /> + )} + +); + +/** + * 热门事件列表项 + */ +const TopEventItem = ({ event, rank }) => ( + + + {rank} + + + + {event.title} + + + + {formatChg(event.max_chg)} + + +); + +const EventEffectivenessStats = () => { + const [loading, setLoading] = useState(true); + const [stats, setStats] = useState(null); + const [error, setError] = useState(null); + const toast = useToast(); + + const fetchStats = useCallback(async () => { + setLoading(true); + setError(null); + try { + const apiBase = getApiBase(); + const response = await fetch(`${apiBase}/api/v1/events/effectiveness-stats?days=30`); + if (!response.ok) throw new Error('获取数据失败'); + const data = await response.json(); + if (data.code === 200) { + setStats(data.data); + } else { + throw new Error(data.message || '数据格式错误'); + } + } catch (err) { + console.error('获取事件有效性统计失败:', err); + setError(err.message); + toast({ + title: '获取统计数据失败', + description: err.message, + status: 'error', + duration: 3000, + isClosable: true, + }); + } finally { + setLoading(false); + } + }, [toast]); + + useEffect(() => { + fetchStats(); + }, [fetchStats]); + + if (loading) { + return ( + +
+ +
+
+ ); + } + + if (error || !stats) { + return ( + +
+ + 暂无数据 + {error} + +
+
+ ); + } + + const { summary, topPerformers = [] } = stats; + + return ( + + {/* 背景装饰 */} + + + {/* 标题 */} + + + + 事件有效性统计 + + + 近30天 + + + + + {/* 核心指标 - 2列网格 */} + + } + color="#FFD700" + subText="活跃事件" + /> + } + color={summary?.positiveRate >= 50 ? '#52C41A' : '#FF4D4F'} + progress={summary?.positiveRate || 0} + /> + } + color={getChgColor(summary?.avgChg)} + subText="关联股票" + /> + } + color="#FF4D4F" + subText="单事件最佳" + /> + + + {/* 评分指标 */} + + } + color="#F59E0B" + progress={summary?.avgInvestScore || 0} + subText="平均评分" + /> + } + color="#8B5CF6" + progress={summary?.avgSurpriseScore || 0} + subText="惊喜程度" + /> + + + {/* 分割线 */} + + + {/* TOP表现事件 */} + + + + + TOP 表现事件 + + + + {topPerformers.slice(0, 5).map((event, idx) => ( + + ))} + {topPerformers.length === 0 && ( + + 暂无数据 + + )} + + + + + ); +}; + +export default EventEffectivenessStats;