heropanel修改

This commit is contained in:
2026-01-09 07:36:31 +08:00
parent b9672bcef1
commit 30f6346252
2 changed files with 230 additions and 50 deletions

90
app.py
View File

@@ -11274,6 +11274,95 @@ def get_events_effectiveness_stats():
'positiveRate': round(positive_count / len(avg_chg_list) * 100, 1) if avg_chg_list else 0,
}
# 查询全市场实时涨跌统计(从 ClickHouse
market_stats = {
'risingCount': 0,
'fallingCount': 0,
'flatCount': 0,
'totalCount': 0,
'risingRate': 0,
}
try:
client = get_clickhouse_client()
# 获取当前交易日
if hasattr(current_trading_day, 'strftime'):
target_date = current_trading_day
else:
target_date = datetime.strptime(str(current_trading_day), '%Y-%m-%d').date()
market_start = datetime.combine(target_date, dt_time(9, 30))
market_end = datetime.combine(target_date, dt_time(15, 0))
# 获取前收盘日期
prev_trading_day = None
try:
target_idx = trading_days.index(target_date)
if target_idx > 0:
prev_trading_day = trading_days[target_idx - 1]
except (ValueError, IndexError):
pass
if prev_trading_day:
prev_date_str = prev_trading_day.strftime('%Y%m%d')
# 查询所有 A 股的最新价格(排除指数和北交所)
market_price_query = """
SELECT
code,
argMax(close, timestamp) as last_price
FROM stock_minute
WHERE timestamp >= %(start)s
AND timestamp <= %(end)s
AND (code LIKE '%%.SH' OR code LIKE '%%.SZ')
AND code NOT LIKE '399%%'
AND code NOT LIKE '000%%'
GROUP BY code
"""
market_data = client.execute(market_price_query, {
'start': market_start,
'end': market_end
})
if market_data:
# 提取股票代码并获取前收盘价
all_base_codes = [row[0].split('.')[0] for row in market_data]
all_prev_close = get_cached_prev_close(all_base_codes, prev_date_str)
rising = 0
falling = 0
flat = 0
for row in market_data:
code = row[0]
last_price = float(row[1]) if row[1] else None
base_code = code.split('.')[0]
prev_close = all_prev_close.get(base_code)
if last_price and prev_close and prev_close > 0:
change_pct = (last_price - prev_close) / prev_close * 100
if change_pct > 0.01: # 上涨
rising += 1
elif change_pct < -0.01: # 下跌
falling += 1
else: # 平盘
flat += 1
total = rising + falling + flat
market_stats = {
'risingCount': rising,
'fallingCount': falling,
'flatCount': flat,
'totalCount': total,
'risingRate': round(rising / total * 100, 1) if total > 0 else 0,
}
print(f'[effectiveness-stats] 市场统计: 上涨={rising}, 下跌={falling}, 平盘={flat}, 上涨率={market_stats["risingRate"]}%')
except Exception as e:
import traceback
print(f'[effectiveness-stats] 查询市场统计失败: {e}')
traceback.print_exc()
# 按日期分组统计
daily_data = {}
for event in events_query:
@@ -11343,6 +11432,7 @@ def get_events_effectiveness_stats():
'data': {
'currentDate': current_trading_day.strftime('%Y-%m-%d') if hasattr(current_trading_day, 'strftime') else str(current_trading_day),
'summary': summary,
'marketStats': market_stats,
'dailyStats': daily_stats,
'topPerformers': top_performers_list,
'topStocks': stock_stats

View File

@@ -61,89 +61,175 @@ const getChgColor = (val) => {
};
/**
* 胜率仪表盘组件
* 获取胜率颜色(>50%红色,<50%绿色)
*/
const WinRateGauge = ({ rate }) => {
const getRateColor = (rate) => {
if (rate >= 50) return '#FF4D4F'; // 红色(上涨多)
return '#52C41A'; // 绿色(下跌多)
};
/**
* 半圆仪表盘组件 - 参考用户提供的设计
* 使用渐变色:左侧绿色 -> 中间黄色 -> 右侧红色
*/
const SemiCircleGauge = ({ rate, label, size = 'normal' }) => {
const validRate = Math.min(100, Math.max(0, rate || 0));
const angle = (validRate / 100) * 180;
// 角度0% = -90deg (左), 50% = 0deg (上), 100% = 90deg (右)
const needleAngle = (validRate / 100) * 180 - 90;
const getGaugeColor = (r) => {
if (r >= 70) return '#52C41A';
if (r >= 50) return '#FFD700';
return '#FF4D4F';
};
const gaugeColor = getGaugeColor(validRate);
const gaugeColor = getRateColor(validRate);
const isSmall = size === 'small';
const gaugeWidth = isSmall ? 80 : 100;
const gaugeHeight = gaugeWidth / 2;
const needleLength = isSmall ? 30 : 38;
return (
<Box position="relative" w="100%" h="80px" overflow="hidden">
<Box position="relative" w={`${gaugeWidth}px`} h={`${gaugeHeight + 20}px`}>
{/* 半圆背景(渐变色弧线) */}
<Box
position="absolute"
bottom="0"
left="50%"
transform="translateX(-50%)"
w="120px"
h="60px"
borderTopLeftRadius="60px"
borderTopRightRadius="60px"
bg="rgba(255,255,255,0.05)"
border="3px solid rgba(255,255,255,0.1)"
borderBottom="none"
bottom="16px"
left="0"
w={`${gaugeWidth}px`}
h={`${gaugeHeight}px`}
borderTopLeftRadius={`${gaugeHeight}px`}
borderTopRightRadius={`${gaugeHeight}px`}
bg="transparent"
overflow="hidden"
>
<Box
position="absolute"
bottom="0"
left="0"
w="100%"
h="100%"
bg={`conic-gradient(from 180deg, ${gaugeColor} 0deg, ${gaugeColor} ${angle}deg, transparent ${angle}deg)`}
opacity="0.3"
style={{ transformOrigin: 'center bottom' }}
/>
</Box>
_before={{
content: '""',
position: 'absolute',
top: '0',
left: '0',
right: '0',
bottom: '0',
borderTopLeftRadius: `${gaugeHeight}px`,
borderTopRightRadius: `${gaugeHeight}px`,
border: `${isSmall ? '6px' : '8px'} solid transparent`,
borderBottom: 'none',
background: 'linear-gradient(90deg, #52C41A 0%, #FADB14 50%, #FF4D4F 100%) border-box',
mask: 'linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0)',
maskComposite: 'exclude',
WebkitMaskComposite: 'destination-out',
}}
/>
{/* 内部遮罩(让弧线更细) */}
<Box
position="absolute"
bottom="3px"
bottom="16px"
left={`${isSmall ? 10 : 12}px`}
w={`${gaugeWidth - (isSmall ? 20 : 24)}px`}
h={`${gaugeHeight - (isSmall ? 10 : 12)}px`}
borderTopLeftRadius={`${gaugeHeight - (isSmall ? 10 : 12)}px`}
borderTopRightRadius={`${gaugeHeight - (isSmall ? 10 : 12)}px`}
bg="rgba(17, 24, 39, 0.95)"
/>
{/* 指针 */}
<Box
position="absolute"
bottom="16px"
left="50%"
w="2px"
h="42px"
h={`${needleLength}px`}
bg={gaugeColor}
borderRadius="full"
transformOrigin="bottom center"
transform={`translateX(-50%) rotate(${angle - 90}deg)`}
boxShadow={`0 0 10px ${gaugeColor}`}
transform={`translateX(-50%) rotate(${needleAngle}deg)`}
boxShadow={`0 0 6px ${gaugeColor}`}
transition="transform 0.5s ease-out"
/>
{/* 指针中心点 */}
<Box
position="absolute"
bottom="0"
bottom="13px"
left="50%"
transform="translateX(-50%)"
w="10px"
h="10px"
w="6px"
h="6px"
bg={gaugeColor}
borderRadius="full"
boxShadow={`0 0 8px ${gaugeColor}`}
boxShadow={`0 0 4px ${gaugeColor}`}
/>
{/* 刻度标记 */}
<Text
position="absolute"
bottom="16px"
left="0"
fontSize="2xs"
color="gray.600"
transform="translateX(2px)"
>
0
</Text>
<Text
position="absolute"
bottom="16px"
right="0"
fontSize="2xs"
color="gray.600"
transform="translateX(-2px)"
>
100
</Text>
{/* 数值和标签 */}
<VStack
position="absolute"
bottom="12px"
bottom="0"
left="50%"
transform="translateX(-50%)"
spacing={0}
>
<Text fontSize="xl" fontWeight="bold" color={gaugeColor} lineHeight="1">
{validRate.toFixed(0)}%
<Text fontSize={isSmall ? 'sm' : 'md'} fontWeight="bold" color={gaugeColor} lineHeight="1">
{validRate.toFixed(1)}%
</Text>
<Text fontSize="2xs" color="gray.500">胜率</Text>
<Text fontSize="2xs" color="gray.500">{label}</Text>
</VStack>
</Box>
);
};
<Text position="absolute" bottom="0" left="12px" fontSize="2xs" color="gray.600">0</Text>
<Text position="absolute" bottom="0" right="12px" fontSize="2xs" color="gray.600">100</Text>
/**
* 胜率对比组件 - 双仪表盘
*/
const WinRateGauge = ({ eventRate, marketRate, marketStats }) => {
const eventRateVal = eventRate || 0;
const marketRateVal = marketRate || 0;
return (
<Box>
{/* 双仪表盘对比 */}
<HStack spacing={2} justify="center" mb={1}>
<VStack spacing={0}>
<SemiCircleGauge rate={eventRateVal} label="事件胜率" size="small" />
</VStack>
<Box w="1px" h="50px" bg="rgba(255,215,0,0.2)" />
<VStack spacing={0}>
<SemiCircleGauge rate={marketRateVal} label="大盘上涨率" size="small" />
</VStack>
</HStack>
{/* 市场统计 */}
{marketStats && (
<HStack justify="center" spacing={3} mt={1}>
<HStack spacing={1}>
<Box w="6px" h="6px" borderRadius="full" bg="#FF4D4F" />
<Text fontSize="2xs" color="#FF4D4F">{marketStats.risingCount}</Text>
</HStack>
<HStack spacing={1}>
<Box w="6px" h="6px" borderRadius="full" bg="gray.400" />
<Text fontSize="2xs" color="gray.400">{marketStats.flatCount}</Text>
</HStack>
<HStack spacing={1}>
<Box w="6px" h="6px" borderRadius="full" bg="#52C41A" />
<Text fontSize="2xs" color="#52C41A">{marketStats.fallingCount}</Text>
</HStack>
</HStack>
)}
</Box>
);
};
@@ -350,7 +436,7 @@ const EventDailyStats = () => {
);
}
const { summary, topPerformers = [], topStocks = [] } = stats;
const { summary, marketStats, topPerformers = [], topStocks = [] } = stats;
return (
<Box
@@ -423,8 +509,12 @@ const EventDailyStats = () => {
{/* 内容区域 - 使用 flex: 1 填充剩余空间 */}
<VStack spacing={2} align="stretch" flex="1">
{/* 胜率仪表盘 */}
<WinRateGauge rate={summary?.positiveRate || 0} />
{/* 胜率对比仪表盘 */}
<WinRateGauge
eventRate={summary?.positiveRate || 0}
marketRate={marketStats?.risingRate || 0}
marketStats={marketStats}
/>
{/* 核心指标 - 2x2 网格 */}
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={2}>