// 全局状态管理
const AppState = {
currentTab: 'screening',
loading: false
};
// 全局图表实例管理
const ChartInstances = {
scoringChart: null,
industryChart: null
};
// DOM 加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
// 应用初始化
function initializeApp() {
setupNavigation();
setupEventListeners();
setupRangeSliders();
}
// 导航设置
function setupNavigation() {
const navLinks = document.querySelectorAll('.nav-link');
const tabContents = document.querySelectorAll('.tab-content');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetTab = link.getAttribute('data-tab');
// 更新导航状态
navLinks.forEach(nav => nav.classList.remove('active'));
link.classList.add('active');
// 显示对应内容
tabContents.forEach(content => content.classList.remove('active'));
document.getElementById(targetTab).classList.add('active');
AppState.currentTab = targetTab;
});
});
}
// 事件监听器设置
function setupEventListeners() {
// 股票筛选
document.getElementById('screening-btn').addEventListener('click', handleScreening);
// 综合分析
document.getElementById('analysis-btn').addEventListener('click', handleComprehensiveAnalysis);
// 键盘事件
document.addEventListener('keydown', handleKeyPress);
}
// 范围滑块设置
function setupRangeSliders() {
// 移除范围滑块设置,因为筛选页面不再使用滑块
}
// 股票筛选处理
let isScreeningInProgress = false; // 防重复提交标志
async function handleScreening() {
// 防止重复点击
if (isScreeningInProgress) {
showToast('筛选正在进行中,请稍候...', 'warning');
return;
}
const minScore = 60; // 固定最低评分为60分
const limit = 50; // 固定结果数量为50个
// 综合筛选:使用三种策略综合评估
const data = { strategy: 'comprehensive', min_score: minScore, limit };
isScreeningInProgress = true;
const screeningBtn = document.getElementById('screening-btn');
const originalText = screeningBtn.innerHTML;
screeningBtn.innerHTML = ' 筛选中...';
screeningBtn.disabled = true;
showLoading('正在进行综合筛选...');
try {
const response = await fetch('/api/screen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
displayScreeningResults(result.results);
showToast('筛选成功!', 'success');
} else {
throw new Error(result.error || '筛选失败');
}
} catch (error) {
showToast(`筛选失败: ${error.message}`, 'error');
} finally {
isScreeningInProgress = false;
screeningBtn.innerHTML = originalText;
screeningBtn.disabled = false;
hideLoading();
}
}
// 显示筛选结果
function displayScreeningResults(results) {
const resultsSection = document.getElementById('screening-results');
const tableContainer = document.getElementById('screening-table');
if (!results || results.length === 0) {
tableContainer.innerHTML = '
未找到符合条件的股票
';
resultsSection.style.display = 'block';
return;
}
// 检查是否是综合评分结果
const isComprehensive = results[0].value_score !== undefined;
// 创建表格
let headers, tableData;
if (isComprehensive) {
headers = ['股票代码', '名称', '行业', '综合评分', '价值评分', '成长评分', '技术评分', '建议'];
tableData = results.map(stock => [
stock.ts_code,
stock.name,
stock.industry,
stock.score.toFixed(1),
stock.value_score.toFixed(1),
stock.growth_score.toFixed(1),
stock.technical_score.toFixed(1),
stock.recommendation
]);
} else {
headers = ['股票代码', '名称', '行业', '评分', '建议'];
tableData = results.map(stock => [
stock.ts_code,
stock.name,
stock.industry,
stock.score.toFixed(1),
stock.recommendation
]);
}
const table = createDataTable(headers, tableData);
tableContainer.innerHTML = '';
tableContainer.appendChild(table);
// 创建图表
createScoringChart(results.slice(0, 10));
createIndustryChart(results);
resultsSection.style.display = 'block';
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
// 综合分析处理 - 合并多策略分析
async function handleComprehensiveAnalysis() {
const tsCodeInput = document.getElementById('stock-code').value.trim();
if (!tsCodeInput) {
showToast('请输入股票代码', 'warning');
return;
}
// 格式化股票代码:自动添加交易所后缀
const tsCode = formatStockCode(tsCodeInput);
const strategies = ['value', 'growth', 'technical'];
const results = [];
showLoading('正在进行综合分析...');
try {
// 并行获取三种策略的分析结果
for (const strategy of strategies) {
const response = await fetch('/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ts_code: tsCode, strategy })
});
const result = await response.json();
if (response.ok) {
results.push({
strategy: getStrategyName(strategy),
rawStrategy: strategy,
score: result.score,
recommendation: result.recommendation,
data: result
});
}
}
if (results.length > 0) {
displayComprehensiveResults(results, tsCode);
showToast('综合分析完成!', 'success');
} else {
throw new Error('无法获取分析数据');
}
} catch (error) {
showToast(`分析失败: ${error.message}`, 'error');
} finally {
hideLoading();
}
}
// 显示股票基本信息
function displayStockBasicInfo(results, tsCode) {
// 从价值策略结果中获取基本信息(因为价值策略通常有最完整的财务数据)
const valueData = results.find(r => r.rawStrategy === 'value') || results[0];
const stockData = valueData.data;
// 显示股票代码
document.getElementById('stock-code-value').textContent = tsCode;
// 显示股票名称
const stockName = stockData.name || stockData.stock_name || '--';
document.getElementById('stock-name-value').textContent = stockName;
// 显示行业信息
const industry = stockData.industry || '--';
document.getElementById('stock-industry-value').textContent = industry;
// 显示当前价格
const currentPrice = stockData.current_price || stockData.price;
if (currentPrice) {
document.getElementById('stock-price-value').textContent = `¥${currentPrice.toFixed(2)}`;
} else {
document.getElementById('stock-price-value').textContent = '--';
}
// 显示市值
const marketCap = stockData.market_cap;
if (marketCap) {
const marketCapBillion = (marketCap / 100000000).toFixed(2);
document.getElementById('stock-market-cap-value').textContent = `${marketCapBillion}亿`;
} else {
document.getElementById('stock-market-cap-value').textContent = '--';
}
// 显示市盈率
const peRatio = stockData.financial_ratios?.pe_ratio;
if (peRatio && peRatio !== Infinity && peRatio > 0) {
document.getElementById('stock-pe-value').textContent = peRatio.toFixed(2);
} else {
document.getElementById('stock-pe-value').textContent = '--';
}
}
// 显示综合分析结果
function displayComprehensiveResults(results, tsCode) {
const resultsSection = document.getElementById('analysis-results');
// 显示股票基本信息
displayStockBasicInfo(results, tsCode);
// 计算综合指标
const avgScore = results.reduce((sum, r) => sum + r.score, 0) / results.length;
const bestStrategy = results.reduce((best, current) =>
current.score > best.score ? current : best
);
// 综合建议逻辑
const buyCount = results.filter(r => r.recommendation === 'BUY').length;
const holdCount = results.filter(r => r.recommendation === 'HOLD').length;
let finalRecommendation;
let confidenceLevel;
if (buyCount >= 2) {
finalRecommendation = 'BUY';
confidenceLevel = buyCount === 3 ? 'HIGH' : 'MEDIUM';
} else if (buyCount + holdCount >= 2) {
finalRecommendation = 'HOLD';
confidenceLevel = 'MEDIUM';
} else {
finalRecommendation = 'SELL';
confidenceLevel = results.filter(r => r.recommendation === 'SELL').length >= 2 ? 'MEDIUM' : 'LOW';
}
// 显示综合投资评级
displayInvestmentRating(avgScore, bestStrategy, finalRecommendation, confidenceLevel);
// 显示分析总结(包含策略详情表格)
displayAnalysisSummary(results, avgScore, finalRecommendation, bestStrategy);
// 显示财务指标 (使用价值策略的数据)
const valueData = results.find(r => r.rawStrategy === 'value');
if (valueData && valueData.data.financial_ratios) {
displayFinancialRatios(valueData.data.financial_ratios);
}
// 检查是否有技术分析的交易信号
const technicalData = results.find(r => r.rawStrategy === 'technical');
if (technicalData && technicalData.data.trading_signals) {
displayTradingSignals(technicalData.data.trading_signals);
document.getElementById('trading-signals-section').style.display = 'block';
} else {
document.getElementById('trading-signals-section').style.display = 'none';
}
resultsSection.style.display = 'block';
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
// 显示综合投资评级
function displayInvestmentRating(avgScore, bestStrategy, finalRecommendation, confidenceLevel) {
// 显示综合评级指标
document.getElementById('best-strategy-value').textContent = bestStrategy.strategy;
document.getElementById('avg-score-value').textContent = avgScore.toFixed(1) + '分';
document.getElementById('final-recommendation-value').textContent = finalRecommendation;
document.getElementById('confidence-level-value').textContent =
confidenceLevel === 'HIGH' ? '高' :
confidenceLevel === 'MEDIUM' ? '中' : '低';
}
// 显示分析总结
function displayAnalysisSummary(results, avgScore, finalRecommendation, bestStrategy) {
const container = document.getElementById('summary-content');
let summaryHtml = `
综合评分:${avgScore.toFixed(1)}分
投资建议:${finalRecommendation}
最佳策略:${bestStrategy.strategy} (${bestStrategy.score.toFixed(1)}分)
各策略分析:
`;
results.forEach(result => {
let analysis = '';
if (result.score >= 65) {
analysis = '表现优秀,符合投资标准';
} else if (result.score >= 45) {
analysis = '表现一般,可考虑关注';
} else {
analysis = '表现较差,需谨慎考虑';
}
summaryHtml += `- ${result.strategy}:${result.score.toFixed(1)}分,${analysis}
`;
});
summaryHtml += '
';
// 添加投资建议说明
if (finalRecommendation === 'BUY') {
summaryHtml += '建议买入:多数策略看好,具有较好投资价值
';
} else if (finalRecommendation === 'HOLD') {
summaryHtml += '建议持有:策略结果分化,建议持有观望
';
} else {
summaryHtml += '建议卖出:多数策略不看好,存在较大风险
';
}
// 添加策略详细评分表格
summaryHtml += `
`;
container.innerHTML = summaryHtml;
// 创建策略对比表格
createStrategyComparisonTable(results, 'strategy-comparison-table-inline');
}
// 创建策略对比表格
function createStrategyComparisonTable(results, containerId = 'strategy-comparison-table') {
const container = document.getElementById(containerId);
const headers = ['投资策略', '评分', '投资建议', '策略特点'];
const rows = results.map(r => [
r.strategy,
r.score.toFixed(1) + '分',
r.recommendation,
getStrategyDescription(r.rawStrategy)
]);
const table = createDataTable(headers, rows);
container.innerHTML = '';
container.appendChild(table);
}
// 获取策略描述
function getStrategyDescription(strategy) {
const descriptions = {
'value': '注重估值合理性,适合稳健投资',
'growth': '关注成长潜力,适合追求高收益',
'technical': '基于技术指标,适合短期交易'
};
return descriptions[strategy] || '';
}
// 显示财务指标
function displayFinancialRatios(ratios) {
const container = document.getElementById('financial-ratios');
const ratioItems = [
{ key: 'roe', label: 'ROE (净资产收益率)', suffix: '%' },
{ key: 'roa', label: 'ROA (总资产收益率)', suffix: '%' },
{ key: 'gross_margin', label: '毛利率', suffix: '%' },
{ key: 'net_margin', label: '净利率', suffix: '%' },
{ key: 'current_ratio', label: '流动比率', suffix: '' },
{ key: 'debt_ratio', label: '负债比率', suffix: '%' }
];
const grid = document.createElement('div');
grid.className = 'metrics-grid';
ratioItems.forEach(item => {
if (ratios[item.key] !== undefined) {
const card = document.createElement('div');
card.className = 'metric-card';
card.innerHTML = `
${ratios[item.key].toFixed(2)}${item.suffix}
${item.label}
`;
grid.appendChild(card);
}
});
container.innerHTML = '';
container.appendChild(grid);
}
// 显示交易信号分析
function displayTradingSignals(tradingSignals) {
const entrySignals = tradingSignals.entry_signals || {};
const exitSignals = tradingSignals.exit_signals || {};
const stopLossTakeProfit = tradingSignals.stop_loss_take_profit || {};
const tradingAdvice = tradingSignals.trading_advice || {};
const marketTiming = tradingSignals.market_timing || {};
// 显示主要信号指标
document.getElementById('entry-signal-value').textContent =
getSignalDisplayText(entrySignals.overall_signal) + ` (${entrySignals.signal_strength || 0}%)`;
document.getElementById('exit-signal-value').textContent =
getSignalDisplayText(exitSignals.overall_signal) + ` (${exitSignals.signal_strength || 0}%)`;
document.getElementById('trading-action-value').textContent =
getActionDisplayText(tradingAdvice.action);
document.getElementById('risk-level-value').textContent =
getRiskDisplayText(tradingAdvice.risk_assessment);
// 显示买入价格和卖出价格建议
const currentPrice = tradingSignals.current_price || 0;
const entryPriceData = tradingSignals.entry_price || {};
const exitPriceData = tradingSignals.exit_price || {};
// 根据操作建议显示相应的价格
const action = tradingAdvice.action;
document.getElementById('entry-price-value').textContent =
(action === 'BUY' || action === 'CONSIDER_BUY') && entryPriceData.recommended ?
`¥${entryPriceData.recommended}` :
(action === 'BUY' || action === 'CONSIDER_BUY') && currentPrice > 0 ?
`¥${currentPrice.toFixed(2)}` : '--';
document.getElementById('exit-price-value').textContent =
(action === 'SELL') && exitPriceData.recommended ?
`¥${exitPriceData.recommended}` :
(action === 'SELL') && currentPrice > 0 ?
`¥${currentPrice.toFixed(2)}` : '--';
// 显示止盈止损信息(只有在非观望状态时才显示)
const stopLoss = stopLossTakeProfit.stop_loss || {};
const takeProfit = stopLossTakeProfit.take_profit || {};
const isWaitAction = tradingAdvice.action === 'WAIT';
document.getElementById('stop-loss-value').textContent =
(isWaitAction || !stopLoss.recommended) ? '--' : `¥${stopLoss.recommended}`;
document.getElementById('take-profit-value').textContent =
(isWaitAction || !takeProfit.recommended) ? '--' : `¥${takeProfit.recommended}`;
document.getElementById('risk-reward-ratio-value').textContent =
(isWaitAction || !stopLossTakeProfit.risk_reward_ratio) ? '--' : `1:${stopLossTakeProfit.risk_reward_ratio}`;
document.getElementById('position-size-value').textContent =
(isWaitAction || !stopLossTakeProfit.position_sizing_suggestion) ? '--' :
`${stopLossTakeProfit.position_sizing_suggestion.suggested_position}%`;
// 显示详细信号分析
displaySignalsDetails(entrySignals, exitSignals, tradingAdvice, marketTiming);
}
// 显示信号详情
function displaySignalsDetails(entrySignals, exitSignals, tradingAdvice, marketTiming) {
const container = document.getElementById('signals-content');
let detailsHtml = '';
// 买入信号详情
if (entrySignals.entry_reasons && entrySignals.entry_reasons.length > 0) {
detailsHtml += '
';
detailsHtml += '
📈 买入信号原因
';
detailsHtml += '
';
entrySignals.entry_reasons.forEach(reason => {
detailsHtml += `- ${reason}
`;
});
detailsHtml += '
';
}
// 卖出信号详情
if (exitSignals.exit_reasons && exitSignals.exit_reasons.length > 0) {
detailsHtml += '
';
detailsHtml += '
📉 卖出信号原因
';
detailsHtml += '
';
exitSignals.exit_reasons.forEach(reason => {
detailsHtml += `- ${reason}
`;
});
detailsHtml += '
';
}
// 交易建议详情
if (tradingAdvice.reasoning && tradingAdvice.reasoning.length > 0) {
detailsHtml += '
';
detailsHtml += '
🎯 交易建议理由
';
detailsHtml += '
';
tradingAdvice.reasoning.forEach(reason => {
detailsHtml += `- ${reason}
`;
});
detailsHtml += '
';
}
// 市场时机分析
if (marketTiming.recommendations && marketTiming.recommendations.length > 0) {
detailsHtml += '
';
detailsHtml += '
⏰ 市场时机分析
';
detailsHtml += `
市场状态: ${getMarketConditionText(marketTiming.market_condition)}
`;
detailsHtml += `
流动性: ${getLiquidityText(marketTiming.liquidity)}
`;
detailsHtml += `
趋势强度: ${getTrendStrengthText(marketTiming.trend_strength)}
`;
detailsHtml += '
';
marketTiming.recommendations.forEach(rec => {
detailsHtml += `- ${rec}
`;
});
detailsHtml += '
';
}
// 风险提示
const warningFlags = entrySignals.warning_flags || [];
const riskFlags = exitSignals.risk_flags || [];
if (warningFlags.length > 0 || riskFlags.length > 0) {
detailsHtml += '
';
detailsHtml += '
⚠️ 风险提示
';
detailsHtml += '
';
[...warningFlags, ...riskFlags].forEach(flag => {
detailsHtml += `- ${flag}
`;
});
detailsHtml += '
';
}
detailsHtml += '
';
container.innerHTML = detailsHtml;
}
// 信号显示文本转换函数
function getSignalDisplayText(signal) {
const signalMap = {
'STRONG_BUY': '强烈买入',
'BUY': '买入',
'WEAK_BUY': '弱买入',
'NEUTRAL': '中性',
'WEAK_SELL': '弱卖出',
'SELL': '卖出',
'STRONG_SELL': '强烈卖出'
};
return signalMap[signal] || '未知';
}
function getActionDisplayText(action) {
const actionMap = {
'BUY': '买入',
'SELL': '卖出',
'CONSIDER_BUY': '考虑买入',
'WAIT': '观望',
'HOLD': '持有'
};
return actionMap[action] || '观望';
}
function getRiskDisplayText(risk) {
const riskMap = {
'LOW': '低风险',
'MEDIUM': '中等风险',
'HIGH': '高风险'
};
return riskMap[risk] || '中等风险';
}
function getMarketConditionText(condition) {
const conditionMap = {
'NORMAL': '正常',
'VOLATILE': '高波动',
'QUIET': '平静'
};
return conditionMap[condition] || '正常';
}
function getLiquidityText(liquidity) {
const liquidityMap = {
'POOR': '流动性差',
'GOOD': '流动性良好',
'EXCELLENT': '流动性优秀'
};
return liquidityMap[liquidity] || '流动性良好';
}
function getTrendStrengthText(strength) {
const strengthMap = {
'WEAK': '趋势较弱',
'MEDIUM': '趋势中等',
'STRONG': '趋势较强'
};
return strengthMap[strength] || '趋势中等';
}
// 策略对比处理 - 已移除,合并到综合分析中
// 此函数已被 handleComprehensiveAnalysis 替代
// 显示对比结果 - 已移除,合并到综合分析中
// 此函数已被 displayComprehensiveResults 替代
// 工具函数
function formatStockCode(code) {
// 移除所有空格和非数字字符,保留原有后缀
const cleanCode = code.replace(/\s+/g, '').toUpperCase();
// 如果已经包含交易所后缀,直接返回
if (cleanCode.includes('.SZ') || cleanCode.includes('.SH')) {
return cleanCode;
}
// 只包含数字的情况,自动判断交易所
const numericCode = cleanCode.replace(/[^0-9]/g, '');
if (numericCode.length === 6) {
// 深交所:000xxx, 002xxx, 300xxx
if (numericCode.startsWith('000') || numericCode.startsWith('002') || numericCode.startsWith('300')) {
return numericCode + '.SZ';
}
// 上交所:600xxx, 601xxx, 603xxx, 688xxx
else if (numericCode.startsWith('60') || numericCode.startsWith('688')) {
return numericCode + '.SH';
}
}
// 默认返回原输入加.SZ(深交所较多)
return numericCode + '.SZ';
}
function getStrategyName(strategy) {
const names = {
'value': '价值投资',
'growth': '成长投资',
'technical': '技术分析'
};
return names[strategy] || strategy;
}
function getStrategyIcon(strategy) {
const icons = {
'价值投资': '💰',
'成长投资': '🚀',
'技术分析': '📈'
};
return icons[strategy] || '📊';
}
// 创建数据表格
function createDataTable(headers, rows) {
const table = document.createElement('table');
table.className = 'data-table';
// 创建表头
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
headers.forEach(header => {
const th = document.createElement('th');
th.textContent = header;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// 创建表体
const tbody = document.createElement('tbody');
rows.forEach(row => {
const tr = document.createElement('tr');
row.forEach(cell => {
const td = document.createElement('td');
td.textContent = cell;
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
return table;
}
// 图表创建函数
function createScoringChart(data) {
const ctx = document.getElementById('scoring-chart').getContext('2d');
// 销毁已存在的图表
if (ChartInstances.scoringChart) {
ChartInstances.scoringChart.destroy();
}
ChartInstances.scoringChart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.map(item => item.name),
datasets: [{
label: '评分',
data: data.map(item => item.score),
backgroundColor: '#000000', /* 纯黑色 */
borderColor: '#000000',
borderWidth: 1,
borderRadius: 4
}]
},
options: {
responsive: true,
plugins: {
title: { display: true, text: '股票评分排行' },
legend: { display: false }
},
scales: {
y: { beginAtZero: true, max: 100 }
}
}
});
}
function createIndustryChart(data) {
const ctx = document.getElementById('industry-chart').getContext('2d');
// 销毁已存在的图表
if (ChartInstances.industryChart) {
ChartInstances.industryChart.destroy();
}
const industries = {};
data.forEach(item => {
industries[item.industry] = (industries[item.industry] || 0) + 1;
});
ChartInstances.industryChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: Object.keys(industries),
datasets: [{
data: Object.values(industries),
backgroundColor: [
'#000000', '#333333', '#666666', '#999999',
'#cccccc', '#444444', '#777777', '#aaaaaa'
]
}]
},
options: {
responsive: true,
plugins: {
title: { display: true, text: '行业分布' }
}
}
});
}
// UI 辅助函数
function showLoading(message = '加载中...') {
const loading = document.getElementById('loading');
loading.querySelector('p').textContent = message;
loading.style.display = 'flex';
AppState.loading = true;
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
AppState.loading = false;
}
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icon = getToastIcon(type);
toast.innerHTML = `
${icon}
${message}
`;
container.appendChild(toast);
// 自动移除
setTimeout(() => {
toast.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => container.removeChild(toast), 300);
}, 3000);
}
function getToastIcon(type) {
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: 'ℹ️'
};
return icons[type] || icons.info;
}
// 键盘快捷键处理
function handleKeyPress(e) {
if (AppState.loading) return;
// Ctrl + 数字键切换标签
if (e.ctrlKey) {
switch(e.key) {
case '1':
e.preventDefault();
document.querySelector('[data-tab="screening"]').click();
break;
case '2':
e.preventDefault();
document.querySelector('[data-tab="analysis"]').click();
break;
}
}
// Enter 键执行当前标签的主要操作
if (e.key === 'Enter' && e.ctrlKey) {
e.preventDefault();
switch(AppState.currentTab) {
case 'screening':
document.getElementById('screening-btn').click();
break;
case 'analysis':
document.getElementById('analysis-btn').click();
break;
}
}
}