更新ios
This commit is contained in:
@@ -268,7 +268,7 @@ const PlanBubble = memo(({ content, plan }) => (
|
|||||||
));
|
));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行中气泡 - 带实时步骤进度显示
|
* 执行中气泡 - 简洁的步骤进度显示
|
||||||
*/
|
*/
|
||||||
const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, currentStepIndex }) => {
|
const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, currentStepIndex }) => {
|
||||||
const spinAnim = useRef(new Animated.Value(0)).current;
|
const spinAnim = useRef(new Animated.Value(0)).current;
|
||||||
@@ -317,6 +317,7 @@ const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, cu
|
|||||||
return (
|
return (
|
||||||
<View style={styles.agentBubbleContainer}>
|
<View style={styles.agentBubbleContainer}>
|
||||||
<View style={styles.executingBubble}>
|
<View style={styles.executingBubble}>
|
||||||
|
{/* 头部 */}
|
||||||
<View style={styles.executingHeader}>
|
<View style={styles.executingHeader}>
|
||||||
<Animated.View style={{ transform: [{ rotate: rotation }] }}>
|
<Animated.View style={{ transform: [{ rotate: rotation }] }}>
|
||||||
<Text style={styles.gearIcon}>⚙️</Text>
|
<Text style={styles.gearIcon}>⚙️</Text>
|
||||||
@@ -324,12 +325,9 @@ const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, cu
|
|||||||
<Text style={styles.executingTitle}>正在执行</Text>
|
<Text style={styles.executingTitle}>正在执行</Text>
|
||||||
<View style={styles.executingBadge}>
|
<View style={styles.executingBadge}>
|
||||||
<Text style={styles.executingBadgeText}>
|
<Text style={styles.executingBadgeText}>
|
||||||
{completedCount}{totalSteps > 0 ? ` / ${totalSteps}` : ''}
|
{completedCount} / {totalSteps}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{currentStep && (
|
|
||||||
<Text style={styles.currentStepText}>→ {currentStep.tool}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 进度条 */}
|
{/* 进度条 */}
|
||||||
@@ -344,7 +342,7 @@ const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, cu
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 步骤列表 */}
|
{/* 步骤列表 - 简洁版 */}
|
||||||
{displaySteps.length > 0 && (
|
{displaySteps.length > 0 && (
|
||||||
<View style={styles.stepResults}>
|
<View style={styles.stepResults}>
|
||||||
{displaySteps.map((step, index) => {
|
{displaySteps.map((step, index) => {
|
||||||
@@ -353,59 +351,45 @@ const ExecutingBubble = memo(({ content, plan, stepResults = [], currentStep, cu
|
|||||||
const isRunning = currentStepIndex !== null && currentStepIndex !== undefined
|
const isRunning = currentStepIndex !== null && currentStepIndex !== undefined
|
||||||
? index === currentStepIndex
|
? index === currentStepIndex
|
||||||
: index === stepResults.length && currentStep;
|
: index === stepResults.length && currentStep;
|
||||||
const isPending = !isCompleted && !isRunning;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View key={`step-${index}`} style={styles.stepResultSimple}>
|
||||||
key={`step-${index}-${step.tool}`}
|
{/* 状态指示器 */}
|
||||||
style={[
|
<View style={[
|
||||||
styles.stepResult,
|
styles.stepIndicator,
|
||||||
isCompleted && result?.status === 'success' && styles.stepResultSuccess,
|
isCompleted && result?.status === 'success' && styles.stepIndicatorSuccess,
|
||||||
isCompleted && result?.status !== 'success' && styles.stepResultFailed,
|
isCompleted && result?.status !== 'success' && styles.stepIndicatorFailed,
|
||||||
isRunning && styles.stepResultRunning,
|
isRunning && styles.stepIndicatorRunning,
|
||||||
]}
|
|
||||||
>
|
|
||||||
{/* 状态图标 */}
|
|
||||||
<View style={styles.stepStatusIcon}>
|
|
||||||
{isCompleted ? (
|
|
||||||
<Text style={[
|
|
||||||
styles.stepStatus,
|
|
||||||
result?.status === 'success' ? styles.stepSuccess : styles.stepFailed
|
|
||||||
]}>
|
]}>
|
||||||
|
{isCompleted ? (
|
||||||
|
<Text style={styles.stepIndicatorText}>
|
||||||
{result?.status === 'success' ? '✓' : '✗'}
|
{result?.status === 'success' ? '✓' : '✗'}
|
||||||
</Text>
|
</Text>
|
||||||
) : isRunning ? (
|
) : isRunning ? (
|
||||||
<Animated.View style={{ transform: [{ rotate: rotation }] }}>
|
<Animated.View style={{ transform: [{ rotate: rotation }] }}>
|
||||||
<Text style={styles.stepRunningIcon}>◐</Text>
|
<Text style={styles.stepIndicatorText}>◐</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.stepPendingIcon}>○</Text>
|
<Text style={styles.stepIndicatorTextPending}>{index + 1}</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</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={[
|
<Text
|
||||||
styles.stepName,
|
style={[
|
||||||
isPending && styles.stepNamePending,
|
styles.stepToolName,
|
||||||
isRunning && styles.stepNameRunning,
|
isCompleted && styles.stepToolNameCompleted,
|
||||||
]}>
|
isRunning && styles.stepToolNameRunning,
|
||||||
|
]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
{step.tool}
|
{step.tool}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* 执行时间 */}
|
{/* 执行时间 */}
|
||||||
{result?.execution_time && (
|
{result?.execution_time && (
|
||||||
<Text style={styles.stepTime}>
|
<Text style={styles.stepTimeSimple}>
|
||||||
{result.execution_time.toFixed(2)}s
|
{result.execution_time.toFixed(1)}s
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</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 和图表
|
* AI 响应气泡 - 支持 Markdown 和图表
|
||||||
*/
|
*/
|
||||||
const ResponseBubble = memo(({ content, isStreaming }) => {
|
const ResponseBubble = memo(({ content, isStreaming }) => {
|
||||||
const cursorAnim = useRef(new Animated.Value(0)).current;
|
const cursorAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// 过滤特殊标签
|
||||||
|
const filteredContent = filterResponseContent(content);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
const blink = Animated.loop(
|
const blink = Animated.loop(
|
||||||
@@ -448,7 +457,7 @@ const ResponseBubble = memo(({ content, isStreaming }) => {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.agentBubbleContainer}>
|
<View style={styles.agentBubbleContainer}>
|
||||||
<View style={styles.responseBubble}>
|
<View style={styles.responseBubble}>
|
||||||
<MarkdownRenderer content={content} />
|
<MarkdownRenderer content={filteredContent} />
|
||||||
{isStreaming && (
|
{isStreaming && (
|
||||||
<View style={styles.streamingIndicator}>
|
<View style={styles.streamingIndicator}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
@@ -785,93 +794,62 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
},
|
},
|
||||||
stepResults: {
|
stepResults: {
|
||||||
marginTop: 12,
|
marginTop: 10,
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: 'rgba(99, 102, 241, 0.15)',
|
|
||||||
paddingTop: 10,
|
|
||||||
},
|
},
|
||||||
stepResult: {
|
// 简洁版步骤样式
|
||||||
|
stepResultSimple: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginVertical: 3,
|
paddingVertical: 6,
|
||||||
paddingVertical: 4,
|
paddingHorizontal: 8,
|
||||||
paddingHorizontal: 6,
|
marginVertical: 2,
|
||||||
borderRadius: 6,
|
borderRadius: 8,
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
backgroundColor: 'rgba(255, 255, 255, 0.03)',
|
||||||
},
|
},
|
||||||
stepResultSuccess: {
|
stepIndicator: {
|
||||||
backgroundColor: 'rgba(16, 185, 129, 0.08)',
|
width: 22,
|
||||||
},
|
height: 22,
|
||||||
stepResultFailed: {
|
borderRadius: 11,
|
||||||
backgroundColor: 'rgba(239, 68, 68, 0.08)',
|
backgroundColor: 'rgba(107, 114, 128, 0.3)',
|
||||||
},
|
|
||||||
stepResultRunning: {
|
|
||||||
backgroundColor: 'rgba(99, 102, 241, 0.12)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(99, 102, 241, 0.3)',
|
|
||||||
},
|
|
||||||
stepStatusIcon: {
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: 6,
|
marginRight: 10,
|
||||||
},
|
},
|
||||||
stepStatus: {
|
stepIndicatorSuccess: {
|
||||||
fontSize: 14,
|
backgroundColor: 'rgba(16, 185, 129, 0.25)',
|
||||||
},
|
},
|
||||||
stepSuccess: {
|
stepIndicatorFailed: {
|
||||||
color: AgentTheme.success,
|
backgroundColor: 'rgba(239, 68, 68, 0.25)',
|
||||||
},
|
},
|
||||||
stepFailed: {
|
stepIndicatorRunning: {
|
||||||
color: AgentTheme.error,
|
backgroundColor: 'rgba(99, 102, 241, 0.3)',
|
||||||
},
|
},
|
||||||
stepRunningIcon: {
|
stepIndicatorText: {
|
||||||
fontSize: 14,
|
|
||||||
color: AgentTheme.accentSecondary,
|
|
||||||
},
|
|
||||||
stepPendingIcon: {
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AgentTheme.textMuted,
|
color: '#FFFFFF',
|
||||||
},
|
|
||||||
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,
|
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
stepName: {
|
stepIndicatorTextPending: {
|
||||||
color: AgentTheme.textSecondary,
|
fontSize: 11,
|
||||||
fontSize: 13,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
stepNamePending: {
|
|
||||||
color: AgentTheme.textMuted,
|
color: AgentTheme.textMuted,
|
||||||
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
stepNameRunning: {
|
stepToolName: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 13,
|
||||||
|
color: AgentTheme.textSecondary,
|
||||||
|
},
|
||||||
|
stepToolNameCompleted: {
|
||||||
|
color: AgentTheme.textPrimary,
|
||||||
|
},
|
||||||
|
stepToolNameRunning: {
|
||||||
color: AgentTheme.accentSecondary,
|
color: AgentTheme.accentSecondary,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
stepTime: {
|
stepTimeSimple: {
|
||||||
color: AgentTheme.textMuted,
|
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
color: AgentTheme.textMuted,
|
||||||
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
|
|
||||||
// AI 响应
|
// AI 响应
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
fetchMinuteData,
|
fetchMinuteData,
|
||||||
fetchKlineData,
|
fetchKlineData,
|
||||||
setChartType,
|
setChartType,
|
||||||
|
clearCurrentStock,
|
||||||
selectCurrentStock,
|
selectCurrentStock,
|
||||||
selectMinuteData,
|
selectMinuteData,
|
||||||
selectMinutePrevClose,
|
selectMinutePrevClose,
|
||||||
@@ -83,6 +84,20 @@ const StockDetailScreen = () => {
|
|||||||
...realtimeQuote,
|
...realtimeQuote,
|
||||||
}), [currentStock, 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 () => {
|
const loadRiseAnalysis = useCallback(async () => {
|
||||||
if (!stockCode) return;
|
if (!stockCode) return;
|
||||||
@@ -121,6 +136,16 @@ const StockDetailScreen = () => {
|
|||||||
loadRiseAnalysis();
|
loadRiseAnalysis();
|
||||||
}, [dispatch, stockCode, stockName, chartType, eventTime, loadRiseAnalysis]);
|
}, [dispatch, stockCode, stockName, chartType, eventTime, loadRiseAnalysis]);
|
||||||
|
|
||||||
|
// 股票代码变化时,清空之前的数据并重置图表类型
|
||||||
|
useEffect(() => {
|
||||||
|
// 清空之前股票的数据
|
||||||
|
dispatch(clearCurrentStock());
|
||||||
|
// 重置为分时图
|
||||||
|
dispatch(setChartType('minute'));
|
||||||
|
// 清空本地状态(涨幅分析数据)
|
||||||
|
setRiseAnalysisData([]);
|
||||||
|
}, [dispatch, stockCode]);
|
||||||
|
|
||||||
// 初始加载
|
// 初始加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadStockData();
|
loadStockData();
|
||||||
@@ -133,11 +158,10 @@ const StockDetailScreen = () => {
|
|||||||
if (type === 'minute') {
|
if (type === 'minute') {
|
||||||
dispatch(fetchMinuteData(stockCode));
|
dispatch(fetchMinuteData(stockCode));
|
||||||
} else {
|
} else {
|
||||||
if (!klineData[type] || klineData[type].length === 0) {
|
// 每次切换都重新加载数据,确保是当前股票的数据
|
||||||
dispatch(fetchKlineData({ stockCode, type, eventTime }));
|
dispatch(fetchKlineData({ stockCode, type, eventTime }));
|
||||||
}
|
}
|
||||||
}
|
}, [dispatch, stockCode, eventTime]);
|
||||||
}, [dispatch, stockCode, klineData, eventTime]);
|
|
||||||
|
|
||||||
// 返回
|
// 返回
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(() => {
|
||||||
@@ -200,7 +224,7 @@ const StockDetailScreen = () => {
|
|||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
<SafeAreaView style={styles.container} edges={['top']}>
|
||||||
{/* 价格头部 - Wind 风格 */}
|
{/* 价格头部 - Wind 风格 */}
|
||||||
<PriceHeader
|
<PriceHeader
|
||||||
stock={{ stock_code: stockCode, stock_name: stockName }}
|
stock={{ stock_code: stockCode, stock_name: displayStockName }}
|
||||||
quote={quote}
|
quote={quote}
|
||||||
isInWatchlist={inWatchlist}
|
isInWatchlist={inWatchlist}
|
||||||
onToggleWatchlist={handleToggleWatchlist}
|
onToggleWatchlist={handleToggleWatchlist}
|
||||||
|
|||||||
338
app.py
338
app.py
@@ -6074,12 +6074,13 @@ def get_watchlist_realtime():
|
|||||||
low,
|
low,
|
||||||
volume,
|
volume,
|
||||||
amt,
|
amt,
|
||||||
|
change_pct,
|
||||||
ROW_NUMBER() OVER (PARTITION BY code ORDER BY timestamp DESC) as rn
|
ROW_NUMBER() OVER (PARTITION BY code ORDER BY timestamp DESC) as rn
|
||||||
FROM stock_minute
|
FROM stock.stock_minute
|
||||||
WHERE code IN %(codes)s
|
WHERE code IN %(codes)s
|
||||||
AND timestamp >= %(start)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
|
FROM latest
|
||||||
WHERE rn = 1
|
WHERE rn = 1
|
||||||
"""
|
"""
|
||||||
@@ -6092,14 +6093,15 @@ def get_watchlist_realtime():
|
|||||||
# 构建最新价格映射
|
# 构建最新价格映射
|
||||||
latest_data_map = {}
|
latest_data_map = {}
|
||||||
for row in result:
|
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] = {
|
latest_data_map[code] = {
|
||||||
'close': float(close),
|
'close': float(close),
|
||||||
'timestamp': ts,
|
'timestamp': ts,
|
||||||
'high': float(high),
|
'high': float(high),
|
||||||
'low': float(low),
|
'low': float(low),
|
||||||
'volume': int(volume),
|
'volume': int(volume),
|
||||||
'amount': float(amt)
|
'amount': float(amt),
|
||||||
|
'change_pct': float(change_pct) if change_pct else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# 批量查询前收盘价(使用 ea_trade 表,更准确)
|
# 批量查询前收盘价(使用 ea_trade 表,更准确)
|
||||||
@@ -8145,12 +8147,13 @@ def get_stock_quotes():
|
|||||||
prev_close_map[norm_code] = base_close_map[base_code]
|
prev_close_map[norm_code] = base_close_map[base_code]
|
||||||
|
|
||||||
# 批量查询当前价格数据(从 ClickHouse)
|
# 批量查询当前价格数据(从 ClickHouse)
|
||||||
# 使用 argMax 函数获取最新价格,比窗口函数效率高很多
|
# 使用 argMax 函数获取最新价格和涨跌幅
|
||||||
batch_price_query = """
|
batch_price_query = """
|
||||||
SELECT
|
SELECT
|
||||||
code,
|
code,
|
||||||
argMax(close, timestamp) as last_price
|
argMax(close, timestamp) as last_price,
|
||||||
FROM stock_minute
|
argMax(change_pct, timestamp) as last_change_pct
|
||||||
|
FROM stock.stock_minute
|
||||||
WHERE code IN %(codes)s
|
WHERE code IN %(codes)s
|
||||||
AND timestamp >= %(start)s
|
AND timestamp >= %(start)s
|
||||||
AND timestamp <= %(end)s
|
AND timestamp <= %(end)s
|
||||||
@@ -8170,11 +8173,12 @@ def get_stock_quotes():
|
|||||||
for row in batch_data:
|
for row in batch_data:
|
||||||
code = row[0]
|
code = row[0]
|
||||||
last_price = float(row[1]) if row[1] is not None else None
|
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 change_pct is None and last_price is not None:
|
||||||
if last_price is not None and prev_close is not None and prev_close > 0:
|
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
|
change_pct = (last_price - prev_close) / prev_close * 100
|
||||||
|
|
||||||
price_data_map[code] = {
|
price_data_map[code] = {
|
||||||
@@ -8205,20 +8209,19 @@ def get_stock_quotes():
|
|||||||
for orig_code in original_codes:
|
for orig_code in original_codes:
|
||||||
norm_code = code_mapping[orig_code]
|
norm_code = code_mapping[orig_code]
|
||||||
try:
|
try:
|
||||||
# 查询当前价格
|
# 查询当前价格和涨跌幅
|
||||||
current_data = client.execute("""
|
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
|
WHERE code = %(code)s AND timestamp >= %(start)s AND timestamp <= %(end)s
|
||||||
ORDER BY timestamp DESC LIMIT 1
|
ORDER BY timestamp DESC LIMIT 1
|
||||||
""", {'code': norm_code, 'start': start_datetime, 'end': end_datetime})
|
""", {'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
|
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 change_pct is None and prev_trading_day and last_price is not None:
|
||||||
if prev_trading_day and last_price is not None:
|
|
||||||
base_code = orig_code.split('.')[0]
|
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('-', '')
|
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:
|
with engine.connect() as conn:
|
||||||
prev_result = conn.execute(text("""
|
prev_result = conn.execute(text("""
|
||||||
@@ -8227,10 +8230,7 @@ def get_stock_quotes():
|
|||||||
WHERE SECCODE = :code AND TRADEDATE = :trade_date
|
WHERE SECCODE = :code AND TRADEDATE = :trade_date
|
||||||
"""), {'code': base_code, 'trade_date': prev_day_str}).fetchone()
|
"""), {'code': base_code, 'trade_date': prev_day_str}).fetchone()
|
||||||
prev_close = float(prev_result[0]) if prev_result and prev_result[0] else None
|
prev_close = float(prev_result[0]) if prev_result and prev_result[0] else None
|
||||||
|
if prev_close is not None and prev_close > 0:
|
||||||
# 计算涨跌幅
|
|
||||||
change_pct = None
|
|
||||||
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
|
change_pct = (last_price - prev_close) / prev_close * 100
|
||||||
|
|
||||||
results[orig_code] = {
|
results[orig_code] = {
|
||||||
@@ -8865,6 +8865,10 @@ def get_stock_kline(stock_code):
|
|||||||
|
|
||||||
if chart_type == 'daily':
|
if chart_type == 'daily':
|
||||||
return get_daily_kline(stock_code, event_datetime, stock_name)
|
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':
|
elif chart_type == 'minute':
|
||||||
return get_minute_kline(stock_code, event_datetime, stock_name, skip_next_day=skip_next_day)
|
return get_minute_kline(stock_code, event_datetime, stock_name, skip_next_day=skip_next_day)
|
||||||
elif chart_type == 'timeline':
|
elif chart_type == 'timeline':
|
||||||
@@ -8958,8 +8962,8 @@ def get_batch_kline_data():
|
|||||||
|
|
||||||
# 批量查询分时数据(使用标准化代码查询 ClickHouse)
|
# 批量查询分时数据(使用标准化代码查询 ClickHouse)
|
||||||
batch_data = client.execute("""
|
batch_data = client.execute("""
|
||||||
SELECT code, timestamp, close, volume
|
SELECT code, timestamp, close, volume, amt, change_pct
|
||||||
FROM stock_minute
|
FROM stock.stock_minute
|
||||||
WHERE code IN %(codes)s
|
WHERE code IN %(codes)s
|
||||||
AND timestamp BETWEEN %(start)s AND %(end)s
|
AND timestamp BETWEEN %(start)s AND %(end)s
|
||||||
ORDER BY code, timestamp
|
ORDER BY code, timestamp
|
||||||
@@ -8969,7 +8973,7 @@ def get_batch_kline_data():
|
|||||||
'end': end_time
|
'end': end_time
|
||||||
})
|
})
|
||||||
|
|
||||||
# 按股票代码分组,同时计算均价和涨跌幅
|
# 按股票代码分组,同时计算均价
|
||||||
stock_data = {}
|
stock_data = {}
|
||||||
stock_accum = {} # 用于计算均价的累计值
|
stock_accum = {} # 用于计算均价的累计值
|
||||||
for row in batch_data:
|
for row in batch_data:
|
||||||
@@ -8977,27 +8981,25 @@ def get_batch_kline_data():
|
|||||||
base_code = norm_code.split('.')[0]
|
base_code = norm_code.split('.')[0]
|
||||||
price = float(row[2])
|
price = float(row[2])
|
||||||
volume = float(row[3])
|
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:
|
if norm_code not in stock_data:
|
||||||
stock_data[norm_code] = []
|
stock_data[norm_code] = []
|
||||||
stock_accum[norm_code] = {'total_amount': 0, 'total_volume': 0}
|
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
|
stock_accum[norm_code]['total_volume'] += volume
|
||||||
total_vol = stock_accum[norm_code]['total_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
|
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({
|
stock_data[norm_code].append({
|
||||||
'time': row[1].strftime('%H:%M'),
|
'time': row[1].strftime('%H:%M'),
|
||||||
'price': price,
|
'price': price,
|
||||||
'avg_price': round(avg_price, 2),
|
'avg_price': round(avg_price, 2),
|
||||||
'volume': volume,
|
'volume': volume,
|
||||||
'change_percent': round(change_percent, 2)
|
'change_percent': round(change_pct, 2) # 直接使用数据库中的涨跌幅
|
||||||
})
|
})
|
||||||
|
|
||||||
# 组装结果(使用原始代码作为 key 返回)
|
# 组装结果(使用原始代码作为 key 返回)
|
||||||
@@ -9147,7 +9149,7 @@ def get_latest_minute_data(stock_code):
|
|||||||
# 检查这个交易日是否有分钟数据
|
# 检查这个交易日是否有分钟数据
|
||||||
test_data = client.execute("""
|
test_data = client.execute("""
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM stock_minute
|
FROM stock.stock_minute
|
||||||
WHERE code = %(code)s
|
WHERE code = %(code)s
|
||||||
AND timestamp BETWEEN %(start)s AND %(end)s
|
AND timestamp BETWEEN %(start)s AND %(end)s
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@@ -9180,8 +9182,9 @@ def get_latest_minute_data(stock_code):
|
|||||||
low,
|
low,
|
||||||
close,
|
close,
|
||||||
volume,
|
volume,
|
||||||
amt
|
amt,
|
||||||
FROM stock_minute
|
change_pct
|
||||||
|
FROM stock.stock_minute
|
||||||
WHERE code = %(code)s
|
WHERE code = %(code)s
|
||||||
AND timestamp BETWEEN %(start)s AND %(end)s
|
AND timestamp BETWEEN %(start)s AND %(end)s
|
||||||
ORDER BY timestamp
|
ORDER BY timestamp
|
||||||
@@ -9198,7 +9201,8 @@ def get_latest_minute_data(stock_code):
|
|||||||
'low': float(row[3]),
|
'low': float(row[3]),
|
||||||
'close': float(row[4]),
|
'close': float(row[4]),
|
||||||
'volume': float(row[5]),
|
'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]
|
} for row in data]
|
||||||
|
|
||||||
return jsonify({
|
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):
|
def get_minute_kline(stock_code, event_datetime, stock_name, skip_next_day=False):
|
||||||
"""处理分钟K线数据
|
"""处理分钟K线数据
|
||||||
|
|
||||||
@@ -10202,8 +10358,8 @@ def get_minute_kline(stock_code, event_datetime, stock_name, skip_next_day=False
|
|||||||
# 获取目标日期的完整交易时段数据
|
# 获取目标日期的完整交易时段数据
|
||||||
data = client.execute("""
|
data = client.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
timestamp, open, high, low, close, volume, amt
|
timestamp, open, high, low, close, volume, amt, change_pct
|
||||||
FROM stock_minute
|
FROM stock.stock_minute
|
||||||
WHERE code = %(code)s
|
WHERE code = %(code)s
|
||||||
AND timestamp BETWEEN %(start)s
|
AND timestamp BETWEEN %(start)s
|
||||||
AND %(end)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]),
|
'low': float(row[3]),
|
||||||
'close': float(row[4]),
|
'close': float(row[4]),
|
||||||
'volume': float(row[5]),
|
'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]
|
} for row in data]
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -10282,7 +10439,7 @@ def get_timeline_data(stock_code, event_datetime, stock_name):
|
|||||||
# 如果 MySQL 没有数据,回退到 ClickHouse
|
# 如果 MySQL 没有数据,回退到 ClickHouse
|
||||||
if prev_close is None:
|
if prev_close is None:
|
||||||
prev_close_query = """
|
prev_close_query = """
|
||||||
SELECT close FROM stock_minute
|
SELECT close FROM stock.stock_minute
|
||||||
WHERE code = %(code)s AND timestamp < %(start)s
|
WHERE code = %(code)s AND timestamp < %(start)s
|
||||||
ORDER BY timestamp DESC LIMIT 1
|
ORDER BY timestamp DESC LIMIT 1
|
||||||
"""
|
"""
|
||||||
@@ -10293,11 +10450,12 @@ def get_timeline_data(stock_code, event_datetime, stock_name):
|
|||||||
if prev_close_result:
|
if prev_close_result:
|
||||||
prev_close = float(prev_close_result[0][0])
|
prev_close = float(prev_close_result[0][0])
|
||||||
|
|
||||||
|
# 查询分时数据,包含 change_pct 和 amt 用于计算均价
|
||||||
data = client.execute(
|
data = client.execute(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
timestamp, close, volume
|
timestamp, close, volume, amt, change_pct
|
||||||
FROM stock_minute
|
FROM stock.stock_minute
|
||||||
WHERE code = %(code)s
|
WHERE code = %(code)s
|
||||||
AND timestamp BETWEEN %(start)s
|
AND timestamp BETWEEN %(start)s
|
||||||
AND %(end)s
|
AND %(end)s
|
||||||
@@ -10316,19 +10474,19 @@ def get_timeline_data(stock_code, event_datetime, stock_name):
|
|||||||
for row in data:
|
for row in data:
|
||||||
price = float(row[1])
|
price = float(row[1])
|
||||||
volume = float(row[2])
|
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
|
total_volume += volume
|
||||||
avg_price = total_amount / total_volume if total_volume > 0 else price
|
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({
|
timeline_data.append({
|
||||||
'time': row[0].strftime('%H:%M'),
|
'time': row[0].strftime('%H:%M'),
|
||||||
'price': price,
|
'price': price,
|
||||||
'avg_price': avg_price,
|
'avg_price': avg_price,
|
||||||
'volume': volume,
|
'volume': volume,
|
||||||
'change_percent': change_percent,
|
'change_percent': change_pct, # 直接使用数据库中的涨跌幅
|
||||||
})
|
})
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -11414,12 +11572,13 @@ def get_events_effectiveness_stats():
|
|||||||
start_datetime = datetime.combine(target_date, dt_time(9, 30))
|
start_datetime = datetime.combine(target_date, dt_time(9, 30))
|
||||||
end_datetime = datetime.combine(target_date, dt_time(15, 0))
|
end_datetime = datetime.combine(target_date, dt_time(15, 0))
|
||||||
|
|
||||||
# 1. 从 ClickHouse 批量查询最新价格
|
# 1. 从 ClickHouse 批量查询最新价格和涨跌幅
|
||||||
batch_price_query = """
|
batch_price_query = """
|
||||||
SELECT
|
SELECT
|
||||||
code,
|
code,
|
||||||
argMax(close, timestamp) as last_price
|
argMax(close, timestamp) as last_price,
|
||||||
FROM stock_minute
|
argMax(change_pct, timestamp) as last_change_pct
|
||||||
|
FROM stock.stock_minute
|
||||||
WHERE code IN %(codes)s
|
WHERE code IN %(codes)s
|
||||||
AND timestamp >= %(start)s
|
AND timestamp >= %(start)s
|
||||||
AND timestamp <= %(end)s
|
AND timestamp <= %(end)s
|
||||||
@@ -11431,35 +11590,17 @@ def get_events_effectiveness_stats():
|
|||||||
'end': end_datetime
|
'end': end_datetime
|
||||||
})
|
})
|
||||||
|
|
||||||
# 构建价格映射
|
# 构建价格和涨跌幅映射(直接使用数据库中的 change_pct)
|
||||||
price_map = {row[0]: float(row[1]) if row[1] else None for row in batch_data}
|
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():
|
for orig_code, stock_info in unique_stocks.items():
|
||||||
norm_code = code_mapping.get(orig_code)
|
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
|
if db_change_pct is not None:
|
||||||
prev_close = prev_close_map.get(base_code)
|
stock_info['maxChg'] = round(db_change_pct, 2)
|
||||||
|
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
stock_info['maxChg'] = 0
|
stock_info['maxChg'] = 0
|
||||||
|
|
||||||
@@ -11525,8 +11666,9 @@ def get_events_effectiveness_stats():
|
|||||||
market_price_query = """
|
market_price_query = """
|
||||||
SELECT
|
SELECT
|
||||||
code,
|
code,
|
||||||
argMax(close, timestamp) as last_price
|
argMax(close, timestamp) as last_price,
|
||||||
FROM stock_minute
|
argMax(change_pct, timestamp) as last_change_pct
|
||||||
|
FROM stock.stock_minute
|
||||||
WHERE timestamp >= %(start)s
|
WHERE timestamp >= %(start)s
|
||||||
AND timestamp <= %(end)s
|
AND timestamp <= %(end)s
|
||||||
AND (
|
AND (
|
||||||
@@ -11542,22 +11684,16 @@ def get_events_effectiveness_stats():
|
|||||||
})
|
})
|
||||||
|
|
||||||
if market_data:
|
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
|
rising = 0
|
||||||
falling = 0
|
falling = 0
|
||||||
flat = 0
|
flat = 0
|
||||||
|
|
||||||
for row in market_data:
|
for row in market_data:
|
||||||
code = row[0]
|
# row: (code, last_price, last_change_pct)
|
||||||
last_price = float(row[1]) if row[1] else None
|
change_pct = float(row[2]) if row[2] is not None else None
|
||||||
base_code = code.split('.')[0]
|
|
||||||
prev_close = all_prev_close.get(base_code)
|
|
||||||
|
|
||||||
if last_price and prev_close and prev_close > 0:
|
if change_pct is not None:
|
||||||
change_pct = (last_price - prev_close) / prev_close * 100
|
|
||||||
if change_pct > 0.01: # 上涨
|
if change_pct > 0.01: # 上涨
|
||||||
rising += 1
|
rising += 1
|
||||||
elif change_pct < -0.01: # 下跌
|
elif change_pct < -0.01: # 下跌
|
||||||
@@ -17350,10 +17486,10 @@ def get_concept_stocks(concept_id):
|
|||||||
|
|
||||||
ch_codes_str = "','".join(ch_codes)
|
ch_codes_str = "','".join(ch_codes)
|
||||||
|
|
||||||
# 查询当天最新价格
|
# 查询当天最新价格和涨跌幅
|
||||||
query = f"""
|
query = f"""
|
||||||
SELECT code, close
|
SELECT code, close, change_pct
|
||||||
FROM stock_minute
|
FROM stock.stock_minute
|
||||||
WHERE code IN ('{ch_codes_str}')
|
WHERE code IN ('{ch_codes_str}')
|
||||||
AND toDate(timestamp) = today()
|
AND toDate(timestamp) = today()
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
@@ -17361,25 +17497,31 @@ def get_concept_stocks(concept_id):
|
|||||||
"""
|
"""
|
||||||
result = ch_client.execute(query)
|
result = ch_client.execute(query)
|
||||||
|
|
||||||
|
# 存储价格和涨跌幅
|
||||||
|
change_pct_map = {}
|
||||||
for row in result:
|
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:
|
if ch_code in code_mapping and close_price:
|
||||||
original_code = code_mapping[ch_code]
|
original_code = code_mapping[ch_code]
|
||||||
current_price_map[original_code] = float(close_price)
|
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:
|
except Exception as ch_err:
|
||||||
app.logger.warning(f"ClickHouse 获取价格失败: {ch_err}")
|
app.logger.warning(f"ClickHouse 获取价格失败: {ch_err}")
|
||||||
|
change_pct_map = {}
|
||||||
|
|
||||||
# 5. 计算涨跌幅并合并数据
|
# 5. 合并数据(直接使用数据库的涨跌幅)
|
||||||
result_stocks = []
|
result_stocks = []
|
||||||
for stock in stocks_info:
|
for stock in stocks_info:
|
||||||
code = stock['code']
|
code = stock['code']
|
||||||
prev_close = prev_close_map.get(code)
|
prev_close = prev_close_map.get(code)
|
||||||
current_price = current_price_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 = change_pct_map.get(code)
|
||||||
change_pct = round((current_price - prev_close) / prev_close * 100, 2)
|
if change_pct is not None:
|
||||||
|
change_pct = round(change_pct, 2)
|
||||||
|
|
||||||
result_stocks.append({
|
result_stocks.append({
|
||||||
'code': code,
|
'code': code,
|
||||||
@@ -18333,7 +18475,7 @@ def get_latest_price_from_clickhouse(stock_code):
|
|||||||
# 1. 首先尝试获取最新的分钟数据(近30天)
|
# 1. 首先尝试获取最新的分钟数据(近30天)
|
||||||
minute_query = """
|
minute_query = """
|
||||||
SELECT close, timestamp
|
SELECT close, timestamp
|
||||||
FROM stock_minute
|
FROM stock.stock_minute
|
||||||
WHERE code = %(code)s
|
WHERE code = %(code)s
|
||||||
AND timestamp >= today() - 30
|
AND timestamp >= today() - 30
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
@@ -18401,7 +18543,7 @@ def get_next_minute_price(stock_code, order_time):
|
|||||||
# 获取下单后一分钟内的数据
|
# 获取下单后一分钟内的数据
|
||||||
query = """
|
query = """
|
||||||
SELECT close, timestamp
|
SELECT close, timestamp
|
||||||
FROM stock_minute
|
FROM stock.stock_minute
|
||||||
WHERE code = %(code)s
|
WHERE code = %(code)s
|
||||||
AND timestamp \
|
AND timestamp \
|
||||||
> %(order_time)s
|
> %(order_time)s
|
||||||
@@ -18422,9 +18564,9 @@ def get_next_minute_price(stock_code, order_time):
|
|||||||
return float(result[0][0]), result[0][1]
|
return float(result[0][0]), result[0][1]
|
||||||
|
|
||||||
# 如果一分钟内没有数据,获取最近的数据
|
# 如果一分钟内没有数据,获取最近的数据
|
||||||
query = """
|
fallback_query = """
|
||||||
SELECT close, timestamp
|
SELECT close, timestamp
|
||||||
FROM stock_minute
|
FROM stock.stock_minute
|
||||||
WHERE code = %(code)s
|
WHERE code = %(code)s
|
||||||
AND timestamp \
|
AND timestamp \
|
||||||
> %(order_time)s
|
> %(order_time)s
|
||||||
@@ -18432,7 +18574,7 @@ def get_next_minute_price(stock_code, order_time):
|
|||||||
LIMIT 1 \
|
LIMIT 1 \
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = client.execute(query, {
|
result = client.execute(fallback_query, {
|
||||||
'code': stock_code,
|
'code': stock_code,
|
||||||
'order_time': order_time
|
'order_time': order_time
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ itsdangerous==2.1.2
|
|||||||
APScheduler==3.10.4
|
APScheduler==3.10.4
|
||||||
elasticsearch==8.15.0
|
elasticsearch==8.15.0
|
||||||
PyJWT==2.8.0
|
PyJWT==2.8.0
|
||||||
PyAPNs2==0.7.2
|
PyAPNs2==2.0.0
|
||||||
Reference in New Issue
Block a user