862 lines
29 KiB
JavaScript
862 lines
29 KiB
JavaScript
// 全局状态管理
|
||
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 = '<i class="fas fa-spinner fa-spin"></i> 筛选中...';
|
||
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 = '<p class="text-center">未找到符合条件的股票</p>';
|
||
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 = `
|
||
<p><strong>综合评分:</strong>${avgScore.toFixed(1)}分</p>
|
||
<p><strong>投资建议:</strong>${finalRecommendation}</p>
|
||
<p><strong>最佳策略:</strong>${bestStrategy.strategy} (${bestStrategy.score.toFixed(1)}分)</p>
|
||
<br>
|
||
<p><strong>各策略分析:</strong></p>
|
||
<ul>
|
||
`;
|
||
|
||
results.forEach(result => {
|
||
let analysis = '';
|
||
if (result.score >= 65) {
|
||
analysis = '表现优秀,符合投资标准';
|
||
} else if (result.score >= 45) {
|
||
analysis = '表现一般,可考虑关注';
|
||
} else {
|
||
analysis = '表现较差,需谨慎考虑';
|
||
}
|
||
|
||
summaryHtml += `<li><strong>${result.strategy}:</strong>${result.score.toFixed(1)}分,${analysis}</li>`;
|
||
});
|
||
|
||
summaryHtml += '</ul>';
|
||
|
||
// 添加投资建议说明
|
||
if (finalRecommendation === 'BUY') {
|
||
summaryHtml += '<p style="color: #22c55e; margin-top: 1rem;"><strong>建议买入:</strong>多数策略看好,具有较好投资价值</p>';
|
||
} else if (finalRecommendation === 'HOLD') {
|
||
summaryHtml += '<p style="color: #f59e0b; margin-top: 1rem;"><strong>建议持有:</strong>策略结果分化,建议持有观望</p>';
|
||
} else {
|
||
summaryHtml += '<p style="color: #ef4444; margin-top: 1rem;"><strong>建议卖出:</strong>多数策略不看好,存在较大风险</p>';
|
||
}
|
||
|
||
// 添加策略详细评分表格
|
||
summaryHtml += `
|
||
<div style="margin-top: 1.5rem;">
|
||
<div id="strategy-comparison-table-inline"></div>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="metric-value">${ratios[item.key].toFixed(2)}${item.suffix}</div>
|
||
<div class="metric-label">${item.label}</div>
|
||
`;
|
||
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 = '<div class="signals-analysis">';
|
||
|
||
// 买入信号详情
|
||
if (entrySignals.entry_reasons && entrySignals.entry_reasons.length > 0) {
|
||
detailsHtml += '<div class="signal-section">';
|
||
detailsHtml += '<h5 style="color: #22c55e; margin-bottom: 0.5rem;">📈 买入信号原因</h5>';
|
||
detailsHtml += '<ul>';
|
||
entrySignals.entry_reasons.forEach(reason => {
|
||
detailsHtml += `<li>${reason}</li>`;
|
||
});
|
||
detailsHtml += '</ul></div>';
|
||
}
|
||
|
||
// 卖出信号详情
|
||
if (exitSignals.exit_reasons && exitSignals.exit_reasons.length > 0) {
|
||
detailsHtml += '<div class="signal-section">';
|
||
detailsHtml += '<h5 style="color: #ef4444; margin-bottom: 0.5rem;">📉 卖出信号原因</h5>';
|
||
detailsHtml += '<ul>';
|
||
exitSignals.exit_reasons.forEach(reason => {
|
||
detailsHtml += `<li>${reason}</li>`;
|
||
});
|
||
detailsHtml += '</ul></div>';
|
||
}
|
||
|
||
// 交易建议详情
|
||
if (tradingAdvice.reasoning && tradingAdvice.reasoning.length > 0) {
|
||
detailsHtml += '<div class="signal-section">';
|
||
detailsHtml += '<h5 style="color: #3b82f6; margin-bottom: 0.5rem;">🎯 交易建议理由</h5>';
|
||
detailsHtml += '<ul>';
|
||
tradingAdvice.reasoning.forEach(reason => {
|
||
detailsHtml += `<li>${reason}</li>`;
|
||
});
|
||
detailsHtml += '</ul></div>';
|
||
}
|
||
|
||
// 市场时机分析
|
||
if (marketTiming.recommendations && marketTiming.recommendations.length > 0) {
|
||
detailsHtml += '<div class="signal-section">';
|
||
detailsHtml += '<h5 style="color: #f59e0b; margin-bottom: 0.5rem;">⏰ 市场时机分析</h5>';
|
||
detailsHtml += `<p><strong>市场状态:</strong> ${getMarketConditionText(marketTiming.market_condition)}</p>`;
|
||
detailsHtml += `<p><strong>流动性:</strong> ${getLiquidityText(marketTiming.liquidity)}</p>`;
|
||
detailsHtml += `<p><strong>趋势强度:</strong> ${getTrendStrengthText(marketTiming.trend_strength)}</p>`;
|
||
detailsHtml += '<ul>';
|
||
marketTiming.recommendations.forEach(rec => {
|
||
detailsHtml += `<li>${rec}</li>`;
|
||
});
|
||
detailsHtml += '</ul></div>';
|
||
}
|
||
|
||
// 风险提示
|
||
const warningFlags = entrySignals.warning_flags || [];
|
||
const riskFlags = exitSignals.risk_flags || [];
|
||
if (warningFlags.length > 0 || riskFlags.length > 0) {
|
||
detailsHtml += '<div class="signal-section">';
|
||
detailsHtml += '<h5 style="color: #dc2626; margin-bottom: 0.5rem;">⚠️ 风险提示</h5>';
|
||
detailsHtml += '<ul>';
|
||
[...warningFlags, ...riskFlags].forEach(flag => {
|
||
detailsHtml += `<li style="color: #dc2626;">${flag}</li>`;
|
||
});
|
||
detailsHtml += '</ul></div>';
|
||
}
|
||
|
||
detailsHtml += '</div>';
|
||
|
||
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 = `
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<span>${icon}</span>
|
||
<span>${message}</span>
|
||
</div>
|
||
`;
|
||
|
||
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;
|
||
}
|
||
}
|
||
} |