update pay promo
This commit is contained in:
211
app.py
211
app.py
@@ -17583,6 +17583,217 @@ def get_market_heatmap():
|
||||
}), 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'])
|
||||
def get_market_statistics():
|
||||
"""获取市场统计数据(从ea_blocktrading表)"""
|
||||
|
||||
@@ -4967,10 +4967,17 @@ async def shutdown_event():
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
import os
|
||||
# 生产环境禁用 reload,开发环境限制监控目录
|
||||
is_dev = os.environ.get("ENV", "production") == "development"
|
||||
|
||||
uvicorn.run(
|
||||
"mcp_server:app",
|
||||
host="0.0.0.0",
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -199,7 +199,7 @@ const StockOverview: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取市场统计数据
|
||||
// 获取市场统计数据(历史数据,用于非实时场景)
|
||||
const fetchMarketStats = useCallback(
|
||||
async (date: string | null = null) => {
|
||||
try {
|
||||
@@ -238,6 +238,57 @@ const StockOverview: React.FC = () => {
|
||||
[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(
|
||||
(concept: Concept, rank: number = 0) => {
|
||||
@@ -284,7 +335,62 @@ const StockOverview: React.FC = () => {
|
||||
fetchTopConcepts();
|
||||
fetchHeatmapData();
|
||||
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 (
|
||||
<Box minH="100vh" bg={bgColor} position="relative" overflow="hidden">
|
||||
|
||||
Reference in New Issue
Block a user