| {% extends "layout.html" %}
|
|
|
| {% block title %}智能仪表盘 - 智能分析系统{% endblock %}
|
|
|
| {% block content %}
|
| <div class="container-fluid py-3">
|
| <div id="alerts-container"></div>
|
| <div class="row g-3 mb-3">
|
| <div class="col-12">
|
| <div class="card">
|
| <div class="card-header py-1">
|
| <h5 class="mb-0">智能股票分析</h5>
|
| </div>
|
| <div class="card-body py-2">
|
| <form id="analysis-form" class="row g-2">
|
| <div class="col-md-4">
|
| <div class="input-group input-group-sm">
|
| <span class="input-group-text">股票代码</span>
|
| <input type="text" class="form-control" id="stock-code" placeholder="例如: 600519" required>
|
| </div>
|
| </div>
|
| <div class="col-md-3">
|
| <div class="input-group input-group-sm">
|
| <span class="input-group-text">市场</span>
|
| <select class="form-select" id="market-type">
|
| <option value="A" selected>A股</option>
|
| <option value="HK">港股</option>
|
| <option value="US">美股</option>
|
| </select>
|
| </div>
|
| </div>
|
| <div class="col-md-3">
|
| <div class="input-group input-group-sm">
|
| <span class="input-group-text">周期</span>
|
| <select class="form-select" id="analysis-period">
|
| <option value="1m">1个月</option>
|
| <option value="3m">3个月</option>
|
| <option value="6m">6个月</option>
|
| <option value="1y" selected>1年</option>
|
| </select>
|
| </div>
|
| </div>
|
| <div class="col-md-2">
|
| <button type="submit" class="btn btn-primary btn-sm w-100">
|
| <i class="fas fa-chart-line"></i> 分析
|
| </button>
|
| </div>
|
| </form>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div id="analysis-result" style="display: none;">
|
| <div class="row g-3 mb-3">
|
| <div class="col-md-6">
|
| <div class="card h-100">
|
| <div class="card-header py-2">
|
| <h5 class="mb-0">股票概要</h5>
|
| </div>
|
| <div class="card-body">
|
| <div class="row mb-3">
|
| <div class="col-md-7">
|
| <h2 id="stock-name" class="mb-0 fs-4"></h2>
|
| <p id="stock-info" class="text-muted mb-0 small"></p>
|
| </div>
|
| <div class="col-md-5 text-end">
|
| <h3 id="stock-price" class="mb-0 fs-4"></h3>
|
| <p id="price-change" class="mb-0"></p>
|
| </div>
|
| </div>
|
| <div class="row">
|
| <div class="col-md-6">
|
| <div class="mb-2">
|
| <span class="text-muted small">综合评分:</span>
|
| <div class="mt-1">
|
| <span id="total-score" class="badge rounded-pill score-pill"></span>
|
| </div>
|
| </div>
|
| <div class="mb-2">
|
| <span class="text-muted small">投资建议:</span>
|
| <p id="recommendation" class="mb-0 text-strong"></p>
|
| </div>
|
| </div>
|
| <div class="col-md-6">
|
| <div class="mb-2">
|
| <span class="text-muted small">技术面指标:</span>
|
| <ul class="list-unstyled mt-1 mb-0 small">
|
| <li><span class="text-muted">RSI:</span> <span id="rsi-value"></span></li>
|
| <li><span class="text-muted">MA趋势:</span> <span id="ma-trend"></span></li>
|
| <li><span class="text-muted">MACD信号:</span> <span id="macd-signal"></span></li>
|
| <li><span class="text-muted">波动率:</span> <span id="volatility"></span></li>
|
| </ul>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| <div class="col-md-6">
|
| <div class="card h-100">
|
| <div class="card-header py-2">
|
| <h5 class="mb-0">多维度评分</h5>
|
| </div>
|
| <div class="card-body">
|
| <div id="radar-chart" style="height: 200px;"></div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div class="row g-3 mb-3">
|
| <div class="col-12">
|
| <div class="card">
|
| <div class="card-header py-2">
|
| <h5 class="mb-0">价格与技术指标</h5>
|
| </div>
|
| <div class="card-body p-0">
|
| <div id="price-chart" style="height: 400px;"></div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div class="row g-3 mb-3">
|
| <div class="col-md-4">
|
| <div class="card h-100">
|
| <div class="card-header py-2">
|
| <h5 class="mb-0">支撑与压力位</h5>
|
| </div>
|
| <div class="card-body">
|
| <table class="table table-sm">
|
| <thead>
|
| <tr>
|
| <th>类型</th>
|
| <th>价格</th>
|
| <th>距离</th>
|
| </tr>
|
| </thead>
|
| <tbody id="support-resistance-table">
|
|
|
| </tbody>
|
| </table>
|
| </div>
|
| </div>
|
| </div>
|
| <div class="col-md-8">
|
| <div class="card h-100">
|
| <div class="card-header py-2">
|
| <h5 class="mb-0">AI分析建议</h5>
|
| </div>
|
| <div class="card-body">
|
| <div id="ai-analysis" class="analysis-section">
|
|
|
| <div class="loading">
|
| <div class="spinner-border text-primary" role="status">
|
| <span class="visually-hidden">Loading...</span>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| {% endblock %}
|
|
|
| {% block scripts %}
|
| <script>
|
| let stockData = [];
|
| let analysisResult = null;
|
|
|
|
|
| $('#analysis-form').submit(function(e) {
|
| e.preventDefault();
|
| const stockCode = $('#stock-code').val().trim();
|
| const marketType = $('#market-type').val();
|
| const period = $('#analysis-period').val();
|
|
|
| if (!stockCode) {
|
| showError('请输入股票代码!');
|
| return;
|
| }
|
|
|
|
|
| window.location.href = `/stock_detail/${stockCode}?market_type=${marketType}&period=${period}`;
|
| });
|
|
|
|
|
| function formatAIAnalysis(text) {
|
| if (!text) return '';
|
|
|
|
|
| const safeText = text
|
| .replace(/&/g, '&')
|
| .replace(/</g, '<')
|
| .replace(/>/g, '>');
|
|
|
|
|
| let formatted = safeText
|
|
|
| .replace(/\*\*(.*?)\*\*/g, '<strong class="keyword">$1</strong>')
|
| .replace(/__(.*?)__/g, '<strong>$1</strong>')
|
|
|
|
|
| .replace(/\*(.*?)\*/g, '<em>$1</em>')
|
| .replace(/_(.*?)_/g, '<em>$1</em>')
|
|
|
|
|
| .replace(/^# (.*?)$/gm, '<h4 class="mt-3 mb-2">$1</h4>')
|
| .replace(/^## (.*?)$/gm, '<h5 class="mt-2 mb-2">$1</h5>')
|
|
|
|
|
| .replace(/支撑位/g, '<span class="keyword">支撑位</span>')
|
| .replace(/压力位/g, '<span class="keyword">压力位</span>')
|
| .replace(/趋势/g, '<span class="keyword">趋势</span>')
|
| .replace(/均线/g, '<span class="keyword">均线</span>')
|
| .replace(/MACD/g, '<span class="term">MACD</span>')
|
| .replace(/RSI/g, '<span class="term">RSI</span>')
|
| .replace(/KDJ/g, '<span class="term">KDJ</span>')
|
|
|
|
|
| .replace(/([上涨升])/g, '<span class="trend-up">$1</span>')
|
| .replace(/([下跌降])/g, '<span class="trend-down">$1</span>')
|
| .replace(/(买入|做多|多头|突破)/g, '<span class="trend-up">$1</span>')
|
| .replace(/(卖出|做空|空头|跌破)/g, '<span class="trend-down">$1</span>')
|
|
|
|
|
| .replace(/(\d+\.\d{2})/g, '<span class="price">$1</span>')
|
|
|
|
|
| .replace(/\n\n+/g, '</p><p class="analysis-para">')
|
| .replace(/\n/g, '<br>');
|
|
|
|
|
| return '<p class="analysis-para">' + formatted + '</p>';
|
| }
|
|
|
|
|
| function fetchStockData(stockCode, marketType, period) {
|
| showLoading();
|
|
|
| $.ajax({
|
| url: `/api/stock_data?stock_code=${stockCode}&market_type=${marketType}&period=${period}`,
|
| type: 'GET',
|
| dataType: 'json',
|
| success: function(response) {
|
|
|
|
|
| if (!response.data) {
|
| hideLoading();
|
| showError('响应格式不正确: 缺少data字段');
|
| return;
|
| }
|
|
|
| if (response.data.length === 0) {
|
| hideLoading();
|
| showError('未找到股票数据');
|
| return;
|
| }
|
|
|
| stockData = response.data;
|
|
|
|
|
| fetchEnhancedAnalysis(stockCode, marketType);
|
| },
|
| error: function(xhr, status, error) {
|
| hideLoading();
|
|
|
| let errorMsg = '获取股票数据失败';
|
| if (xhr.responseJSON && xhr.responseJSON.error) {
|
| errorMsg += ': ' + xhr.responseJSON.error;
|
| } else if (error) {
|
| errorMsg += ': ' + error;
|
| }
|
| showError(errorMsg);
|
| }
|
| });
|
| }
|
|
|
|
|
| function fetchEnhancedAnalysis(stockCode, marketType) {
|
|
|
| $.ajax({
|
| url: '/api/enhanced_analysis?_=' + new Date().getTime(),
|
| type: 'POST',
|
| contentType: 'application/json',
|
| data: JSON.stringify({
|
| stock_code: stockCode,
|
| market_type: marketType
|
| }),
|
| success: function(response) {
|
|
|
| if (!response.result) {
|
| hideLoading();
|
| showError('增强分析响应格式不正确');
|
| return;
|
| }
|
|
|
| analysisResult = response.result;
|
| renderAnalysisResult();
|
| hideLoading();
|
| $('#analysis-result').show();
|
| },
|
| error: function(xhr, status, error) {
|
| hideLoading();
|
|
|
| let errorMsg = '获取分析数据失败';
|
| if (xhr.responseJSON && xhr.responseJSON.error) {
|
| errorMsg += ': ' + xhr.responseJSON.error;
|
| } else if (error) {
|
| errorMsg += ': ' + error;
|
| }
|
| showError(errorMsg);
|
| }
|
| });
|
| }
|
|
|
|
|
| function renderAnalysisResult() {
|
| if (!analysisResult) return;
|
|
|
|
|
| $('#stock-name').text(analysisResult.basic_info.stock_name + ' (' + analysisResult.basic_info.stock_code + ')');
|
| $('#stock-info').text(analysisResult.basic_info.industry + ' | ' + analysisResult.basic_info.analysis_date);
|
|
|
|
|
| $('#stock-price').text('¥' + formatNumber(analysisResult.price_data.current_price, 2));
|
| const priceChangeClass = analysisResult.price_data.price_change >= 0 ? 'trend-up' : 'trend-down';
|
| const priceChangeIcon = analysisResult.price_data.price_change >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
|
| $('#price-change').html(`<span class="${priceChangeClass}">${priceChangeIcon} ${formatNumber(analysisResult.price_data.price_change_value, 2)} (${formatPercent(analysisResult.price_data.price_change, 2)})</span>`);
|
|
|
|
|
| const scoreClass = getScoreColorClass(analysisResult.scores.total_score);
|
| $('#total-score').text(analysisResult.scores.total_score).addClass(scoreClass);
|
| $('#recommendation').text(analysisResult.recommendation.action);
|
|
|
|
|
| $('#rsi-value').text(formatNumber(analysisResult.technical_analysis.indicators.rsi, 2));
|
|
|
| const maTrendClass = getTrendColorClass(analysisResult.technical_analysis.trend.ma_trend);
|
| const maTrendIcon = getTrendIcon(analysisResult.technical_analysis.trend.ma_trend);
|
| $('#ma-trend').html(`<span class="${maTrendClass}">${maTrendIcon} ${analysisResult.technical_analysis.trend.ma_status}</span>`);
|
|
|
| const macdSignal = analysisResult.technical_analysis.indicators.macd > analysisResult.technical_analysis.indicators.macd_signal ? 'BUY' : 'SELL';
|
| const macdClass = macdSignal === 'BUY' ? 'trend-up' : 'trend-down';
|
| const macdIcon = macdSignal === 'BUY' ? '<i class="fas fa-arrow-up"></i>' : '<i class="fas fa-arrow-down"></i>';
|
| $('#macd-signal').html(`<span class="${macdClass}">${macdIcon} ${macdSignal}</span>`);
|
|
|
| $('#volatility').text(formatPercent(analysisResult.technical_analysis.indicators.volatility, 2));
|
|
|
|
|
| let supportResistanceHtml = '';
|
|
|
|
|
| if (analysisResult.technical_analysis.support_resistance.resistance &&
|
| analysisResult.technical_analysis.support_resistance.resistance.short_term &&
|
| analysisResult.technical_analysis.support_resistance.resistance.short_term.length > 0) {
|
| const resistance = analysisResult.technical_analysis.support_resistance.resistance.short_term[0];
|
| const distance = ((resistance - analysisResult.price_data.current_price) / analysisResult.price_data.current_price * 100).toFixed(2);
|
| supportResistanceHtml += `
|
| <tr>
|
| <td><span class="badge bg-danger">短期压力</span></td>
|
| <td>${formatNumber(resistance, 2)}</td>
|
| <td>+${distance}%</td>
|
| </tr>
|
| `;
|
| }
|
|
|
| if (analysisResult.technical_analysis.support_resistance.resistance &&
|
| analysisResult.technical_analysis.support_resistance.resistance.medium_term &&
|
| analysisResult.technical_analysis.support_resistance.resistance.medium_term.length > 0) {
|
| const resistance = analysisResult.technical_analysis.support_resistance.resistance.medium_term[0];
|
| const distance = ((resistance - analysisResult.price_data.current_price) / analysisResult.price_data.current_price * 100).toFixed(2);
|
| supportResistanceHtml += `
|
| <tr>
|
| <td><span class="badge bg-warning text-dark">中期压力</span></td>
|
| <td>${formatNumber(resistance, 2)}</td>
|
| <td>+${distance}%</td>
|
| </tr>
|
| `;
|
| }
|
|
|
|
|
| if (analysisResult.technical_analysis.support_resistance.support &&
|
| analysisResult.technical_analysis.support_resistance.support.short_term &&
|
| analysisResult.technical_analysis.support_resistance.support.short_term.length > 0) {
|
| const support = analysisResult.technical_analysis.support_resistance.support.short_term[0];
|
| const distance = ((support - analysisResult.price_data.current_price) / analysisResult.price_data.current_price * 100).toFixed(2);
|
| supportResistanceHtml += `
|
| <tr>
|
| <td><span class="badge bg-success">短期支撑</span></td>
|
| <td>${formatNumber(support, 2)}</td>
|
| <td>${distance}%</td>
|
| </tr>
|
| `;
|
| }
|
|
|
| if (analysisResult.technical_analysis.support_resistance.support &&
|
| analysisResult.technical_analysis.support_resistance.support.medium_term &&
|
| analysisResult.technical_analysis.support_resistance.support.medium_term.length > 0) {
|
| const support = analysisResult.technical_analysis.support_resistance.support.medium_term[0];
|
| const distance = ((support - analysisResult.price_data.current_price) / analysisResult.price_data.current_price * 100).toFixed(2);
|
| supportResistanceHtml += `
|
| <tr>
|
| <td><span class="badge bg-info">中期支撑</span></td>
|
| <td>${formatNumber(support, 2)}</td>
|
| <td>${distance}%</td>
|
| </tr>
|
| `;
|
| }
|
|
|
| $('#support-resistance-table').html(supportResistanceHtml);
|
|
|
|
|
| $('#ai-analysis').html(formatAIAnalysis(analysisResult.ai_analysis));
|
|
|
|
|
| renderRadarChart();
|
|
|
|
|
| renderPriceChart();
|
| }
|
|
|
|
|
| function renderRadarChart() {
|
| if (!analysisResult) return;
|
|
|
| const options = {
|
| series: [{
|
| name: '评分',
|
| data: [
|
| analysisResult.scores.trend_score || 0,
|
| analysisResult.scores.indicators_score || 0,
|
| analysisResult.scores.support_resistance_score || 0,
|
| analysisResult.scores.volatility_volume_score || 0
|
| ]
|
| }],
|
| chart: {
|
| height: 200,
|
| type: 'radar',
|
| toolbar: {
|
| show: false
|
| }
|
| },
|
| title: {
|
| text: '多维度技术分析评分',
|
| style: {
|
| fontSize: '14px'
|
| }
|
| },
|
| xaxis: {
|
| categories: ['趋势分析', '技术指标', '支撑压力位', '波动与成交量']
|
| },
|
| yaxis: {
|
| max: 10,
|
| min: 0
|
| },
|
| fill: {
|
| opacity: 0.5,
|
| colors: ['#4e73df']
|
| },
|
| markers: {
|
| size: 4
|
| }
|
| };
|
|
|
|
|
| $('#radar-chart').empty();
|
|
|
| const chart = new ApexCharts(document.querySelector("#radar-chart"), options);
|
| chart.render();
|
| }
|
|
|
|
|
| function renderPriceChart() {
|
| if (!stockData || stockData.length === 0) return;
|
|
|
|
|
| const seriesData = [];
|
|
|
|
|
| const candleData = stockData.map(item => ({
|
| x: new Date(item.date),
|
| y: [item.open, item.high, item.low, item.close]
|
| }));
|
| seriesData.push({
|
| name: '价格',
|
| type: 'candlestick',
|
| data: candleData
|
| });
|
|
|
|
|
| const ma5Data = stockData.map(item => ({
|
| x: new Date(item.date),
|
| y: item.MA5
|
| }));
|
| seriesData.push({
|
| name: 'MA5',
|
| type: 'line',
|
| data: ma5Data
|
| });
|
|
|
| const ma20Data = stockData.map(item => ({
|
| x: new Date(item.date),
|
| y: item.MA20
|
| }));
|
| seriesData.push({
|
| name: 'MA20',
|
| type: 'line',
|
| data: ma20Data
|
| });
|
|
|
| const ma60Data = stockData.map(item => ({
|
| x: new Date(item.date),
|
| y: item.MA60
|
| }));
|
| seriesData.push({
|
| name: 'MA60',
|
| type: 'line',
|
| data: ma60Data
|
| });
|
|
|
|
|
| const options = {
|
| series: seriesData,
|
| chart: {
|
| height: 400,
|
| type: 'candlestick',
|
| toolbar: {
|
| show: true,
|
| tools: {
|
| download: true,
|
| selection: true,
|
| zoom: true,
|
| zoomin: true,
|
| zoomout: true,
|
| pan: true,
|
| reset: true
|
| }
|
| }
|
| },
|
| title: {
|
| text: `${analysisResult.basic_info.stock_name} (${analysisResult.basic_info.stock_code}) 价格走势`,
|
| align: 'left',
|
| style: {
|
| fontSize: '14px'
|
| }
|
| },
|
| xaxis: {
|
| type: 'datetime'
|
| },
|
| yaxis: {
|
| tooltip: {
|
| enabled: true
|
| },
|
| labels: {
|
| formatter: function(value) {
|
| return formatNumber(value, 2);
|
| }
|
| }
|
| },
|
| tooltip: {
|
| shared: true,
|
| custom: [
|
| function({ seriesIndex, dataPointIndex, w }) {
|
| if (seriesIndex === 0) {
|
| const o = w.globals.seriesCandleO[seriesIndex][dataPointIndex];
|
| const h = w.globals.seriesCandleH[seriesIndex][dataPointIndex];
|
| const l = w.globals.seriesCandleL[seriesIndex][dataPointIndex];
|
| const c = w.globals.seriesCandleC[seriesIndex][dataPointIndex];
|
|
|
| return `
|
| <div class="apexcharts-tooltip-candlestick">
|
| <div>开盘: <span>${formatNumber(o, 2)}</span></div>
|
| <div>最高: <span>${formatNumber(h, 2)}</span></div>
|
| <div>最低: <span>${formatNumber(l, 2)}</span></div>
|
| <div>收盘: <span>${formatNumber(c, 2)}</span></div>
|
| </div>
|
| `;
|
| }
|
| return '';
|
| }
|
| ]
|
| },
|
| plotOptions: {
|
| candlestick: {
|
| colors: {
|
| upward: '#3C90EB',
|
| downward: '#DF7D46'
|
| }
|
| }
|
| }
|
| };
|
|
|
|
|
| $('#price-chart').empty();
|
|
|
| const chart = new ApexCharts(document.querySelector("#price-chart"), options);
|
| chart.render();
|
| }
|
| </script>
|
| {% endblock %} |