File size: 19,286 Bytes
0f10134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322

{% 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 %}