Spaces:
Running
Running
| {% 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"> <!-- 减少padding-top和padding-bottom --> | |
| <h5 class="mb-0">智能股票分析</h5> | |
| </div> | |
| <div class="card-body py-2"> <!-- 减少padding-top和padding-bottom --> | |
| <form id="analysis-form" class="row g-2"> <!-- 减少间距g-3到g-2 --> | |
| <div class="col-md-4"> | |
| <div class="input-group input-group-sm"> <!-- 添加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"> <!-- 添加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"> <!-- 添加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"> <!-- 使用btn-sm减小按钮尺寸 --> | |
| <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"> | |
| <!-- 支撑压力位数据将在JS中动态填充 --> | |
| </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"> | |
| <!-- AI分析结果将在JS中动态填充 --> | |
| <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; | |
| // === THEME-AWARE CHART LOGIC START === | |
| let charts = {}; // Store chart instances | |
| function getApexChartThemeOptions() { | |
| const isDarkMode = $('html').attr('data-theme') === 'dark'; | |
| const options = { | |
| theme: { | |
| mode: isDarkMode ? 'dark' : 'light' | |
| }, | |
| chart: { | |
| background: 'transparent' | |
| }, | |
| tooltip: { | |
| theme: isDarkMode ? 'dark' : 'light' | |
| } | |
| }; | |
| if(isDarkMode) { | |
| options.plotOptions = { | |
| radar: { | |
| polygons: { | |
| strokeColors: '#555', | |
| fill: { colors: ['#393939', '#444444'] } | |
| } | |
| } | |
| }; | |
| } else { | |
| options.plotOptions = { | |
| radar: { | |
| polygons: { | |
| strokeColors: '#e9e9e9', | |
| fill: { colors: ['#f8f8f8', '#fff'] } | |
| } | |
| } | |
| }; | |
| } | |
| return options; | |
| } | |
| function destroyAllCharts() { | |
| Object.values(charts).forEach(chart => { | |
| if (chart && typeof chart.destroy === 'function') { | |
| chart.destroy(); | |
| } | |
| }); | |
| charts = {}; | |
| } | |
| function rerenderAllCharts() { | |
| if (!analysisResult) return; | |
| destroyAllCharts(); | |
| renderRadarChart(); | |
| renderPriceChart(); | |
| } | |
| // === THEME-AWARE CHART LOGIC END === | |
| // 提交表单进行分析 | |
| $('#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}`; | |
| }); | |
| // Observe theme changes for charts | |
| const observer = new MutationObserver(function(mutations) { | |
| mutations.forEach(function(mutation) { | |
| if (mutation.attributeName === "data-theme") { | |
| rerenderAllCharts(); | |
| } | |
| }); | |
| }); | |
| observer.observe(document.documentElement, { | |
| attributes: true | |
| }); | |
| // Format AI analysis text | |
| function formatAIAnalysis(text) { | |
| if (!text) return ''; | |
| // First, make the text safe for HTML | |
| const safeText = text | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>'); | |
| // Replace basic Markdown elements | |
| let formatted = safeText | |
| // Bold text with ** or __ | |
| .replace(/\*\*(.*?)\*\*/g, '<strong class="keyword">$1</strong>') | |
| .replace(/__(.*?)__/g, '<strong>$1</strong>') | |
| // Italic text with * or _ | |
| .replace(/\*(.*?)\*/g, '<em>$1</em>') | |
| .replace(/_(.*?)_/g, '<em>$1</em>') | |
| // Headers | |
| .replace(/^# (.*?)$/gm, '<h4 class="mt-3 mb-2">$1</h4>') | |
| .replace(/^## (.*?)$/gm, '<h5 class="mt-2 mb-2">$1</h5>') | |
| // Apply special styling to financial terms | |
| .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>') | |
| // Highlight price patterns and movements | |
| .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>') | |
| // Highlight price values (matches patterns like 31.25, 120.50) | |
| .replace(/(\d+\.\d{2})/g, '<span class="price">$1</span>') | |
| // Convert line breaks to paragraph tags | |
| .replace(/\n\n+/g, '</p><p class="analysis-para">') | |
| .replace(/\n/g, '<br>'); | |
| // Wrap in paragraph tags for consistent styling | |
| 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) { | |
| // 检查response是否有data属性 | |
| 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分析 | |
| $('#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 finalOptions = $.extend(true, {}, options, getApexChartThemeOptions()); | |
| const chart = new ApexCharts(document.querySelector("#radar-chart"), finalOptions); | |
| charts.radar = chart; | |
| 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); // 统一使用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 finalOptions = $.extend(true, {}, options, getApexChartThemeOptions()); | |
| const chart = new ApexCharts(document.querySelector("#price-chart"), finalOptions); | |
| charts.price = chart; | |
| chart.render(); | |
| } | |
| </script> | |
| {% endblock %} |