更新ios

This commit is contained in:
2026-01-18 16:37:40 +08:00
parent 7ef80716ea
commit 7d9e1174e4
4 changed files with 360 additions and 216 deletions

View File

@@ -268,7 +268,7 @@ const PlanBubble = memo(({ content, plan }) => (
));
/**
* 执行中气泡 - 带实时步骤进度显示
* 执行中气泡 - 简洁的步骤进度显示
*/
const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, currentStepIndex }) => {
const spinAnim = useRef(new Animated.Value(0)).current;
@@ -317,6 +317,7 @@ const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, cu
return (
<View style={styles.agentBubbleContainer}>
<View style={styles.executingBubble}>
{/* 头部 */}
<View style={styles.executingHeader}>
<Animated.View style={{ transform: [{ rotate: rotation }] }}>
<Text style={styles.gearIcon}></Text>
@@ -324,12 +325,9 @@ const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, cu
<Text style={styles.executingTitle}>正在执行</Text>
<View style={styles.executingBadge}>
<Text style={styles.executingBadgeText}>
{completedCount}{totalSteps > 0 ? ` / ${totalSteps}` : ''}
{completedCount} / {totalSteps}
</Text>
</View>
{currentStep && (
<Text style={styles.currentStepText}> {currentStep.tool}</Text>
)}
</View>
{/* 进度条 */}
@@ -344,7 +342,7 @@ const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, cu
</View>
)}
{/* 步骤列表 */}
{/* 步骤列表 - 简洁版 */}
{displaySteps.length > 0 && (
<View style={styles.stepResults}>
{displaySteps.map((step, index) => {
@@ -353,59 +351,45 @@ const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, cu
const isRunning = currentStepIndex !== null && currentStepIndex !== undefined
? index === currentStepIndex
: index === stepResults.length && currentStep;
const isPending = !isCompleted && !isRunning;
return (
<View
key={`step-${index}-${step.tool}`}
style={[
styles.stepResult,
isCompleted && result?.status === 'success' && styles.stepResultSuccess,
isCompleted && result?.status !== 'success' && styles.stepResultFailed,
isRunning && styles.stepResultRunning,
]}
>
{/* 状态图标 */}
<View style={styles.stepStatusIcon}>
{isCompleted ? (
<Text style={[
styles.stepStatus,
result?.status === 'success' ? styles.stepSuccess : styles.stepFailed
<View key={`step-${index}`} style={styles.stepResultSimple}>
{/* 状态指示器 */}
<View style={[
styles.stepIndicator,
isCompleted && result?.status === 'success' && styles.stepIndicatorSuccess,
isCompleted && result?.status !== 'success' && styles.stepIndicatorFailed,
isRunning && styles.stepIndicatorRunning,
]}>
{isCompleted ? (
<Text style={styles.stepIndicatorText}>
{result?.status === 'success' ? '✓' : '✗'}
</Text>
) : isRunning ? (
<Animated.View style={{ transform: [{ rotate: rotation }] }}>
<Text style={styles.stepRunningIcon}></Text>
<Text style={styles.stepIndicatorText}></Text>
</Animated.View>
) : (
<Text style={styles.stepPendingIcon}></Text>
<Text style={styles.stepIndicatorTextPending}>{index + 1}</Text>
)}
</View>
{/* 步骤序号 */}
<View style={[
styles.stepNumber,
isCompleted && result?.status === 'success' && styles.stepNumberSuccess,
isCompleted && result?.status !== 'success' && styles.stepNumberFailed,
isRunning && styles.stepNumberRunning,
]}>
<Text style={styles.stepNumberText}>{index + 1}</Text>
</View>
{/* 工具名称 */}
<Text style={[
styles.stepName,
isPending && styles.stepNamePending,
isRunning && styles.stepNameRunning,
]}>
<Text
style={[
styles.stepToolName,
isCompleted && styles.stepToolNameCompleted,
isRunning && styles.stepToolNameRunning,
]}
numberOfLines={1}
>
{step.tool}
</Text>
{/* 执行时间 */}
{result?.execution_time && (
<Text style={styles.stepTime}>
{result.execution_time.toFixed(2)}s
<Text style={styles.stepTimeSimple}>
{result.execution_time.toFixed(1)}s
</Text>
)}
</View>
@@ -418,12 +402,37 @@ const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, cu
);
});
/**
* 过滤 AI 响应中的特殊标签
* @param {string} content - 原始内容
* @returns {string} - 过滤后的内容
*/
const filterResponseContent = (content) => {
if (!content) return '';
// 过滤 minimax:tool_call 标签及其内容
// 格式: <minimax:tool_call>...</minimax:tool_call>
let filtered = content.replace(/<minimax:tool_call>[\s\S]*?<\/minimax:tool_call>/g, '');
// 过滤可能的其他特殊标签
// 格式: <tool_call>...</tool_call>
filtered = filtered.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '');
// 清理多余的空行
filtered = filtered.replace(/\n{3,}/g, '\n\n');
return filtered.trim();
};
/**
* AI 响应气泡 - 支持 Markdown 和图表
*/
const ResponseBubble = memo(({ content, isStreaming }) => {
const cursorAnim = useRef(new Animated.Value(0)).current;
// 过滤特殊标签
const filteredContent = filterResponseContent(content);
useEffect(() => {
if (isStreaming) {
const blink = Animated.loop(
@@ -448,7 +457,7 @@ const ResponseBubble = memo(({ content, isStreaming }) => {
return (
<View style={styles.agentBubbleContainer}>
<View style={styles.responseBubble}>
<MarkdownRenderer content={content} />
<MarkdownRenderer content={filteredContent} />
{isStreaming && (
<View style={styles.streamingIndicator}>
<Animated.View
@@ -785,93 +794,62 @@ const styles = StyleSheet.create({
borderRadius: 2,
},
stepResults: {
marginTop: 12,
borderTopWidth: 1,
borderTopColor: 'rgba(99, 102, 241, 0.15)',
paddingTop: 10,
marginTop: 10,
},
stepResult: {
// 简洁版步骤样式
stepResultSimple: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 3,
paddingVertical: 4,
paddingHorizontal: 6,
borderRadius: 6,
backgroundColor: 'rgba(255, 255, 255, 0.02)',
paddingVertical: 6,
paddingHorizontal: 8,
marginVertical: 2,
borderRadius: 8,
backgroundColor: 'rgba(255, 255, 255, 0.03)',
},
stepResultSuccess: {
backgroundColor: 'rgba(16, 185, 129, 0.08)',
},
stepResultFailed: {
backgroundColor: 'rgba(239, 68, 68, 0.08)',
},
stepResultRunning: {
backgroundColor: 'rgba(99, 102, 241, 0.12)',
borderWidth: 1,
borderColor: 'rgba(99, 102, 241, 0.3)',
},
stepStatusIcon: {
width: 20,
height: 20,
stepIndicator: {
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: 'rgba(107, 114, 128, 0.3)',
justifyContent: 'center',
alignItems: 'center',
marginRight: 6,
marginRight: 10,
},
stepStatus: {
fontSize: 14,
stepIndicatorSuccess: {
backgroundColor: 'rgba(16, 185, 129, 0.25)',
},
stepSuccess: {
color: AgentTheme.success,
stepIndicatorFailed: {
backgroundColor: 'rgba(239, 68, 68, 0.25)',
},
stepFailed: {
color: AgentTheme.error,
stepIndicatorRunning: {
backgroundColor: 'rgba(99, 102, 241, 0.3)',
},
stepRunningIcon: {
fontSize: 14,
color: AgentTheme.accentSecondary,
},
stepPendingIcon: {
stepIndicatorText: {
fontSize: 12,
color: AgentTheme.textMuted,
},
stepNumber: {
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: 'rgba(107, 114, 128, 0.2)',
justifyContent: 'center',
alignItems: 'center',
marginRight: 8,
},
stepNumberSuccess: {
backgroundColor: 'rgba(16, 185, 129, 0.2)',
},
stepNumberFailed: {
backgroundColor: 'rgba(239, 68, 68, 0.2)',
},
stepNumberRunning: {
backgroundColor: 'rgba(99, 102, 241, 0.2)',
},
stepNumberText: {
color: AgentTheme.textSecondary,
fontSize: 10,
color: '#FFFFFF',
fontWeight: '600',
},
stepName: {
color: AgentTheme.textSecondary,
fontSize: 13,
flex: 1,
},
stepNamePending: {
stepIndicatorTextPending: {
fontSize: 11,
color: AgentTheme.textMuted,
fontWeight: '500',
},
stepNameRunning: {
stepToolName: {
flex: 1,
fontSize: 13,
color: AgentTheme.textSecondary,
},
stepToolNameCompleted: {
color: AgentTheme.textPrimary,
},
stepToolNameRunning: {
color: AgentTheme.accentSecondary,
fontWeight: '500',
},
stepTime: {
color: AgentTheme.textMuted,
stepTimeSimple: {
fontSize: 11,
color: AgentTheme.textMuted,
marginLeft: 8,
},
// AI 响应

View File

@@ -32,6 +32,7 @@ import {
fetchMinuteData,
fetchKlineData,
setChartType,
clearCurrentStock,
selectCurrentStock,
selectMinuteData,
selectMinutePrevClose,
@@ -83,6 +84,20 @@ const StockDetailScreen = () => {
...realtimeQuote,
}), [currentStock, realtimeQuote]);
// 获取显示用的股票名称(优先使用 API 返回的名称)
const displayStockName = useMemo(() => {
// 1. 优先使用 API 返回的股票名称
if (currentStock?.stock_name) {
return currentStock.stock_name;
}
// 2. 如果路由参数的 stockName 与 stockCode 不同,使用它
if (stockName && stockName !== stockCode) {
return stockName;
}
// 3. 降级显示股票代码
return stockCode;
}, [currentStock?.stock_name, stockName, stockCode]);
// 加载涨幅分析数据
const loadRiseAnalysis = useCallback(async () => {
if (!stockCode) return;
@@ -121,6 +136,16 @@ const StockDetailScreen = () => {
loadRiseAnalysis();
}, [dispatch, stockCode, stockName, chartType, eventTime, loadRiseAnalysis]);
// 股票代码变化时,清空之前的数据并重置图表类型
useEffect(() => {
// 清空之前股票的数据
dispatch(clearCurrentStock());
// 重置为分时图
dispatch(setChartType('minute'));
// 清空本地状态(涨幅分析数据)
setRiseAnalysisData([]);
}, [dispatch, stockCode]);
// 初始加载
useEffect(() => {
loadStockData();
@@ -133,11 +158,10 @@ const StockDetailScreen = () => {
if (type === 'minute') {
dispatch(fetchMinuteData(stockCode));
} else {
if (!klineData[type] || klineData[type].length === 0) {
// 每次切换都重新加载数据,确保是当前股票的数据
dispatch(fetchKlineData({ stockCode, type, eventTime }));
}
}
}, [dispatch, stockCode, klineData, eventTime]);
}, [dispatch, stockCode, eventTime]);
// 返回
const handleBack = useCallback(() => {
@@ -200,7 +224,7 @@ const StockDetailScreen = () => {
<SafeAreaView style={styles.container} edges={['top']}>
{/* 价格头部 - Wind 风格 */}
<PriceHeader
stock={{ stock_code: stockCode, stock_name: stockName }}
stock={{ stock_code: stockCode, stock_name: displayStockName }}
quote={quote}
isInWatchlist={inWatchlist}
onToggleWatchlist={handleToggleWatchlist}

338
app.py
View File

@@ -6074,12 +6074,13 @@ def get_watchlist_realtime():
low,
volume,
amt,
change_pct,
ROW_NUMBER() OVER (PARTITION BY code ORDER BY timestamp DESC) as rn
FROM stock_minute
FROM stock.stock_minute
WHERE code IN %(codes)s
AND timestamp >= %(start)s
)
SELECT code, close, timestamp, high, low, volume, amt
SELECT code, close, timestamp, high, low, volume, amt, change_pct
FROM latest
WHERE rn = 1
"""
@@ -6092,14 +6093,15 @@ def get_watchlist_realtime():
# 构建最新价格映射
latest_data_map = {}
for row in result:
code, close, ts, high, low, volume, amt = row
code, close, ts, high, low, volume, amt, change_pct = row
latest_data_map[code] = {
'close': float(close),
'timestamp': ts,
'high': float(high),
'low': float(low),
'volume': int(volume),
'amount': float(amt)
'amount': float(amt),
'change_pct': float(change_pct) if change_pct else 0
}
# 批量查询前收盘价(使用 ea_trade 表,更准确)
@@ -8145,12 +8147,13 @@ def get_stock_quotes():
prev_close_map[norm_code] = base_close_map[base_code]
# 批量查询当前价格数据(从 ClickHouse
# 使用 argMax 函数获取最新价格,比窗口函数效率高很多
# 使用 argMax 函数获取最新价格和涨跌幅
batch_price_query = """
SELECT
code,
argMax(close, timestamp) as last_price
FROM stock_minute
argMax(close, timestamp) as last_price,
argMax(change_pct, timestamp) as last_change_pct
FROM stock.stock_minute
WHERE code IN %(codes)s
AND timestamp >= %(start)s
AND timestamp <= %(end)s
@@ -8170,11 +8173,12 @@ def get_stock_quotes():
for row in batch_data:
code = row[0]
last_price = float(row[1]) if row[1] is not None else None
prev_close = prev_close_map.get(code)
change_pct = float(row[2]) if row[2] is not None else None
# 计算涨跌幅
change_pct = None
if last_price is not None and prev_close is not None and prev_close > 0:
# 如果数据库中没有涨跌幅,使用前收盘价计算
if change_pct is None and last_price is not None:
prev_close = prev_close_map.get(code)
if prev_close is not None and prev_close > 0:
change_pct = (last_price - prev_close) / prev_close * 100
price_data_map[code] = {
@@ -8205,20 +8209,19 @@ def get_stock_quotes():
for orig_code in original_codes:
norm_code = code_mapping[orig_code]
try:
# 查询当前价格
# 查询当前价格和涨跌幅
current_data = client.execute("""
SELECT close FROM stock_minute
SELECT close, change_pct FROM stock.stock_minute
WHERE code = %(code)s AND timestamp >= %(start)s AND timestamp <= %(end)s
ORDER BY timestamp DESC LIMIT 1
""", {'code': norm_code, 'start': start_datetime, 'end': end_datetime})
last_price = float(current_data[0][0]) if current_data and current_data[0] and current_data[0][0] else None
change_pct = float(current_data[0][1]) if current_data and current_data[0] and len(current_data[0]) > 1 and current_data[0][1] else None
# 查询前一交易日收盘价
prev_close = None
if prev_trading_day and last_price is not None:
# 如果数据库中没有涨跌幅,使用前收盘价计算
if change_pct is None and prev_trading_day and last_price is not None:
base_code = orig_code.split('.')[0]
# ea_trade 表的 TRADEDATE 格式是 YYYYMMDD无连字符
prev_day_str = prev_trading_day.strftime('%Y%m%d') if hasattr(prev_trading_day, 'strftime') else str(prev_trading_day).replace('-', '')
with engine.connect() as conn:
prev_result = conn.execute(text("""
@@ -8227,10 +8230,7 @@ def get_stock_quotes():
WHERE SECCODE = :code AND TRADEDATE = :trade_date
"""), {'code': base_code, 'trade_date': prev_day_str}).fetchone()
prev_close = float(prev_result[0]) if prev_result and prev_result[0] else None
# 计算涨跌幅
change_pct = None
if last_price is not None and prev_close is not None and prev_close > 0:
if prev_close is not None and prev_close > 0:
change_pct = (last_price - prev_close) / prev_close * 100
results[orig_code] = {
@@ -8865,6 +8865,10 @@ def get_stock_kline(stock_code):
if chart_type == 'daily':
return get_daily_kline(stock_code, event_datetime, stock_name)
elif chart_type == 'weekly':
return get_weekly_kline(stock_code, event_datetime, stock_name)
elif chart_type == 'monthly':
return get_monthly_kline(stock_code, event_datetime, stock_name)
elif chart_type == 'minute':
return get_minute_kline(stock_code, event_datetime, stock_name, skip_next_day=skip_next_day)
elif chart_type == 'timeline':
@@ -8958,8 +8962,8 @@ def get_batch_kline_data():
# 批量查询分时数据(使用标准化代码查询 ClickHouse
batch_data = client.execute("""
SELECT code, timestamp, close, volume
FROM stock_minute
SELECT code, timestamp, close, volume, amt, change_pct
FROM stock.stock_minute
WHERE code IN %(codes)s
AND timestamp BETWEEN %(start)s AND %(end)s
ORDER BY code, timestamp
@@ -8969,7 +8973,7 @@ def get_batch_kline_data():
'end': end_time
})
# 按股票代码分组,同时计算均价和涨跌幅
# 按股票代码分组,同时计算均价
stock_data = {}
stock_accum = {} # 用于计算均价的累计值
for row in batch_data:
@@ -8977,27 +8981,25 @@ def get_batch_kline_data():
base_code = norm_code.split('.')[0]
price = float(row[2])
volume = float(row[3])
amount = float(row[4]) if row[4] else price * volume
change_pct = float(row[5]) if row[5] else 0
if norm_code not in stock_data:
stock_data[norm_code] = []
stock_accum[norm_code] = {'total_amount': 0, 'total_volume': 0}
# 累计计算均价
stock_accum[norm_code]['total_amount'] += price * volume
# 累计计算均价(使用真实成交额)
stock_accum[norm_code]['total_amount'] += amount
stock_accum[norm_code]['total_volume'] += volume
total_vol = stock_accum[norm_code]['total_volume']
avg_price = stock_accum[norm_code]['total_amount'] / total_vol if total_vol > 0 else price
# 计算涨跌幅
prev_close = prev_close_map.get(base_code)
change_percent = ((price - prev_close) / prev_close * 100) if prev_close and prev_close > 0 else 0
stock_data[norm_code].append({
'time': row[1].strftime('%H:%M'),
'price': price,
'avg_price': round(avg_price, 2),
'volume': volume,
'change_percent': round(change_percent, 2)
'change_percent': round(change_pct, 2) # 直接使用数据库中的涨跌幅
})
# 组装结果(使用原始代码作为 key 返回)
@@ -9147,7 +9149,7 @@ def get_latest_minute_data(stock_code):
# 检查这个交易日是否有分钟数据
test_data = client.execute("""
SELECT COUNT(*)
FROM stock_minute
FROM stock.stock_minute
WHERE code = %(code)s
AND timestamp BETWEEN %(start)s AND %(end)s
LIMIT 1
@@ -9180,8 +9182,9 @@ def get_latest_minute_data(stock_code):
low,
close,
volume,
amt
FROM stock_minute
amt,
change_pct
FROM stock.stock_minute
WHERE code = %(code)s
AND timestamp BETWEEN %(start)s AND %(end)s
ORDER BY timestamp
@@ -9198,7 +9201,8 @@ def get_latest_minute_data(stock_code):
'low': float(row[3]),
'close': float(row[4]),
'volume': float(row[5]),
'amount': float(row[6])
'amount': float(row[6]),
'change_pct': float(row[7]) if row[7] else 0
} for row in data]
return jsonify({
@@ -10167,6 +10171,158 @@ def get_daily_kline(stock_code, event_datetime, stock_name):
})
def get_weekly_kline(stock_code, event_datetime, stock_name):
"""处理周K线数据 - 从日K数据聚合计算"""
stock_code = stock_code.split('.')[0]
with engine.connect() as conn:
# 获取3年的日K数据然后在 Python 端聚合为周K
kline_sql = """
SELECT
t.TRADEDATE,
CAST(t.F003N AS FLOAT) as open,
CAST(t.F007N AS FLOAT) as close,
CAST(t.F005N AS FLOAT) as high,
CAST(t.F006N AS FLOAT) as low,
CAST(t.F004N AS FLOAT) as volume
FROM ea_trade t
WHERE t.SECCODE = :stock_code
AND t.TRADEDATE BETWEEN DATE_SUB(:trade_date, INTERVAL 3 YEAR)
AND DATE_ADD(:trade_date, INTERVAL 30 DAY)
ORDER BY t.TRADEDATE
"""
result = conn.execute(text(kline_sql), {
"stock_code": stock_code,
"trade_date": event_datetime.date()
}).fetchall()
if not result:
return jsonify({
'error': 'No data available',
'code': stock_code,
'name': stock_name,
'data': [],
'trade_date': event_datetime.date().strftime('%Y-%m-%d'),
'type': 'weekly'
})
# 按周聚合日K数据
from collections import defaultdict
weekly_data = defaultdict(list)
for row in result:
# 使用 ISO 周:(年, 周数)
week_key = row.TRADEDATE.isocalendar()[:2]
weekly_data[week_key].append({
'date': row.TRADEDATE,
'open': float(row.open) if row.open else 0,
'high': float(row.high) if row.high else 0,
'low': float(row.low) if row.low else 0,
'close': float(row.close) if row.close else 0,
'volume': float(row.volume) if row.volume else 0
})
# 聚合为周K
kline_data = []
for week_key in sorted(weekly_data.keys()):
days = weekly_data[week_key]
days.sort(key=lambda x: x['date'])
kline_data.append({
'time': days[0]['date'].strftime('%Y-%m-%d'), # 周一日期
'open': days[0]['open'], # 周一开盘价
'high': max(d['high'] for d in days), # 周内最高
'low': min(d['low'] for d in days), # 周内最低
'close': days[-1]['close'], # 周五收盘价
'volume': sum(d['volume'] for d in days) # 周成交量
})
return jsonify({
'code': stock_code,
'name': stock_name,
'data': kline_data,
'trade_date': event_datetime.date().strftime('%Y-%m-%d'),
'type': 'weekly',
'is_history': True
})
def get_monthly_kline(stock_code, event_datetime, stock_name):
"""处理月K线数据 - 从日K数据聚合计算"""
stock_code = stock_code.split('.')[0]
with engine.connect() as conn:
# 获取5年的日K数据然后在 Python 端聚合为月K
kline_sql = """
SELECT
t.TRADEDATE,
CAST(t.F003N AS FLOAT) as open,
CAST(t.F007N AS FLOAT) as close,
CAST(t.F005N AS FLOAT) as high,
CAST(t.F006N AS FLOAT) as low,
CAST(t.F004N AS FLOAT) as volume
FROM ea_trade t
WHERE t.SECCODE = :stock_code
AND t.TRADEDATE BETWEEN DATE_SUB(:trade_date, INTERVAL 5 YEAR)
AND DATE_ADD(:trade_date, INTERVAL 30 DAY)
ORDER BY t.TRADEDATE
"""
result = conn.execute(text(kline_sql), {
"stock_code": stock_code,
"trade_date": event_datetime.date()
}).fetchall()
if not result:
return jsonify({
'error': 'No data available',
'code': stock_code,
'name': stock_name,
'data': [],
'trade_date': event_datetime.date().strftime('%Y-%m-%d'),
'type': 'monthly'
})
# 按月聚合日K数据
from collections import defaultdict
monthly_data = defaultdict(list)
for row in result:
# 月份键:(年, 月)
month_key = (row.TRADEDATE.year, row.TRADEDATE.month)
monthly_data[month_key].append({
'date': row.TRADEDATE,
'open': float(row.open) if row.open else 0,
'high': float(row.high) if row.high else 0,
'low': float(row.low) if row.low else 0,
'close': float(row.close) if row.close else 0,
'volume': float(row.volume) if row.volume else 0
})
# 聚合为月K
kline_data = []
for month_key in sorted(monthly_data.keys()):
days = monthly_data[month_key]
days.sort(key=lambda x: x['date'])
kline_data.append({
'time': days[0]['date'].strftime('%Y-%m-%d'), # 月初日期
'open': days[0]['open'], # 月初开盘价
'high': max(d['high'] for d in days), # 月内最高
'low': min(d['low'] for d in days), # 月内最低
'close': days[-1]['close'], # 月末收盘价
'volume': sum(d['volume'] for d in days) # 月成交量
})
return jsonify({
'code': stock_code,
'name': stock_name,
'data': kline_data,
'trade_date': event_datetime.date().strftime('%Y-%m-%d'),
'type': 'monthly',
'is_history': True
})
def get_minute_kline(stock_code, event_datetime, stock_name, skip_next_day=False):
"""处理分钟K线数据
@@ -10202,8 +10358,8 @@ def get_minute_kline(stock_code, event_datetime, stock_name, skip_next_day=False
# 获取目标日期的完整交易时段数据
data = client.execute("""
SELECT
timestamp, open, high, low, close, volume, amt
FROM stock_minute
timestamp, open, high, low, close, volume, amt, change_pct
FROM stock.stock_minute
WHERE code = %(code)s
AND timestamp BETWEEN %(start)s
AND %(end)s
@@ -10221,7 +10377,8 @@ def get_minute_kline(stock_code, event_datetime, stock_name, skip_next_day=False
'low': float(row[3]),
'close': float(row[4]),
'volume': float(row[5]),
'amount': float(row[6])
'amount': float(row[6]),
'change_pct': float(row[7]) if row[7] else 0
} for row in data]
return jsonify({
@@ -10282,7 +10439,7 @@ def get_timeline_data(stock_code, event_datetime, stock_name):
# 如果 MySQL 没有数据,回退到 ClickHouse
if prev_close is None:
prev_close_query = """
SELECT close FROM stock_minute
SELECT close FROM stock.stock_minute
WHERE code = %(code)s AND timestamp < %(start)s
ORDER BY timestamp DESC LIMIT 1
"""
@@ -10293,11 +10450,12 @@ def get_timeline_data(stock_code, event_datetime, stock_name):
if prev_close_result:
prev_close = float(prev_close_result[0][0])
# 查询分时数据,包含 change_pct 和 amt 用于计算均价
data = client.execute(
"""
SELECT
timestamp, close, volume
FROM stock_minute
timestamp, close, volume, amt, change_pct
FROM stock.stock_minute
WHERE code = %(code)s
AND timestamp BETWEEN %(start)s
AND %(end)s
@@ -10316,19 +10474,19 @@ def get_timeline_data(stock_code, event_datetime, stock_name):
for row in data:
price = float(row[1])
volume = float(row[2])
total_amount += price * volume
amount = float(row[3]) if row[3] else price * volume
change_pct = float(row[4]) if row[4] else 0
total_amount += amount
total_volume += volume
avg_price = total_amount / total_volume if total_volume > 0 else price
# 计算涨跌幅
change_percent = ((price - prev_close) / prev_close * 100) if prev_close else 0
timeline_data.append({
'time': row[0].strftime('%H:%M'),
'price': price,
'avg_price': avg_price,
'volume': volume,
'change_percent': change_percent,
'change_percent': change_pct, # 直接使用数据库中的涨跌幅
})
return jsonify({
@@ -11414,12 +11572,13 @@ def get_events_effectiveness_stats():
start_datetime = datetime.combine(target_date, dt_time(9, 30))
end_datetime = datetime.combine(target_date, dt_time(15, 0))
# 1. 从 ClickHouse 批量查询最新价格
# 1. 从 ClickHouse 批量查询最新价格和涨跌幅
batch_price_query = """
SELECT
code,
argMax(close, timestamp) as last_price
FROM stock_minute
argMax(close, timestamp) as last_price,
argMax(change_pct, timestamp) as last_change_pct
FROM stock.stock_minute
WHERE code IN %(codes)s
AND timestamp >= %(start)s
AND timestamp <= %(end)s
@@ -11431,35 +11590,17 @@ def get_events_effectiveness_stats():
'end': end_datetime
})
# 构建价格映射
# 构建价格和涨跌幅映射(直接使用数据库中的 change_pct
price_map = {row[0]: float(row[1]) if row[1] else None for row in batch_data}
change_pct_map = {row[0]: float(row[2]) if row[2] is not None else None for row in batch_data}
# 2. 批量获取前收盘价(使用 Redis 缓存)
prev_date_str = None
try:
target_idx = trading_days.index(target_date)
if target_idx > 0:
prev_trading_day = trading_days[target_idx - 1]
prev_date_str = prev_trading_day.strftime('%Y%m%d')
except (ValueError, IndexError):
pass
prev_close_map = {}
if prev_date_str:
base_codes = list(set([code.split('.')[0] for code in unique_stocks.keys() if code]))
prev_close_map = get_cached_prev_close(base_codes, prev_date_str)
# 3. 计算涨跌幅并更新
# 直接使用数据库返回的涨跌幅更新
for orig_code, stock_info in unique_stocks.items():
norm_code = code_mapping.get(orig_code)
base_code = orig_code.split('.')[0] if orig_code else ''
db_change_pct = change_pct_map.get(norm_code) if norm_code else None
last_price = price_map.get(norm_code) if norm_code else None
prev_close = prev_close_map.get(base_code)
if last_price is not None and prev_close is not None and prev_close > 0:
change_pct = (last_price - prev_close) / prev_close * 100
stock_info['maxChg'] = round(change_pct, 2)
if db_change_pct is not None:
stock_info['maxChg'] = round(db_change_pct, 2)
else:
stock_info['maxChg'] = 0
@@ -11525,8 +11666,9 @@ def get_events_effectiveness_stats():
market_price_query = """
SELECT
code,
argMax(close, timestamp) as last_price
FROM stock_minute
argMax(close, timestamp) as last_price,
argMax(change_pct, timestamp) as last_change_pct
FROM stock.stock_minute
WHERE timestamp >= %(start)s
AND timestamp <= %(end)s
AND (
@@ -11542,22 +11684,16 @@ def get_events_effectiveness_stats():
})
if market_data:
# 提取股票代码并获取前收盘价
all_base_codes = [row[0].split('.')[0] for row in market_data]
all_prev_close = get_cached_prev_close(all_base_codes, prev_date_str)
# 直接使用数据库返回的涨跌幅统计
rising = 0
falling = 0
flat = 0
for row in market_data:
code = row[0]
last_price = float(row[1]) if row[1] else None
base_code = code.split('.')[0]
prev_close = all_prev_close.get(base_code)
# row: (code, last_price, last_change_pct)
change_pct = float(row[2]) if row[2] is not None else None
if last_price and prev_close and prev_close > 0:
change_pct = (last_price - prev_close) / prev_close * 100
if change_pct is not None:
if change_pct > 0.01: # 上涨
rising += 1
elif change_pct < -0.01: # 下跌
@@ -17350,10 +17486,10 @@ def get_concept_stocks(concept_id):
ch_codes_str = "','".join(ch_codes)
# 查询当天最新价格
# 查询当天最新价格和涨跌幅
query = f"""
SELECT code, close
FROM stock_minute
SELECT code, close, change_pct
FROM stock.stock_minute
WHERE code IN ('{ch_codes_str}')
AND toDate(timestamp) = today()
ORDER BY timestamp DESC
@@ -17361,25 +17497,31 @@ def get_concept_stocks(concept_id):
"""
result = ch_client.execute(query)
# 存储价格和涨跌幅
change_pct_map = {}
for row in result:
ch_code, close_price = row
ch_code, close_price, db_change_pct = row
if ch_code in code_mapping and close_price:
original_code = code_mapping[ch_code]
current_price_map[original_code] = float(close_price)
if db_change_pct is not None:
change_pct_map[original_code] = float(db_change_pct)
except Exception as ch_err:
app.logger.warning(f"ClickHouse 获取价格失败: {ch_err}")
change_pct_map = {}
# 5. 计算涨跌幅并合并数据
# 5. 合并数据(直接使用数据库的涨跌幅)
result_stocks = []
for stock in stocks_info:
code = stock['code']
prev_close = prev_close_map.get(code)
current_price = current_price_map.get(code)
change_pct = None
if prev_close and current_price and prev_close > 0:
change_pct = round((current_price - prev_close) / prev_close * 100, 2)
# 优先使用数据库返回的涨跌幅
change_pct = change_pct_map.get(code)
if change_pct is not None:
change_pct = round(change_pct, 2)
result_stocks.append({
'code': code,
@@ -18333,7 +18475,7 @@ def get_latest_price_from_clickhouse(stock_code):
# 1. 首先尝试获取最新的分钟数据近30天
minute_query = """
SELECT close, timestamp
FROM stock_minute
FROM stock.stock_minute
WHERE code = %(code)s
AND timestamp >= today() - 30
ORDER BY timestamp DESC
@@ -18401,7 +18543,7 @@ def get_next_minute_price(stock_code, order_time):
# 获取下单后一分钟内的数据
query = """
SELECT close, timestamp
FROM stock_minute
FROM stock.stock_minute
WHERE code = %(code)s
AND timestamp \
> %(order_time)s
@@ -18422,9 +18564,9 @@ def get_next_minute_price(stock_code, order_time):
return float(result[0][0]), result[0][1]
# 如果一分钟内没有数据,获取最近的数据
query = """
fallback_query = """
SELECT close, timestamp
FROM stock_minute
FROM stock.stock_minute
WHERE code = %(code)s
AND timestamp \
> %(order_time)s
@@ -18432,7 +18574,7 @@ def get_next_minute_price(stock_code, order_time):
LIMIT 1 \
"""
result = client.execute(query, {
result = client.execute(fallback_query, {
'code': stock_code,
'order_time': order_time
})

View File

@@ -26,4 +26,4 @@ itsdangerous==2.1.2
APScheduler==3.10.4
elasticsearch==8.15.0
PyJWT==2.8.0
PyAPNs2==0.7.2
PyAPNs2==2.0.0