heropanel修改
This commit is contained in:
90
app.py
90
app.py
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user