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,
|
'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 = {}
|
daily_data = {}
|
||||||
for event in events_query:
|
for event in events_query:
|
||||||
@@ -11343,6 +11432,7 @@ def get_events_effectiveness_stats():
|
|||||||
'data': {
|
'data': {
|
||||||
'currentDate': current_trading_day.strftime('%Y-%m-%d') if hasattr(current_trading_day, 'strftime') else str(current_trading_day),
|
'currentDate': current_trading_day.strftime('%Y-%m-%d') if hasattr(current_trading_day, 'strftime') else str(current_trading_day),
|
||||||
'summary': summary,
|
'summary': summary,
|
||||||
|
'marketStats': market_stats,
|
||||||
'dailyStats': daily_stats,
|
'dailyStats': daily_stats,
|
||||||
'topPerformers': top_performers_list,
|
'topPerformers': top_performers_list,
|
||||||
'topStocks': stock_stats
|
'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 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) => {
|
const gaugeColor = getRateColor(validRate);
|
||||||
if (r >= 70) return '#52C41A';
|
const isSmall = size === 'small';
|
||||||
if (r >= 50) return '#FFD700';
|
const gaugeWidth = isSmall ? 80 : 100;
|
||||||
return '#FF4D4F';
|
const gaugeHeight = gaugeWidth / 2;
|
||||||
};
|
const needleLength = isSmall ? 30 : 38;
|
||||||
|
|
||||||
const gaugeColor = getGaugeColor(validRate);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position="relative" w="100%" h="80px" overflow="hidden">
|
<Box position="relative" w={`${gaugeWidth}px`} h={`${gaugeHeight + 20}px`}>
|
||||||
|
{/* 半圆背景(渐变色弧线) */}
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="0"
|
bottom="16px"
|
||||||
left="50%"
|
left="0"
|
||||||
transform="translateX(-50%)"
|
w={`${gaugeWidth}px`}
|
||||||
w="120px"
|
h={`${gaugeHeight}px`}
|
||||||
h="60px"
|
borderTopLeftRadius={`${gaugeHeight}px`}
|
||||||
borderTopLeftRadius="60px"
|
borderTopRightRadius={`${gaugeHeight}px`}
|
||||||
borderTopRightRadius="60px"
|
bg="transparent"
|
||||||
bg="rgba(255,255,255,0.05)"
|
|
||||||
border="3px solid rgba(255,255,255,0.1)"
|
|
||||||
borderBottom="none"
|
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
_before={{
|
||||||
<Box
|
content: '""',
|
||||||
position="absolute"
|
position: 'absolute',
|
||||||
bottom="0"
|
top: '0',
|
||||||
left="0"
|
left: '0',
|
||||||
w="100%"
|
right: '0',
|
||||||
h="100%"
|
bottom: '0',
|
||||||
bg={`conic-gradient(from 180deg, ${gaugeColor} 0deg, ${gaugeColor} ${angle}deg, transparent ${angle}deg)`}
|
borderTopLeftRadius: `${gaugeHeight}px`,
|
||||||
opacity="0.3"
|
borderTopRightRadius: `${gaugeHeight}px`,
|
||||||
style={{ transformOrigin: 'center bottom' }}
|
border: `${isSmall ? '6px' : '8px'} solid transparent`,
|
||||||
/>
|
borderBottom: 'none',
|
||||||
</Box>
|
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
|
<Box
|
||||||
position="absolute"
|
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%"
|
left="50%"
|
||||||
w="2px"
|
w="2px"
|
||||||
h="42px"
|
h={`${needleLength}px`}
|
||||||
bg={gaugeColor}
|
bg={gaugeColor}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
transformOrigin="bottom center"
|
transformOrigin="bottom center"
|
||||||
transform={`translateX(-50%) rotate(${angle - 90}deg)`}
|
transform={`translateX(-50%) rotate(${needleAngle}deg)`}
|
||||||
boxShadow={`0 0 10px ${gaugeColor}`}
|
boxShadow={`0 0 6px ${gaugeColor}`}
|
||||||
transition="transform 0.5s ease-out"
|
transition="transform 0.5s ease-out"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 指针中心点 */}
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="0"
|
bottom="13px"
|
||||||
left="50%"
|
left="50%"
|
||||||
transform="translateX(-50%)"
|
transform="translateX(-50%)"
|
||||||
w="10px"
|
w="6px"
|
||||||
h="10px"
|
h="6px"
|
||||||
bg={gaugeColor}
|
bg={gaugeColor}
|
||||||
borderRadius="full"
|
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
|
<VStack
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="12px"
|
bottom="0"
|
||||||
left="50%"
|
left="50%"
|
||||||
transform="translateX(-50%)"
|
transform="translateX(-50%)"
|
||||||
spacing={0}
|
spacing={0}
|
||||||
>
|
>
|
||||||
<Text fontSize="xl" fontWeight="bold" color={gaugeColor} lineHeight="1">
|
<Text fontSize={isSmall ? 'sm' : 'md'} fontWeight="bold" color={gaugeColor} lineHeight="1">
|
||||||
{validRate.toFixed(0)}%
|
{validRate.toFixed(1)}%
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="2xs" color="gray.500">胜率</Text>
|
<Text fontSize="2xs" color="gray.500">{label}</Text>
|
||||||
</VStack>
|
</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>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -350,7 +436,7 @@ const EventDailyStats = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { summary, topPerformers = [], topStocks = [] } = stats;
|
const { summary, marketStats, topPerformers = [], topStocks = [] } = stats;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -423,8 +509,12 @@ const EventDailyStats = () => {
|
|||||||
|
|
||||||
{/* 内容区域 - 使用 flex: 1 填充剩余空间 */}
|
{/* 内容区域 - 使用 flex: 1 填充剩余空间 */}
|
||||||
<VStack spacing={2} align="stretch" 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 网格 */}
|
{/* 核心指标 - 2x2 网格 */}
|
||||||
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={2}>
|
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={2}>
|
||||||
|
|||||||
Reference in New Issue
Block a user