update pay promo
This commit is contained in:
211
app.py
211
app.py
@@ -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表)"""
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user