stockany2 / app /web /templates /etf_analysis.html
fiewolf1000's picture
Upload 67 files
0f10134 verified
{% extends "layout.html" %}
{% block title %}ETF 分析{% endblock %}
{% block content %}
<div class="container mt-4">
<h1 class="mb-4">ETF 综合分析</h1>
<!-- 输入区域 -->
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">输入ETF代码</h5>
<div class="input-group">
<input type="text" id="etf-code-input" class="form-control" placeholder="例如: 510300">
<button id="analyze-btn" class="btn btn-primary">开始分析</button>
</div>
<small class="form-text text-muted">输入您想分析的ETF基金代码,然后点击“开始分析”按钮。</small>
</div>
</div>
<!-- 加载与状态显示 -->
<div id="status-container" class="mb-4" style="display: none;">
<div class="d-flex align-items-center">
<strong>正在分析中...</strong>
<div class="spinner-border ms-auto" role="status" aria-hidden="true"></div>
</div>
<div class="progress mt-2">
<div id="progress-bar" class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div>
</div>
<!-- 错误提示 -->
<div id="error-alert" class="alert alert-danger" style="display: none;" role="alert"></div>
<!-- 结果显示区域 -->
<div id="results-container" class="mt-4" style="display: none;">
<!-- Tab 导航 -->
<ul class="nav nav-tabs" id="analysisTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="summary-tab" data-bs-toggle="tab" data-bs-target="#summary" type="button" role="tab" aria-controls="summary" aria-selected="true">AI总结与概览</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="performance-tab" data-bs-toggle="tab" data-bs-target="#performance" type="button" role="tab" aria-controls="performance" aria-selected="false">市场表现</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="fund-flow-tab" data-bs-toggle="tab" data-bs-target="#fund-flow" type="button" role="tab" aria-controls="fund-flow" aria-selected="false">资金流向</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="risk-tab" data-bs-toggle="tab" data-bs-target="#risk" type="button" role="tab" aria-controls="risk" aria-selected="false">风险与跟踪</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="holdings-tab" data-bs-toggle="tab" data-bs-target="#holdings" type="button" role="tab" aria-controls="holdings" aria-selected="false">持仓分析</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="sector-tab" data-bs-toggle="tab" data-bs-target="#sector" type="button" role="tab" aria-controls="sector" aria-selected="false">板块分析</button>
</li>
</ul>
<!-- Tab 内容 -->
<div class="tab-content" id="analysisTabContent">
<!-- AI总结与概览 -->
<div class="tab-pane fade show active" id="summary" role="tabpanel" aria-labelledby="summary-tab">
<div class="row mt-3">
<div class="col-md-8">
<div class="card">
<div class="card-header">AI 综合诊断</div>
<div class="card-body" id="ai-summary-content"><p>AI分析摘要将显示在这里。</p></div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">基本信息</div>
<div class="card-body" id="basic-info-content"><p>ETF基本信息将显示在这里。</p></div>
</div>
</div>
</div>
</div>
<!-- 市场表现 -->
<div class="tab-pane fade" id="performance" role="tabpanel" aria-labelledby="performance-tab">
<div class="card mt-3">
<div class="card-header">市场表现与技术分析</div>
<div class="card-body" id="performance-content"><p>回报率、技术指标图表将显示在这里。</p></div>
</div>
</div>
<!-- 资金流向 -->
<div class="tab-pane fade" id="fund-flow" role="tabpanel" aria-labelledby="fund-flow-tab">
<div class="card mt-3">
<div class="card-header">资金流向分析</div>
<div class="card-body" id="fund-flow-content"><p>资金流向图表将显示在这里。</p></div>
</div>
</div>
<!-- 风险与跟踪 -->
<div class="tab-pane fade" id="risk" role="tabpanel" aria-labelledby="risk-tab">
<div class="card mt-3">
<div class="card-header">风险与跟踪能力分析</div>
<div class="card-body" id="risk-content"><p>波动率、跟踪误差、溢价率将显示在这里。</p></div>
</div>
</div>
<!-- 持仓分析 -->
<div class="tab-pane fade" id="holdings" role="tabpanel" aria-labelledby="holdings-tab">
<div class="card mt-3">
<div class="card-header">持仓分析</div>
<div class="card-body" id="holdings-content"><p>前十大持仓表格将显示在这里。</p></div>
</div>
</div>
<!-- 板块分析 -->
<div class="tab-pane fade" id="sector" role="tabpanel" aria-labelledby="sector-tab">
<div class="card mt-3">
<div class="card-header">板块深度分析</div>
<div class="card-body" id="sector-content"><p>板块景气度、估值水平将显示在这里。</p></div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
let taskId = null;
let pollingInterval = null;
// Override showError from layout to use the local error alert
function showError(message) {
$('#error-alert').text(message).show();
}
$('#analyze-btn').click(function() {
const etfCode = $('#etf-code-input').val().trim();
if (!etfCode) {
showError('请输入有效的ETF代码。');
return;
}
// Reset UI
$('#results-container').hide();
$('#error-alert').hide();
$('#status-container').show();
$('#progress-bar').css('width', '0%').attr('aria-valuenow', 0).text('0%');
// Start analysis
$.ajax({
url: '/api/start_etf_analysis',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ etf_code: etfCode }),
success: function(response) {
taskId = response.task_id;
if (response.status === 'completed') {
displayResults(response.result);
} else {
startPolling();
}
},
error: function(xhr) {
const errorMsg = xhr.responseJSON ? xhr.responseJSON.error : '未知错误';
showError('启动分析任务失败: ' + errorMsg);
$('#status-container').hide();
}
});
});
function startPolling() {
if (pollingInterval) clearInterval(pollingInterval);
pollingInterval = setInterval(function() {
if (!taskId) {
clearInterval(pollingInterval);
return;
}
$.ajax({
url: `/api/etf_analysis_status/${taskId}`,
type: 'GET',
success: function(response) {
$('#progress-bar').css('width', response.progress + '%').attr('aria-valuenow', response.progress).text(response.progress + '%');
if (response.status === 'completed') {
clearInterval(pollingInterval);
pollingInterval = null;
displayResults(response.result);
} else if (response.status === 'failed') {
clearInterval(pollingInterval);
pollingInterval = null;
showError('分析任务失败: ' + response.error);
$('#status-container').hide();
}
},
error: function(xhr) {
clearInterval(pollingInterval);
pollingInterval = null;
const errorMsg = xhr.responseJSON ? xhr.responseJSON.error : '未知错误';
showError('获取任务状态失败: ' + errorMsg);
$('#status-container').hide();
}
});
}, 2000); // Poll every 2 seconds
}
function displayResults(results) {
$('#status-container').hide();
$('#results-container').show();
// 1. AI 总结
const summary = results.ai_summary;
if (summary && summary.message) {
// 使用 marked.js 将 Markdown 转换为 HTML
$('#ai-summary-content').html(marked.parse(summary.message));
} else {
$('#ai-summary-content').html(`<div class="p-3"><p class="text-danger">${summary.error || 'AI总结加载失败'}</p></div>`);
}
// 2. 基本信息
const basicInfo = results.basic_info;
console.log('ETF anlaysis basic info:', basicInfo);
if (basicInfo && !basicInfo.error) {
let infoHtml = '<ul class="list-group list-group-flush">';
const keyMap = {'基金简称': '简称', '基金代码': '代码', '跟踪标的': '跟踪指数', '基金规模': '规模', '成立日期': '成立日', '基金管理人': '管理人'};
for (const [key, value] of Object.entries(keyMap)) {
if (basicInfo[key]) {
infoHtml += `<li class="list-group-item d-flex justify-content-between align-items-center">${value} <span class="badge bg-primary rounded-pill">${basicInfo[key]}</span></li>`;
}
}
$('#basic-info-content').html(infoHtml + '</ul>');
} else {
$('#basic-info-content').html(`<div class="p-3"><p class="text-danger">${basicInfo.error || '信息加载失败'}</p></div>`);
}
// 3. 市场表现
const perf = results.market_performance;
if (perf && !perf.error) {
let perfHtml = '<div class="row"><div class="col-lg-6"><h6 class="text-center mb-3">回报率 vs. 基准(沪深300)</h6><table class="table table-sm table-hover"><thead><tr><th>周期</th><th>ETF</th><th>基准</th><th>超额</th></tr></thead><tbody>';
const returns = perf.returns || {}, ben_returns = perf.benchmark_returns || {}, alpha = perf.alpha || {};
for(const period in returns) {
const r = returns[period] || 0;
const br = ben_returns[period] || 0;
const a = alpha[period] || 0;
perfHtml += `<tr><td>${period}</td><td class="${r >= 0 ? 'text-success' : 'text-danger'} fw-bold">${r.toFixed(2)}%</td><td class="${br >= 0 ? 'text-success' : 'text-danger'}">${br.toFixed(2)}%</td><td class="${a >= 0 ? 'text-success' : 'text-danger'} fw-bold">${a.toFixed(2)}%</td></tr>`;
}
perfHtml += '</tbody></table></div><div class="col-lg-3"><h6 class="text-center mb-3">流动性</h6><ul class="list-group">';
const liq = perf.liquidity || {};
const avg_volume = liq['日均成交额(近一月)'] ? (liq['日均成交额(近一月)']/1e8).toFixed(2) + ' 亿' : 'N/A';
const avg_turnover = liq['日均换手率(近一月)'] ? liq['日均换手率(近一月)'].toFixed(2) + ' %' : 'N/A';
perfHtml += `<li class="list-group-item">日均成交额<br><strong class="h5">${avg_volume}</strong></li>`;
perfHtml += `<li class="list-group-item">日均换手率<br><strong class="h5">${avg_turnover}</strong></li></ul></div>`;
perfHtml += '<div class="col-lg-3"><h6 class="text-center mb-3">技术指标</h6><ul class="list-group">';
const ind = perf.tech_indicators || {};
for(const [name, value] of Object.entries(ind)) {
perfHtml += `<li class="list-group-item d-flex justify-content-between"><span>${name}</span> <span class="fw-bold">${value ? value.toFixed(2) : 'N/A'}</span></li>`;
}
$('#performance-content').html(perfHtml + '</ul></div></div>');
} else {
$('#performance-content').html(`<div class="p-3"><p class="text-danger">${perf.error || '表现数据加载失败'}</p></div>`);
}
// 4. 资金流向
const flow = results.fund_flow;
if (flow && !flow.error) {
let flowHtml = '<div class="row"><div class="col-md-4"><h6 class="text-center mb-3">累计资金净流入(估算)</h6><ul class="list-group">';
for(const [period, amount] of Object.entries(flow.summary || {})) {
flowHtml += `<li class="list-group-item d-flex justify-content-between"><span>${period}</span> <span class="${amount >= 0 ? 'text-success' : 'text-danger'} fw-bold">${(amount / 1e8).toFixed(2)} 亿</span></li>`;
}
flowHtml += '</ul></div><div class="col-md-8"><div id="fund-flow-chart"></div></div></div>';
$('#fund-flow-content').html(flowHtml);
if (flow.daily_flow_chart_data && flow.daily_flow_chart_data.data) {
const chartData = flow.daily_flow_chart_data.data.map(item => ({ x: item[0], y: item[1] }));
const options = { series: [{ name: '资金净流入(亿元)', data: chartData }], chart: { type: 'bar', height: 350, toolbar: { show: true }, zoom: { enabled: true } }, plotOptions: { bar: { colors: { ranges: [{ from: -Infinity, to: 0, color: '#dc3545' }, { from: 0, to: Infinity, color: '#28a745' }] } } }, xaxis: { type: 'datetime' }, yaxis: { title: { text: '资金净流入估算 (亿元)' } }, tooltip: { y: { formatter: (val) => `${val.toFixed(4)} 亿` } }, title: { text: '每日资金净流入估算 (近60日)', align: 'center' } };
new ApexCharts(document.querySelector("#fund-flow-chart"), options).render();
}
} else {
$('#fund-flow-content').html(`<div class="p-3"><p class="text-danger">${flow.error || '资金流数据加载失败'}</p></div>`);
}
// 5. 风险与跟踪能力
const risk = results.risk_and_tracking;
if (risk && !risk.error) {
let riskHtml = `<div class="row text-center"><div class="col-md-3"><div class="card p-2"><h6 class="card-title">年化波动率</h6><p class="h4">${(risk.annualized_volatility * 100).toFixed(2)}%</p></div></div>`;
riskHtml += `<div class="col-md-2"><div class="card p-2"><h6 class="card-title">Beta</h6><p class="h4">${risk.beta.toFixed(2)}</p></div></div>`;
riskHtml += `<div class="col-md-2"><div class="card p-2"><h6 class="card-title">夏普比率</h6><p class="h4">${risk.sharpe_ratio.toFixed(2)}</p></div></div>`;
riskHtml += `<div class="col-md-2"><div class="card p-2"><h6 class="card-title">跟踪误差</h6><p class="h4">${(risk.tracking_error * 100).toFixed(2)}%</p></div></div>`;
const premium = risk.avg_premium_discount_monthly || 0;
riskHtml += `<div class="col-md-3"><div class="card p-2"><h6 class="card-title">月均溢价率</h6><p class="h4 ${premium >= 0 ? 'text-danger' : 'text-success'}">${premium.toFixed(2)}%</p></div></div></div>`; // 溢价为红,折价为绿
$('#risk-content').html(riskHtml);
} else {
$('#risk-content').html(`<div class="p-3"><p class="text-danger">${risk.error || '风险数据加载失败'}</p></div>`);
}
// 6. 持仓分析
const holdings = results.holdings;
if (holdings && !holdings.error) {
let holdingsHtml = `<div class="row"><div class="col-md-8"><h6 class="text-center mb-3">前十大持仓股</h6><table class="table table-sm table-hover"><thead><tr><th>代码</th><th>名称</th><th>持仓市值(元)</th><th>占净值比例(%)</th></tr></thead><tbody>`;
for (const stock of holdings.top_10_holdings) {
holdingsHtml += `<tr><td>${stock['股票代码']}</td><td>${stock['股票名称']}</td><td>${stock['持仓市值']}</td><td>${stock['占净值比例(%)'].toFixed(2)}</td></tr>`;
}
holdingsHtml += `</tbody></table></div><div class="col-md-4"><h6 class="text-center mb-3">持仓集中度</h6><div class="card p-3 text-center"><p class="h1">${holdings.concentration.toFixed(2)}%</p><small class="text-muted">前十大持仓占比</small></div></div></div>`;
$('#holdings-content').html(holdingsHtml);
} else {
$('#holdings-content').html(`<div class="p-3"><p class="text-danger">${holdings.error || '持仓数据加载失败'}</p></div>`);
}
// 7. 板块深度分析
const sector = results.sector_analysis;
if (sector && !sector.error) {
let sectorHtml = `<h5 class="text-center mb-4">板块: ${sector.sector_name}</h5><div class="row"><div class="col-md-6"><h6 class="text-center mb-3">板块回报率</h6><ul class="list-group">`;
for (const [period, rate] of Object.entries(sector.returns)) {
sectorHtml += `<li class="list-group-item d-flex justify-content-between">${period}<span class="${rate >= 0 ? 'text-success' : 'text-danger'} fw-bold">${rate.toFixed(2)}%</span></li>`;
}
const pe = sector.valuation.current_pe || 0;
const percentile = sector.valuation.pe_percentile || 0;
sectorHtml += `</ul></div><div class="col-md-6"><h6 class="text-center mb-3">板块估值</h6><div class="card p-3 text-center"><p>当前滚动PE: <strong class="h5">${pe.toFixed(2)}</strong></p><p>位于历史 <strong class="h5">${percentile.toFixed(2)}%</strong> 分位点</p></div></div></div>`;
$('#sector-content').html(sectorHtml);
} else {
$('#sector-content').html(`<div class="p-3"><p class="text-danger">${sector.error || '板块数据加载失败'}</p></div>`);
}
}
});
</script>
{% endblock %}