update pay ui
This commit is contained in:
174
app.py
174
app.py
@@ -12053,10 +12053,11 @@ def get_market_summary(seccode):
|
||||
|
||||
@app.route('/api/stocks/search', methods=['GET'])
|
||||
def search_stocks():
|
||||
"""搜索股票(支持股票代码、股票简称、拼音首字母)"""
|
||||
"""搜索股票和指数(支持代码、名称搜索)"""
|
||||
try:
|
||||
query = request.args.get('q', '').strip()
|
||||
limit = request.args.get('limit', 20, type=int)
|
||||
search_type = request.args.get('type', 'all') # all, stock, index
|
||||
|
||||
if not query:
|
||||
return jsonify({
|
||||
@@ -12064,73 +12065,132 @@ def search_stocks():
|
||||
'error': '请输入搜索关键词'
|
||||
}), 400
|
||||
|
||||
results = []
|
||||
|
||||
with engine.connect() as conn:
|
||||
test_sql = text("""
|
||||
SELECT SECCODE, SECNAME, F001V, F003V, F010V, F011V
|
||||
FROM ea_stocklist
|
||||
WHERE SECCODE = '300750'
|
||||
OR F001V LIKE '%ndsd%' LIMIT 5
|
||||
""")
|
||||
test_result = conn.execute(test_sql).fetchall()
|
||||
# 搜索指数(优先显示指数,因为通常用户搜索代码时指数更常用)
|
||||
if search_type in ('all', 'index'):
|
||||
index_sql = text("""
|
||||
SELECT DISTINCT
|
||||
INDEXCODE as stock_code,
|
||||
SECNAME as stock_name,
|
||||
INDEXNAME as full_name,
|
||||
F018V as exchange
|
||||
FROM ea_exchangeindex
|
||||
WHERE (
|
||||
UPPER(INDEXCODE) LIKE UPPER(:query_pattern)
|
||||
OR UPPER(SECNAME) LIKE UPPER(:query_pattern)
|
||||
OR UPPER(INDEXNAME) LIKE UPPER(:query_pattern)
|
||||
)
|
||||
ORDER BY CASE
|
||||
WHEN UPPER(INDEXCODE) = UPPER(:exact_query) THEN 1
|
||||
WHEN UPPER(SECNAME) = UPPER(:exact_query) THEN 2
|
||||
WHEN UPPER(INDEXCODE) LIKE UPPER(:prefix_pattern) THEN 3
|
||||
WHEN UPPER(SECNAME) LIKE UPPER(:prefix_pattern) THEN 4
|
||||
ELSE 5
|
||||
END,
|
||||
INDEXCODE
|
||||
LIMIT :limit
|
||||
""")
|
||||
|
||||
# 构建搜索SQL - 支持股票代码、股票简称、拼音简称搜索
|
||||
search_sql = text("""
|
||||
SELECT DISTINCT SECCODE as stock_code,
|
||||
SECNAME as stock_name,
|
||||
F001V as pinyin_abbr,
|
||||
F003V as security_type,
|
||||
F005V as exchange,
|
||||
F011V as listing_status
|
||||
FROM ea_stocklist
|
||||
WHERE (
|
||||
UPPER(SECCODE) LIKE UPPER(:query_pattern)
|
||||
OR UPPER(SECNAME) LIKE UPPER(:query_pattern)
|
||||
OR UPPER(F001V) LIKE UPPER(:query_pattern)
|
||||
)
|
||||
-- 基本过滤条件:只搜索正常的A股和B股
|
||||
AND (F011V = '正常上市' OR F010V = '013001') -- 正常上市状态
|
||||
AND F003V IN ('A股', 'B股') -- 只搜索A股和B股
|
||||
ORDER BY CASE
|
||||
WHEN UPPER(SECCODE) = UPPER(:exact_query) THEN 1
|
||||
WHEN UPPER(SECNAME) = UPPER(:exact_query) THEN 2
|
||||
WHEN UPPER(F001V) = UPPER(:exact_query) THEN 3
|
||||
WHEN UPPER(SECCODE) LIKE UPPER(:prefix_pattern) THEN 4
|
||||
WHEN UPPER(SECNAME) LIKE UPPER(:prefix_pattern) THEN 5
|
||||
WHEN UPPER(F001V) LIKE UPPER(:prefix_pattern) THEN 6
|
||||
ELSE 7
|
||||
END,
|
||||
SECCODE LIMIT :limit
|
||||
""")
|
||||
index_result = conn.execute(index_sql, {
|
||||
'query_pattern': f'%{query}%',
|
||||
'exact_query': query,
|
||||
'prefix_pattern': f'{query}%',
|
||||
'limit': limit
|
||||
}).fetchall()
|
||||
|
||||
result = conn.execute(search_sql, {
|
||||
'query_pattern': f'%{query}%',
|
||||
'exact_query': query,
|
||||
'prefix_pattern': f'{query}%',
|
||||
'limit': limit
|
||||
}).fetchall()
|
||||
for row in index_result:
|
||||
results.append({
|
||||
'stock_code': row.stock_code,
|
||||
'stock_name': row.stock_name,
|
||||
'full_name': row.full_name,
|
||||
'exchange': row.exchange,
|
||||
'isIndex': True,
|
||||
'security_type': '指数'
|
||||
})
|
||||
|
||||
stocks = []
|
||||
for row in result:
|
||||
# 获取当前价格
|
||||
current_price, _ = get_latest_price_from_clickhouse(row.stock_code)
|
||||
# 搜索股票
|
||||
if search_type in ('all', 'stock'):
|
||||
stock_sql = text("""
|
||||
SELECT DISTINCT SECCODE as stock_code,
|
||||
SECNAME as stock_name,
|
||||
F001V as pinyin_abbr,
|
||||
F003V as security_type,
|
||||
F005V as exchange,
|
||||
F011V as listing_status
|
||||
FROM ea_stocklist
|
||||
WHERE (
|
||||
UPPER(SECCODE) LIKE UPPER(:query_pattern)
|
||||
OR UPPER(SECNAME) LIKE UPPER(:query_pattern)
|
||||
OR UPPER(F001V) LIKE UPPER(:query_pattern)
|
||||
)
|
||||
AND (F011V = '正常上市' OR F010V = '013001')
|
||||
AND F003V IN ('A股', 'B股')
|
||||
ORDER BY CASE
|
||||
WHEN UPPER(SECCODE) = UPPER(:exact_query) THEN 1
|
||||
WHEN UPPER(SECNAME) = UPPER(:exact_query) THEN 2
|
||||
WHEN UPPER(F001V) = UPPER(:exact_query) THEN 3
|
||||
WHEN UPPER(SECCODE) LIKE UPPER(:prefix_pattern) THEN 4
|
||||
WHEN UPPER(SECNAME) LIKE UPPER(:prefix_pattern) THEN 5
|
||||
WHEN UPPER(F001V) LIKE UPPER(:prefix_pattern) THEN 6
|
||||
ELSE 7
|
||||
END,
|
||||
SECCODE
|
||||
LIMIT :limit
|
||||
""")
|
||||
|
||||
stocks.append({
|
||||
'stock_code': row.stock_code,
|
||||
'stock_name': row.stock_name,
|
||||
'current_price': current_price or 0, # 添加当前价格
|
||||
'pinyin_abbr': row.pinyin_abbr,
|
||||
'security_type': row.security_type,
|
||||
'exchange': row.exchange,
|
||||
'listing_status': row.listing_status
|
||||
})
|
||||
stock_result = conn.execute(stock_sql, {
|
||||
'query_pattern': f'%{query}%',
|
||||
'exact_query': query,
|
||||
'prefix_pattern': f'{query}%',
|
||||
'limit': limit
|
||||
}).fetchall()
|
||||
|
||||
for row in stock_result:
|
||||
results.append({
|
||||
'stock_code': row.stock_code,
|
||||
'stock_name': row.stock_name,
|
||||
'pinyin_abbr': row.pinyin_abbr,
|
||||
'security_type': row.security_type,
|
||||
'exchange': row.exchange,
|
||||
'listing_status': row.listing_status,
|
||||
'isIndex': False
|
||||
})
|
||||
|
||||
# 如果搜索全部,按相关性重新排序(精确匹配优先)
|
||||
if search_type == 'all':
|
||||
def sort_key(item):
|
||||
code = item['stock_code'].upper()
|
||||
name = item['stock_name'].upper()
|
||||
q = query.upper()
|
||||
# 精确匹配代码优先
|
||||
if code == q:
|
||||
return (0, not item['isIndex'], code) # 指数优先
|
||||
# 精确匹配名称
|
||||
if name == q:
|
||||
return (1, not item['isIndex'], code)
|
||||
# 前缀匹配代码
|
||||
if code.startswith(q):
|
||||
return (2, not item['isIndex'], code)
|
||||
# 前缀匹配名称
|
||||
if name.startswith(q):
|
||||
return (3, not item['isIndex'], code)
|
||||
return (4, not item['isIndex'], code)
|
||||
|
||||
results.sort(key=sort_key)
|
||||
|
||||
# 限制总数
|
||||
results = results[:limit]
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': stocks,
|
||||
'count': len(stocks)
|
||||
'data': results,
|
||||
'count': len(results)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"搜索股票/指数错误: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
|
||||
@@ -209,11 +209,14 @@ const FlexScreen: React.FC = () => {
|
||||
(security: SearchResultItem | WatchlistItem): void => {
|
||||
const code = 'stock_code' in security ? security.stock_code : security.code;
|
||||
const name = 'stock_name' in security ? security.stock_name : security.name;
|
||||
const isIndex =
|
||||
security.isIndex || code.startsWith('000') || code.startsWith('399');
|
||||
// 优先使用 API 返回的 isIndex 字段
|
||||
const isIndex = security.isIndex === true;
|
||||
|
||||
// 检查是否已存在
|
||||
if (watchlist.some(item => item.code === code)) {
|
||||
// 生成唯一标识(带后缀的完整代码)
|
||||
const fullCode = getFullCode(code, isIndex);
|
||||
|
||||
// 检查是否已存在(使用带后缀的代码比较,避免上证指数和平安银行冲突)
|
||||
if (watchlist.some(item => getFullCode(item.code, item.isIndex) === fullCode)) {
|
||||
toast({
|
||||
title: '已在自选列表中',
|
||||
status: 'info',
|
||||
@@ -227,7 +230,7 @@ const FlexScreen: React.FC = () => {
|
||||
setWatchlist(prev => [...prev, { code, name, isIndex }]);
|
||||
|
||||
toast({
|
||||
title: `已添加 ${name}`,
|
||||
title: `已添加 ${name}${isIndex ? '(指数)' : ''}`,
|
||||
status: 'success',
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
@@ -397,7 +400,7 @@ const FlexScreen: React.FC = () => {
|
||||
<List spacing={0}>
|
||||
{searchResults.map((stock, index) => (
|
||||
<ListItem
|
||||
key={stock.stock_code}
|
||||
key={`${stock.stock_code}-${stock.isIndex ? 'index' : 'stock'}`}
|
||||
px={4}
|
||||
py={2}
|
||||
cursor="pointer"
|
||||
@@ -408,9 +411,18 @@ const FlexScreen: React.FC = () => {
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text fontWeight="medium" color={textColor}>
|
||||
{stock.stock_name}
|
||||
</Text>
|
||||
<HStack spacing={2}>
|
||||
<Text fontWeight="medium" color={textColor}>
|
||||
{stock.stock_name}
|
||||
</Text>
|
||||
<Badge
|
||||
colorScheme={stock.isIndex ? 'purple' : 'blue'}
|
||||
fontSize="xs"
|
||||
variant="subtle"
|
||||
>
|
||||
{stock.isIndex ? '指数' : '股票'}
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color={subTextColor}>
|
||||
{stock.stock_code}
|
||||
</Text>
|
||||
|
||||
@@ -112,29 +112,29 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
||||
{getAlertTypeLabel(alert.alert_type)}
|
||||
</Badge>
|
||||
{/* 确认率 */}
|
||||
{isV2 && alert.confirm_ratio !== undefined && (
|
||||
{isV2 && alert.confirm_ratio != null && (
|
||||
<HStack spacing={1}>
|
||||
<Box w="30px" h="4px" bg="gray.200" borderRadius="full" overflow="hidden">
|
||||
<Box
|
||||
w={`${alert.confirm_ratio * 100}%`}
|
||||
w={`${(alert.confirm_ratio || 0) * 100}%`}
|
||||
h="100%"
|
||||
bg={alert.confirm_ratio >= 0.8 ? 'green.500' : 'orange.500'}
|
||||
bg={(alert.confirm_ratio || 0) >= 0.8 ? 'green.500' : 'orange.500'}
|
||||
/>
|
||||
</Box>
|
||||
<Text>{Math.round(alert.confirm_ratio * 100)}%</Text>
|
||||
<Text>{Math.round((alert.confirm_ratio || 0) * 100)}%</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{/* Alpha + Z-Score 简化显示 */}
|
||||
<HStack spacing={3}>
|
||||
{alert.alpha !== undefined && (
|
||||
<Text color={alert.alpha >= 0 ? 'red.500' : 'green.500'} fontWeight="medium">
|
||||
α {alert.alpha >= 0 ? '+' : ''}{alert.alpha.toFixed(2)}%
|
||||
{alert.alpha != null && (
|
||||
<Text color={(alert.alpha || 0) >= 0 ? 'red.500' : 'green.500'} fontWeight="medium">
|
||||
α {(alert.alpha || 0) >= 0 ? '+' : ''}{(alert.alpha || 0).toFixed(2)}%
|
||||
</Text>
|
||||
)}
|
||||
{isV2 && alert.alpha_zscore !== undefined && (
|
||||
<Tooltip label={`Alpha Z-Score: ${alert.alpha_zscore.toFixed(2)}σ`}>
|
||||
{isV2 && alert.alpha_zscore != null && (
|
||||
<Tooltip label={`Alpha Z-Score: ${(alert.alpha_zscore || 0).toFixed(2)}σ`}>
|
||||
<HStack spacing={0.5}>
|
||||
<Box
|
||||
w="24px"
|
||||
@@ -146,23 +146,23 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
left={alert.alpha_zscore >= 0 ? '50%' : undefined}
|
||||
right={alert.alpha_zscore < 0 ? '50%' : undefined}
|
||||
w={`${Math.min(Math.abs(alert.alpha_zscore) / 5 * 50, 50)}%`}
|
||||
left={(alert.alpha_zscore || 0) >= 0 ? '50%' : undefined}
|
||||
right={(alert.alpha_zscore || 0) < 0 ? '50%' : undefined}
|
||||
w={`${Math.min(Math.abs(alert.alpha_zscore || 0) / 5 * 50, 50)}%`}
|
||||
h="100%"
|
||||
bg={alert.alpha_zscore >= 0 ? 'red.500' : 'green.500'}
|
||||
bg={(alert.alpha_zscore || 0) >= 0 ? 'red.500' : 'green.500'}
|
||||
/>
|
||||
</Box>
|
||||
<Text color={alert.alpha_zscore >= 0 ? 'red.400' : 'green.400'}>
|
||||
{alert.alpha_zscore >= 0 ? '+' : ''}{alert.alpha_zscore.toFixed(1)}σ
|
||||
<Text color={(alert.alpha_zscore || 0) >= 0 ? 'red.400' : 'green.400'}>
|
||||
{(alert.alpha_zscore || 0) >= 0 ? '+' : ''}{(alert.alpha_zscore || 0).toFixed(1)}σ
|
||||
</Text>
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
)}
|
||||
{alert.limit_up_ratio > 0.05 && (
|
||||
{(alert.limit_up_ratio || 0) > 0.05 && (
|
||||
<HStack spacing={0.5} color="orange.500">
|
||||
<Icon as={FaFire} boxSize={3} />
|
||||
<Text>{Math.round(alert.limit_up_ratio * 100)}%</Text>
|
||||
<Text>{Math.round((alert.limit_up_ratio || 0) * 100)}%</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
@@ -193,40 +193,44 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{stocks.slice(0, 10).map((stock, idx) => (
|
||||
<Tr
|
||||
key={idx}
|
||||
cursor="pointer"
|
||||
_hover={{ bg: hoverBg }}
|
||||
onClick={(e) => handleStockClick(e, stock.code || stock.stock_code)}
|
||||
>
|
||||
<Td px={2} py={1.5}>
|
||||
<Text fontSize="xs" color="cyan.400" fontWeight="medium">
|
||||
{stock.name || stock.stock_name}
|
||||
</Text>
|
||||
</Td>
|
||||
<Td px={2} py={1.5} isNumeric>
|
||||
<Text
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
color={
|
||||
stock.change_pct > 0 ? 'red.400' :
|
||||
stock.change_pct < 0 ? 'green.400' : 'gray.400'
|
||||
}
|
||||
>
|
||||
{stock.change_pct !== undefined
|
||||
? `${stock.change_pct > 0 ? '+' : ''}${stock.change_pct.toFixed(2)}%`
|
||||
: '-'
|
||||
}
|
||||
</Text>
|
||||
</Td>
|
||||
<Td px={2} py={1.5} maxW="120px">
|
||||
<Text fontSize="xs" color="gray.500" noOfLines={1}>
|
||||
{stock.reason || '-'}
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
{stocks.slice(0, 10).map((stock, idx) => {
|
||||
const changePct = stock.change_pct;
|
||||
const hasChange = changePct != null && !isNaN(changePct);
|
||||
return (
|
||||
<Tr
|
||||
key={idx}
|
||||
cursor="pointer"
|
||||
_hover={{ bg: hoverBg }}
|
||||
onClick={(e) => handleStockClick(e, stock.code || stock.stock_code)}
|
||||
>
|
||||
<Td px={2} py={1.5}>
|
||||
<Text fontSize="xs" color="cyan.400" fontWeight="medium">
|
||||
{stock.name || stock.stock_name || '-'}
|
||||
</Text>
|
||||
</Td>
|
||||
<Td px={2} py={1.5} isNumeric>
|
||||
<Text
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
color={
|
||||
hasChange && changePct > 0 ? 'red.400' :
|
||||
hasChange && changePct < 0 ? 'green.400' : 'gray.400'
|
||||
}
|
||||
>
|
||||
{hasChange
|
||||
? `${changePct > 0 ? '+' : ''}${changePct.toFixed(2)}%`
|
||||
: '-'
|
||||
}
|
||||
</Text>
|
||||
</Td>
|
||||
<Td px={2} py={1.5} maxW="120px">
|
||||
<Text fontSize="xs" color="gray.500" noOfLines={1}>
|
||||
{stock.reason || '-'}
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{stocks.length > 10 && (
|
||||
|
||||
@@ -152,8 +152,9 @@ export const formatScore = (score) => {
|
||||
* @returns {string} 颜色代码
|
||||
*/
|
||||
export const getScoreColor = (score) => {
|
||||
if (score >= 80) return '#ff4757';
|
||||
if (score >= 60) return '#ff6348';
|
||||
if (score >= 40) return '#ffa502';
|
||||
const s = score || 0;
|
||||
if (s >= 80) return '#ff4757';
|
||||
if (s >= 60) return '#ff6348';
|
||||
if (s >= 40) return '#ffa502';
|
||||
return '#747d8c';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user