update pay ui

This commit is contained in:
2025-12-09 08:31:18 +08:00
parent e4937c2719
commit 25492caf15
26 changed files with 15577 additions and 1061 deletions

273
app.py
View File

@@ -12458,6 +12458,279 @@ def get_daily_top_concepts():
}), 500
# ==================== 热点概览 API ====================
@app.route('/api/market/hotspot-overview', methods=['GET'])
def get_hotspot_overview():
"""
获取热点概览数据(用于个股中心的热点概览图表)
返回:指数分时数据 + 概念异动标注
"""
try:
trade_date = request.args.get('date')
index_code = request.args.get('index', '000001.SH')
# 如果没有指定日期,使用最新交易日
if not trade_date:
today = date.today()
if today in trading_days_set:
trade_date = today.strftime('%Y-%m-%d')
else:
target_date = get_trading_day_near_date(today)
trade_date = target_date.strftime('%Y-%m-%d') if target_date else today.strftime('%Y-%m-%d')
# 1. 获取指数分时数据
client = get_clickhouse_client()
target_date_obj = datetime.strptime(trade_date, '%Y-%m-%d').date()
index_data = client.execute(
"""
SELECT timestamp, open, high, low, close, volume
FROM index_minute
WHERE code = %(code)s
AND toDate(timestamp) = %(date)s
ORDER BY timestamp
""",
{
'code': index_code,
'date': target_date_obj
}
)
# 获取昨收价
code_no_suffix = index_code.split('.')[0]
prev_close = None
with engine.connect() as conn:
prev_result = conn.execute(text("""
SELECT F006N FROM ea_exchangetrade
WHERE INDEXCODE = :code
AND TRADEDATE < :today
ORDER BY TRADEDATE DESC LIMIT 1
"""), {
'code': code_no_suffix,
'today': target_date_obj
}).fetchone()
if prev_result and prev_result[0]:
prev_close = float(prev_result[0])
# 格式化指数数据
index_timeline = []
for row in index_data:
ts, open_p, high_p, low_p, close_p, vol = row
change_pct = None
if prev_close and close_p:
change_pct = round((float(close_p) - prev_close) / prev_close * 100, 4)
index_timeline.append({
'time': ts.strftime('%H:%M'),
'timestamp': ts.isoformat(),
'price': float(close_p) if close_p else None,
'open': float(open_p) if open_p else None,
'high': float(high_p) if high_p else None,
'low': float(low_p) if low_p else None,
'volume': int(vol) if vol else 0,
'change_pct': change_pct
})
# 2. 获取概念异动数据
alerts = []
with engine.connect() as conn:
alert_result = conn.execute(text("""
SELECT
concept_id, concept_name, alert_time, alert_type,
change_pct, change_delta, limit_up_count, limit_up_delta,
rank_position, index_price, index_change_pct,
stock_count, concept_type, extra_info,
prev_change_pct, zscore, importance_score
FROM concept_minute_alert
WHERE trade_date = :trade_date
ORDER BY alert_time
"""), {'trade_date': trade_date})
for row in alert_result:
alert_time = row[2]
extra_info = None
if row[13]:
try:
extra_info = json.loads(row[13]) if isinstance(row[13], str) else row[13]
except:
pass
# 从 extra_info 提取 zscore 和 importance_score兼容旧数据
zscore = None
importance_score = None
if len(row) > 15:
zscore = float(row[15]) if row[15] else None
importance_score = float(row[16]) if row[16] else None
if extra_info:
zscore = zscore or extra_info.get('zscore')
importance_score = importance_score or extra_info.get('importance_score')
alerts.append({
'concept_id': row[0],
'concept_name': row[1],
'time': alert_time.strftime('%H:%M') if alert_time else None,
'timestamp': alert_time.isoformat() if alert_time else None,
'alert_type': row[3],
'change_pct': float(row[4]) if row[4] else None,
'change_delta': float(row[5]) if row[5] else None,
'limit_up_count': row[6],
'limit_up_delta': row[7],
'rank_position': row[8],
'index_price': float(row[9]) if row[9] else None,
'index_change_pct': float(row[10]) if row[10] else None,
'stock_count': row[11],
'concept_type': row[12],
'extra_info': extra_info,
'prev_change_pct': float(row[14]) if len(row) > 14 and row[14] else None,
'zscore': zscore,
'importance_score': importance_score
})
# 计算统计信息
day_high = max([d['price'] for d in index_timeline if d['price']], default=None)
day_low = min([d['price'] for d in index_timeline if d['price']], default=None)
latest_price = index_timeline[-1]['price'] if index_timeline else None
latest_change_pct = index_timeline[-1]['change_pct'] if index_timeline else None
return jsonify({
'success': True,
'data': {
'trade_date': trade_date,
'index': {
'code': index_code,
'name': '上证指数' if index_code == '000001.SH' else index_code,
'prev_close': prev_close,
'latest_price': latest_price,
'change_pct': latest_change_pct,
'high': day_high,
'low': day_low,
'timeline': index_timeline
},
'alerts': alerts,
'alert_count': len(alerts),
'alert_summary': {
'surge': len([a for a in alerts if a['alert_type'] == 'surge']),
'surge_up': len([a for a in alerts if a['alert_type'] == 'surge_up']),
'surge_down': len([a for a in alerts if a['alert_type'] == 'surge_down']),
'limit_up': len([a for a in alerts if a['alert_type'] == 'limit_up']),
'rank_jump': len([a for a in alerts if a['alert_type'] == 'rank_jump'])
}
}
})
except Exception as e:
import traceback
logger.error(f"获取热点概览数据失败: {traceback.format_exc()}")
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/market/concept-alerts', methods=['GET'])
def get_concept_alerts():
"""
获取概念异动列表(支持分页和筛选)
"""
try:
trade_date = request.args.get('date')
alert_type = request.args.get('type') # surge/limit_up/rank_jump
concept_type = request.args.get('concept_type') # leaf/lv1/lv2/lv3
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
# 构建查询条件
conditions = []
params = {'limit': limit, 'offset': offset}
if trade_date:
conditions.append("trade_date = :trade_date")
params['trade_date'] = trade_date
else:
conditions.append("trade_date = CURDATE()")
if alert_type:
conditions.append("alert_type = :alert_type")
params['alert_type'] = alert_type
if concept_type:
conditions.append("concept_type = :concept_type")
params['concept_type'] = concept_type
where_clause = " AND ".join(conditions) if conditions else "1=1"
with engine.connect() as conn:
# 获取总数
count_sql = text(f"SELECT COUNT(*) FROM concept_minute_alert WHERE {where_clause}")
total = conn.execute(count_sql, params).scalar()
# 获取数据
query_sql = text(f"""
SELECT
id, concept_id, concept_name, alert_time, alert_type, trade_date,
change_pct, prev_change_pct, change_delta,
limit_up_count, prev_limit_up_count, limit_up_delta,
rank_position, prev_rank_position, rank_delta,
index_price, index_change_pct,
stock_count, concept_type, extra_info
FROM concept_minute_alert
WHERE {where_clause}
ORDER BY alert_time DESC
LIMIT :limit OFFSET :offset
""")
result = conn.execute(query_sql, params)
alerts = []
for row in result:
extra_info = None
if row[19]:
try:
extra_info = json.loads(row[19]) if isinstance(row[19], str) else row[19]
except:
pass
alerts.append({
'id': row[0],
'concept_id': row[1],
'concept_name': row[2],
'alert_time': row[3].isoformat() if row[3] else None,
'alert_type': row[4],
'trade_date': row[5].isoformat() if row[5] else None,
'change_pct': float(row[6]) if row[6] else None,
'prev_change_pct': float(row[7]) if row[7] else None,
'change_delta': float(row[8]) if row[8] else None,
'limit_up_count': row[9],
'prev_limit_up_count': row[10],
'limit_up_delta': row[11],
'rank_position': row[12],
'prev_rank_position': row[13],
'rank_delta': row[14],
'index_price': float(row[15]) if row[15] else None,
'index_change_pct': float(row[16]) if row[16] else None,
'stock_count': row[17],
'concept_type': row[18],
'extra_info': extra_info
})
return jsonify({
'success': True,
'data': alerts,
'total': total,
'limit': limit,
'offset': offset
})
except Exception as e:
import traceback
logger.error(f"获取概念异动列表失败: {traceback.format_exc()}")
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/market/rise-analysis/<seccode>', methods=['GET'])
def get_rise_analysis(seccode):
"""获取股票涨幅分析数据(从 Elasticsearch 获取)"""