update pay promo

This commit is contained in:
2026-02-05 17:13:44 +08:00
parent 7ca9742a12
commit a819beb1f2
3 changed files with 327 additions and 3 deletions

211
app.py
View File

@@ -17583,6 +17583,217 @@ def get_market_heatmap():
}), 500 }), 500
@app.route('/api/market/realtime-stats', methods=['GET'])
def get_market_realtime_stats():
"""
获取市场实时统计数据(从 ClickHouse 实时行情表)
返回:大盘涨跌幅、涨停/跌停数、多空对比、成交额等
"""
from clickhouse_driver import Client as CHClient
try:
ch_client = CHClient(
host=os.environ.get('CLICKHOUSE_HOST', 'localhost'),
port=int(os.environ.get('CLICKHOUSE_PORT', 9000)),
user=os.environ.get('CLICKHOUSE_USER', 'default'),
password=os.environ.get('CLICKHOUSE_PASSWORD', ''),
database='stock'
)
result = {
'success': True,
'data': {
'index': None, # 大盘指数
'limit_up_count': 0, # 涨停数
'limit_down_count': 0, # 跌停数
'rising_count': 0, # 上涨家数
'falling_count': 0, # 下跌家数
'flat_count': 0, # 平盘家数
'total_amount': 0, # 总成交额
'continuous_limit': [], # 连板股
'update_time': None
}
}
# 1. 获取上证指数实时数据
try:
index_query = """
SELECT
security_id,
current_index,
prev_close,
open_index,
high_index,
low_index,
amount,
trade_time
FROM stock.szse_index_realtime
WHERE trade_date = today()
AND security_id = '399001'
ORDER BY trade_time DESC
LIMIT 1
"""
index_data = ch_client.execute(index_query)
if index_data:
row = index_data[0]
prev_close = float(row[2]) if row[2] else 0
current = float(row[1]) if row[1] else 0
change_pct = ((current - prev_close) / prev_close * 100) if prev_close else 0
result['data']['index'] = {
'code': '399001.SZ',
'name': '深证成指',
'current': current,
'prev_close': prev_close,
'change_pct': round(change_pct, 2),
'open': float(row[3]) if row[3] else 0,
'high': float(row[4]) if row[4] else 0,
'low': float(row[5]) if row[5] else 0,
}
result['data']['update_time'] = str(row[7]) if row[7] else None
except Exception as e:
app.logger.warning(f"获取指数实时数据失败: {e}")
# 2. 统计深交所股票涨跌情况
try:
szse_stats_query = """
SELECT
security_id,
last_price,
prev_close,
upper_limit_price,
lower_limit_price,
amount
FROM stock.szse_stock_realtime
WHERE trade_date = today()
AND last_price > 0
AND prev_close > 0
ORDER BY security_id, trade_time DESC
LIMIT 1 BY security_id
"""
szse_data = ch_client.execute(szse_stats_query)
for row in szse_data:
last_price = float(row[1])
prev_close = float(row[2])
upper_limit = float(row[3]) if row[3] else None
lower_limit = float(row[4]) if row[4] else None
amount = float(row[5]) if row[5] else 0
change_pct = (last_price - prev_close) / prev_close * 100
# 统计涨跌
if change_pct > 0.01:
result['data']['rising_count'] += 1
elif change_pct < -0.01:
result['data']['falling_count'] += 1
else:
result['data']['flat_count'] += 1
# 判断涨停/跌停
if upper_limit and abs(last_price - upper_limit) < 0.01:
result['data']['limit_up_count'] += 1
elif lower_limit and abs(last_price - lower_limit) < 0.01:
result['data']['limit_down_count'] += 1
result['data']['total_amount'] += amount
except Exception as e:
app.logger.warning(f"统计深交所数据失败: {e}")
# 3. 统计上交所股票涨跌情况
try:
sse_stats_query = """
SELECT
security_id,
last_price,
prev_close,
upper_limit_price,
lower_limit_price,
amount
FROM stock.sse_stock_realtime
WHERE trade_date = today()
AND last_price > 0
AND prev_close > 0
ORDER BY security_id, trade_time DESC
LIMIT 1 BY security_id
"""
sse_data = ch_client.execute(sse_stats_query)
for row in sse_data:
last_price = float(row[1])
prev_close = float(row[2])
upper_limit = float(row[3]) if row[3] else None
lower_limit = float(row[4]) if row[4] else None
amount = float(row[5]) if row[5] else 0
change_pct = (last_price - prev_close) / prev_close * 100
if change_pct > 0.01:
result['data']['rising_count'] += 1
elif change_pct < -0.01:
result['data']['falling_count'] += 1
else:
result['data']['flat_count'] += 1
if upper_limit and abs(last_price - upper_limit) < 0.01:
result['data']['limit_up_count'] += 1
elif lower_limit and abs(last_price - lower_limit) < 0.01:
result['data']['limit_down_count'] += 1
result['data']['total_amount'] += amount
except Exception as e:
app.logger.warning(f"统计上交所数据失败: {e}")
# 4. 尝试获取上证指数(补充)
try:
sh_index_query = """
SELECT
seccode,
close as current_price,
pre_close,
open,
high,
low,
amount,
tradetime
FROM stock.index_minute
WHERE tradetime >= today()
AND seccode = '000001.SH'
ORDER BY tradetime DESC
LIMIT 1
"""
sh_data = ch_client.execute(sh_index_query)
if sh_data:
row = sh_data[0]
prev_close = float(row[2]) if row[2] else 0
current = float(row[1]) if row[1] else 0
change_pct = ((current - prev_close) / prev_close * 100) if prev_close else 0
result['data']['index'] = {
'code': '000001.SH',
'name': '上证指数',
'current': current,
'prev_close': prev_close,
'change_pct': round(change_pct, 2),
'open': float(row[3]) if row[3] else 0,
'high': float(row[4]) if row[4] else 0,
'low': float(row[5]) if row[5] else 0,
}
if row[7]:
result['data']['update_time'] = str(row[7])
except Exception as e:
app.logger.warning(f"获取上证指数失败: {e}")
return jsonify(result)
except Exception as e:
app.logger.error(f"获取实时市场统计失败: {e}")
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/market/statistics', methods=['GET']) @app.route('/api/market/statistics', methods=['GET'])
def get_market_statistics(): def get_market_statistics():
"""获取市场统计数据从ea_blocktrading表""" """获取市场统计数据从ea_blocktrading表"""

View File

@@ -4967,10 +4967,17 @@ async def shutdown_event():
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
import os
# 生产环境禁用 reload开发环境限制监控目录
is_dev = os.environ.get("ENV", "production") == "development"
uvicorn.run( uvicorn.run(
"mcp_server:app", "mcp_server:app",
host="0.0.0.0", host="0.0.0.0",
port=8900, port=8900,
reload=True, reload=is_dev,
reload_dirs=["."] if is_dev else None, # 只监控当前目录的 py 文件
reload_includes=["*.py"] if is_dev else None,
reload_excludes=["node_modules/*", ".git/*", "build/*", "data/*"] if is_dev else None,
log_level="info" log_level="info"
) )

View File

@@ -199,7 +199,7 @@ const StockOverview: React.FC = () => {
} }
}, []); }, []);
// 获取市场统计数据 // 获取市场统计数据(历史数据,用于非实时场景)
const fetchMarketStats = useCallback( const fetchMarketStats = useCallback(
async (date: string | null = null) => { async (date: string | null = null) => {
try { try {
@@ -238,6 +238,57 @@ const StockOverview: React.FC = () => {
[trackMarketStatsViewed] [trackMarketStatsViewed]
); );
// 获取实时市场统计数据(盘中实时刷新)
const fetchRealtimeStats = useCallback(async () => {
try {
const response = await fetch(`${getApiBase()}/api/market/realtime-stats`);
const data = await response.json();
if (data.success && data.data) {
const realtimeData = data.data;
// 更新涨停/跌停统计
setLimitStats((prev) => ({
...prev,
limitUpCount: realtimeData.limit_up_count || 0,
limitDownCount: realtimeData.limit_down_count || 0,
}));
// 更新市场统计(涨跌家数、成交额)
setMarketStats((prev) => ({
...(prev || {}),
rising_count: realtimeData.rising_count || 0,
falling_count: realtimeData.falling_count || 0,
total_amount: realtimeData.total_amount || prev?.total_amount || 0,
}));
// 更新大盘指数数据
if (realtimeData.index) {
setHotspotData((prev) => ({
...(prev || {}),
index: {
code: realtimeData.index.code,
name: realtimeData.index.name,
current: realtimeData.index.current,
change_pct: realtimeData.index.change_pct,
prev_close: realtimeData.index.prev_close,
},
}));
}
logger.debug('StockOverview', '实时统计数据更新', {
limitUp: realtimeData.limit_up_count,
limitDown: realtimeData.limit_down_count,
rising: realtimeData.rising_count,
falling: realtimeData.falling_count,
updateTime: realtimeData.update_time,
});
}
} catch (error) {
logger.error('StockOverview', 'fetchRealtimeStats', error);
}
}, []);
// 查看概念详情 // 查看概念详情
const handleConceptClick = useCallback( const handleConceptClick = useCallback(
(concept: Concept, rank: number = 0) => { (concept: Concept, rank: number = 0) => {
@@ -284,7 +335,62 @@ const StockOverview: React.FC = () => {
fetchTopConcepts(); fetchTopConcepts();
fetchHeatmapData(); fetchHeatmapData();
fetchMarketStats(); fetchMarketStats();
}, [fetchTopConcepts, fetchHeatmapData, fetchMarketStats]); fetchRealtimeStats(); // 初始获取实时数据
}, [fetchTopConcepts, fetchHeatmapData, fetchMarketStats, fetchRealtimeStats]);
// 盘中定时刷新实时数据每30秒
useEffect(() => {
// 判断是否在交易时间内
const isTradeTime = () => {
const now = new Date();
const day = now.getDay();
const hour = now.getHours();
const minute = now.getMinutes();
const timeNum = hour * 100 + minute;
// 周一到周五9:15-11:30 或 13:00-15:00
if (day >= 1 && day <= 5) {
if ((timeNum >= 915 && timeNum <= 1130) || (timeNum >= 1300 && timeNum <= 1500)) {
return true;
}
}
return false;
};
// 只在交易时间内刷新
let intervalId: NodeJS.Timeout | null = null;
const startRefresh = () => {
if (isTradeTime()) {
intervalId = setInterval(() => {
if (isTradeTime()) {
fetchRealtimeStats();
} else {
// 非交易时间,停止刷新
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
}, 30000); // 30秒刷新一次
}
};
startRefresh();
// 每分钟检查是否进入交易时间
const checkInterval = setInterval(() => {
if (isTradeTime() && !intervalId) {
fetchRealtimeStats();
startRefresh();
}
}, 60000);
return () => {
if (intervalId) clearInterval(intervalId);
clearInterval(checkInterval);
};
}, [fetchRealtimeStats]);
return ( return (
<Box minH="100vh" bg={bgColor} position="relative" overflow="hidden"> <Box minH="100vh" bg={bgColor} position="relative" overflow="hidden">