stock / templates /dashboard.html
gitdeem's picture
Upload 32 files
f5d52f6 verified
{% 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;
// 提交表单进行分析
$('#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}`;
});
// Format AI analysis text
function formatAIAnalysis(text) {
if (!text) return '';
// First, make the text safe for HTML
const safeText = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 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 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); // 统一使用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 %}