stock / templates /stock_detail.html
gitdeem's picture
Upload 32 files
f5d52f6 verified
{% extends "layout.html" %}
{% block title %}股票详情 - {{ stock_code }} - 智能分析系统{% endblock %}
{% block content %}
<div class="container-fluid py-1" style="padding-top: 0.2rem !important; padding-bottom: 0.2rem !important;">
<div id="alerts-container"></div>
<!-- 调整布局: 减少垂直方向上的间距 -->
<div class="d-flex justify-content-between align-items-center mb-1" style="margin-top: 0.2rem; margin-bottom: 0.2rem;">
<h4 id="stock-title" class="mb-0 fw-bold" style="font-size: 1.1rem; line-height: 1.2;">股票详情加载中...</h4>
<div class="d-flex align-items-center">
<select class="form-select form-select-sm me-2" id="market-type" style="max-width: 100px; height: 32px; padding-top: 2px; padding-bottom: 2px;">
<option value="A" {% if market_type == 'A' %}selected{% endif %}>A股</option>
<option value="HK" {% if market_type == 'HK' %}selected{% endif %}>港股</option>
<option value="US" {% if market_type == 'US' %}selected{% endif %}>美股</option>
</select>
<select class="form-select form-select-sm me-2" id="analysis-period" style="max-width: 100px; height: 32px; padding-top: 2px; padding-bottom: 2px;">
<option value="1m">1个月</option>
<option value="3m">3个月</option>
<option value="6m">6个月</option>
<option value="1y" selected>1年</option>
</select>
<button id="refresh-btn" class="btn btn-primary btn-sm" style="height: 32px; padding: 2px 8px;">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div id="loading-panel" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 mb-0">正在加载股票数据和分析结果...</p>
<p class="text-muted small mt-2">
<i class="fas fa-info-circle"></i>
AI分析需要30-300秒,已处理<span id="processing-time">0</span>
</p>
<div class="progress mt-3" style="height: 5px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%"></div>
</div>
<button id="cancel-analysis-btn" class="btn btn-outline-secondary mt-3">
<i class="fas fa-times"></i> 取消分析
</button>
</div>
<div id="error-retry" class="text-center mt-3" style="display: none;">
<button id="retry-button" class="btn btn-primary mt-2">
<i class="fas fa-sync-alt"></i> 重试分析
</button>
<p class="text-muted small mt-2">
如果重试失败,请访问<a href="/dashboard">仪表盘</a>尝试其他股票
</p>
</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">
<h3 id="stock-name" class="mb-0 fs-4"></h3>
<p id="stock-info" class="text-muted mb-0 small"></p>
</div>
<div class="col-md-5 text-end">
<h2 id="stock-price" class="mb-0 fs-4"></h2>
<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="volume-status"></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 class="row">
<div class="col-md-6">
<div id="score-chart" style="height: 180px;"></div>
</div>
<div class="col-md-6">
<div id="radar-chart" style="height: 180px;"></div>
</div>
</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-12">
<div class="card">
<div class="card-header py-2">
<h5 class="mb-0">MACD & RSI 指标</h5>
</div>
<div class="card-body p-0">
<div id="indicators-chart" style="height: 350px;"></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>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const stockCode = '{{ stock_code }}';
let marketType = '{{ market_type }}';
let period = '1y';
let stockData = [];
let analysisResult = null;
$(document).ready(function() {
// 初始加载
loadStockData();
// 刷新按钮点击事件
$('#refresh-btn').click(function() {
marketType = $('#market-type').val();
period = $('#analysis-period').val();
loadStockData();
});
// 市场类型改变事件
$('#market-type').change(function() {
marketType = $(this).val();
});
// 分析周期改变事件
$('#analysis-period').change(function() {
period = $(this).val();
});
});
// 加载股票数据
function loadStockData() {
$('#loading-panel').show();
$('#analysis-result').hide();
// 获取股票数据
$.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) {
$('#loading-panel').hide();
showError('响应格式不正确: 缺少data字段');
return;
}
if (response.data.length === 0) {
$('#loading-panel').hide();
showError('未找到股票数据');
return;
}
stockData = response.data;
// 获取增强分析数据
loadAnalysisResult();
},
error: function(xhr, status, error) {
$('#loading-panel').hide();
let errorMsg = '获取股票数据失败';
if (xhr.responseJSON && xhr.responseJSON.error) {
errorMsg += ': ' + xhr.responseJSON.error;
} else if (error) {
errorMsg += ': ' + error;
}
showError(errorMsg);
}
});
}
// 加载分析结果
function loadAnalysisResult() {
// 显示加载状态并启动进度更新
$('#loading-panel').show();
$('#analysis-result').hide();
$('#error-retry').hide();
// 添加处理时间计数器
let processingTime = 0;
const processingTimer = setInterval(function() {
processingTime++;
$('#processing-time').text(processingTime);
}, 1000);
// 使用新的API启动分析任务
$.ajax({
url: '/api/start_stock_analysis',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
stock_code: stockCode,
market_type: marketType
}),
success: function(response) {
// 检查是否已有结果
if (response.status === 'completed' && response.result) {
// 任务已完成,直接处理结果
handleAnalysisResult(response.result);
clearInterval(processingTimer);
return;
}
// 开始轮询任务状态
pollAnalysisStatus(response.task_id, processingTime, processingTimer);
},
error: function(xhr, status, error) {
clearInterval(processingTimer);
handleAnalysisError(xhr, status, error);
}
});
}
// 轮询分析任务状态
function pollAnalysisStatus(taskId, startTime, timerInterval) {
let elapsedTime = startTime || 0;
let pollInterval;
// 保存当前任务ID,用于取消
window.currentAnalysisTaskId = taskId;
// 立即执行一次,然后设置定时器
checkStatus();
function checkStatus() {
$.ajax({
url: `/api/analysis_status/${taskId}`,
type: 'GET',
success: function(response) {
// 更新计时和进度
elapsedTime = startTime + 1;
const progress = response.progress || 0;
// 更新进度显示
$('#processing-time').text(elapsedTime);
// 根据任务状态处理
if (response.status === 'completed') {
// 分析完成,停止轮询
clearInterval(pollInterval);
clearInterval(timerInterval);
// 处理结果
handleAnalysisResult(response.result);
} else if (response.status === 'failed') {
// 分析失败,停止轮询
clearInterval(pollInterval);
clearInterval(timerInterval);
$('#loading-panel').hide();
showError('分析失败: ' + (response.error || '未知错误'));
$('#error-retry').show();
} else {
// 任务仍在进行中,继续轮询
if (!pollInterval) {
pollInterval = setInterval(checkStatus, 2000);
}
}
},
error: function(xhr, status, error) {
if (!pollInterval) {
pollInterval = setInterval(checkStatus, 3000);
}
}
});
}
}
// 处理分析结果
function handleAnalysisResult(result) {
try {
// 设置全局变量
analysisResult = result;
// 渲染分析结果
renderAnalysisResult();
// 更新UI
$('#loading-panel').hide();
$('#analysis-result').show();
} catch (error) {
$('#loading-panel').hide();
showError('处理分析结果时出错: ' + error.message);
}
}
// 处理分析错误
function handleAnalysisError(xhr, status, error) {
$('#loading-panel').hide();
let errorMsg = '获取分析数据失败';
if (status === 'timeout') {
errorMsg = '请求超时,分析可能需要较长时间,请稍后再试';
} else if (xhr.status === 524 || xhr.status === 504) {
errorMsg = '请求超时,服务器处理时间过长';
} else if (xhr.responseJSON && xhr.responseJSON.error) {
errorMsg += ': ' + xhr.responseJSON.error;
} else if (error) {
errorMsg += ': ' + error;
}
showError(errorMsg);
$('#error-retry').show();
}
// 取消按钮功能
$('#cancel-analysis-btn').click(function() {
if (window.currentAnalysisTaskId) {
$.ajax({
url: `/api/cancel_analysis/${window.currentAnalysisTaskId}`,
type: 'POST',
success: function(response) {
$('#loading-panel').hide();
showInfo('分析已取消');
},
error: function(error) {
console.error('取消分析失败:', error);
}
});
} else {
$('#loading-panel').hide();
}
});
// 重试按钮功能
$('#retry-button').click(function() {
$('#error-retry').hide();
loadAnalysisResult();
});
// 通用安全格式化函数
function safeFormat(value, decimals=2) {
try {
// 处理numpy对象残留
if (value && typeof value === 'object') {
if (value._dtype === 'float64') {
return parseFloat(value._values[0]).toFixed(decimals);
}
if (value._dtype === 'int64') {
return parseInt(value._values[0]);
}
}
return parseFloat(value).toFixed(decimals);
} catch (e) {
console.error('Format error:', e);
return '--';
}
}
// 使用示例
$('#rsi-value').text(safeFormat(analysisResult.technical_analysis.indicators.rsi));
// 渲染分析结果
function renderAnalysisResult() {
if (!analysisResult) {
showError("分析结果为空");
return;
}
try {
// 使用全新的安全访问函数
function safeGet(obj, path, defaultValue) {
if (!obj) return defaultValue;
const props = path.split('.');
let current = obj;
for (let i = 0; i < props.length; i++) {
if (current === undefined || current === null) {
console.warn(`属性路径 ${path}${props.slice(0, i).join('.')} 处中断`);
return defaultValue;
}
current = current[props[i]];
}
return current !== undefined && current !== null ? current : defaultValue;
}
// 安全检查技术分析数据
if (!analysisResult.technical_analysis) {
analysisResult.technical_analysis = {
trend: {ma_trend: 'UNKNOWN', ma_status: '未知', ma_values: {}},
indicators: {rsi: 50, macd: 0, macd_signal: 0, macd_histogram: 0, volatility: 0},
volume: {current_volume: 0, volume_ratio: 0, volume_status: 'NORMAL'},
support_resistance: {support_levels: {}, resistance_levels: {}}
};
}
// 使用安全函数获取所有数据
const stockName = safeGet(analysisResult, 'basic_info.stock_name', '未知');
const stockCode = safeGet(analysisResult, 'basic_info.stock_code', '未知');
const industry = safeGet(analysisResult, 'basic_info.industry', '未知');
const analysisDate = safeGet(analysisResult, 'basic_info.analysis_date', '未知日期');
// 更新页面标题
$('#stock-title').text(`${stockName} (${stockCode}) 股票分析`);
// 渲染股票基本信息
$('#stock-name').text(`${stockName} (${stockCode})`);
$('#stock-info').text(`${industry} | ${analysisDate}`);
// 渲染价格信息
const currentPrice = safeGet(analysisResult, 'price_data.current_price', 0);
const priceChange = safeGet(analysisResult, 'price_data.price_change', 0);
const priceChangeValue = safeGet(analysisResult, 'price_data.price_change_value', 0);
$('#stock-price').text('¥' + formatNumber(currentPrice, 2));
const priceChangeClass = priceChange >= 0 ? 'trend-up' : 'trend-down';
const priceChangeIcon = priceChange >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
$('#price-change').html(`<span class="${priceChangeClass}">${priceChangeIcon} ${formatNumber(priceChangeValue, 2)} (${formatPercent(priceChange, 2)})</span>`);
// 渲染评分和建议
const totalScore = safeGet(analysisResult, 'scores.total', 0);
const scoreClass = getScoreColorClass(totalScore);
$('#total-score').text(totalScore).addClass(scoreClass);
$('#recommendation').text(safeGet(analysisResult, 'recommendation.action', '无建议'));
// 渲染技术指标 - 所有属性都使用安全访问
$('#rsi-value').text(formatNumber(safeGet(analysisResult, 'technical_analysis.indicators.rsi', 0), 2));
const maTrend = safeGet(analysisResult, 'technical_analysis.trend.ma_trend', 'UNKNOWN');
const maStatus = safeGet(analysisResult, 'technical_analysis.trend.ma_status', '未知');
const maTrendClass = getTrendColorClass(maTrend);
const maTrendIcon = getTrendIcon(maTrend);
$('#ma-trend').html(`<span class="${maTrendClass}">${maTrendIcon} ${maStatus}</span>`);
// MACD信号
const macd = safeGet(analysisResult, 'technical_analysis.indicators.macd', 0);
const macdSignal = safeGet(analysisResult, 'technical_analysis.indicators.macd_signal', 0);
const macdStatus = macd > macdSignal ? 'BUY' : 'SELL';
const macdClass = macdStatus === 'BUY' ? 'trend-up' : 'trend-down';
const macdIcon = macdStatus === 'BUY' ? '<i class="fas fa-arrow-up"></i>' : '<i class="fas fa-arrow-down"></i>';
$('#macd-signal').html(`<span class="${macdClass}">${macdIcon} ${macdStatus}</span>`);
// 成交量状态
const volumeStatus = safeGet(analysisResult, 'technical_analysis.volume.volume_status', 'NORMAL');
const volumeClass = volumeStatus === 'HIGH' ? 'trend-up' : 'trend-down';
const volumeIcon = volumeStatus === 'HIGH' ? '<i class="fas fa-arrow-up"></i>' : '<i class="fas fa-arrow-down"></i>';
$('#volume-status').html(`<span class="${volumeClass}">${volumeIcon} ${volumeStatus}</span>`);
// 支撑压力位 - 完全重写为更安全的版本
let supportResistanceHtml = '';
// 渲染压力位
const shortTermResistance = safeGet(analysisResult, 'technical_analysis.support_resistance.resistance_levels.short_term', []);
if (shortTermResistance.length > 0) {
const resistance = shortTermResistance[0];
const distance = ((resistance - currentPrice) / currentPrice * 100).toFixed(2);
supportResistanceHtml += `
<tr>
<td><span class="badge bg-danger">短期压力</span></td>
<td>${formatNumber(resistance, 2)}</td>
<td>+${distance}%</td>
</tr>
`;
}
const mediumTermResistance = safeGet(analysisResult, 'technical_analysis.support_resistance.resistance_levels.medium_term', []);
if (mediumTermResistance.length > 0) {
const resistance = mediumTermResistance[0];
const distance = ((resistance - currentPrice) / currentPrice * 100).toFixed(2);
supportResistanceHtml += `
<tr>
<td><span class="badge bg-warning text-dark">中期压力</span></td>
<td>${formatNumber(resistance, 2)}</td>
<td>+${distance}%</td>
</tr>
`;
}
// 渲染支撑位
const shortTermSupport = safeGet(analysisResult, 'technical_analysis.support_resistance.support_levels.short_term', []);
if (shortTermSupport.length > 0) {
const support = shortTermSupport[0];
const distance = ((support - currentPrice) / currentPrice * 100).toFixed(2);
supportResistanceHtml += `
<tr>
<td><span class="badge bg-success">短期支撑</span></td>
<td>${formatNumber(support, 2)}</td>
<td>${distance}%</td>
</tr>
`;
}
const mediumTermSupport = safeGet(analysisResult, 'technical_analysis.support_resistance.support_levels.medium_term', []);
if (mediumTermSupport.length > 0) {
const support = mediumTermSupport[0];
const distance = ((support - currentPrice) / currentPrice * 100).toFixed(2);
supportResistanceHtml += `
<tr>
<td><span class="badge bg-info">中期支撑</span></td>
<td>${formatNumber(support, 2)}</td>
<td>${distance}%</td>
</tr>
`;
}
if (supportResistanceHtml === '') {
supportResistanceHtml = '<tr><td colspan="3" class="text-center">暂无支撑压力位数据</td></tr>';
}
$('#support-resistance-table').html(supportResistanceHtml);
// 渲染AI分析
const aiAnalysis = safeGet(analysisResult, 'ai_analysis', '暂无AI分析');
$('#ai-analysis').html(formatAIAnalysis(aiAnalysis));
// 安全地绘制图表
try {
renderScoreChart();
} catch (e) {
$('#score-chart').html('<div class="text-center text-muted">评分图表渲染失败</div>');
}
try {
renderRadarChart();
} catch (e) {
$('#radar-chart').html('<div class="text-center text-muted">雷达图表渲染失败</div>');
}
try {
renderPriceChart();
} catch (e) {
$('#price-chart').html('<div class="text-center text-muted">价格图表渲染失败</div>');
}
try {
renderIndicatorsChart();
} catch (e) {
$('#indicators-chart').html('<div class="text-center text-muted">指标图表渲染失败</div>');
}
} catch (error) {
showError(`渲染分析结果时出错: ${error.message}`);
}
}
// 绘制评分图表
function renderScoreChart() {
if (!analysisResult) return;
const totalScore = analysisResult.scores.total || 0;
const options = {
series: [totalScore],
chart: {
height: 180,
type: 'radialBar',
toolbar: {
show: false
}
},
plotOptions: {
radialBar: {
hollow: {
size: '70%',
},
dataLabels: {
showOn: 'always',
name: {
show: true,
fontSize: '14px',
fontWeight: 600,
offsetY: -10
},
value: {
formatter: function(val) {
return val;
},
fontSize: '22px',
fontWeight: 700,
offsetY: 5
}
}
}
},
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
type: 'horizontal',
gradientToColors: ['#ABE5A1'],
stops: [0, 100]
}
},
stroke: {
lineCap: 'round'
},
labels: ['总分'],
colors: ['#20E647']
};
// 清除旧图表
$('#score-chart').empty();
const chart = new ApexCharts(document.querySelector("#score-chart"), options);
chart.render();
}
// 绘制雷达图
function renderRadarChart() {
if (!analysisResult) return;
const trendScore = analysisResult.scores.trend || 0;
const indicatorsScore = analysisResult.scores.indicators || 0;
const supportResistanceScore = analysisResult.scores.support_resistance || 0;
const volatilityVolumeScore = analysisResult.scores.volatility_volume || 0;
const options = {
series: [{
name: '评分',
data: [
trendScore,
indicatorsScore,
supportResistanceScore,
volatilityVolumeScore
]
}],
chart: {
height: 180,
type: 'radar',
toolbar: {
show: false
}
},
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() {
try {
// Create a fully separate array for each price series (no OHLC array)
const closePrices = stockData.map(item => {
// 处理numpy日期格式
let dateStr = item.date;
if (dateStr && typeof dateStr === 'object') {
dateStr = dateStr.toString().split('T')[0];
}
return {
x: new Date(dateStr + 'T00:00:00'), // 标准化日期
y: safeFormat(item.close)
};
});
const ma5Data = stockData.map(item => ({
x: new Date(item.date),
y: parseFloat(item.MA5 || 0)
}));
const ma20Data = stockData.map(item => ({
x: new Date(item.date),
y: parseFloat(item.MA20 || 0)
}));
const ma60Data = stockData.map(item => ({
x: new Date(item.date),
y: parseFloat(item.MA60 || 0)
}));
// Create chart options using line chart instead of candlestick
const priceOptions = {
series: [
{
name: '收盘价',
type: 'line',
data: closePrices
},
{
name: 'MA5',
type: 'line',
data: ma5Data
},
{
name: 'MA20',
type: 'line',
data: ma20Data
},
{
name: 'MA60',
type: 'line',
data: ma60Data
}
],
chart: {
height: 400,
type: 'line',
toolbar: {
show: true
},
animations: {
enabled: false
}
},
stroke: {
width: [3, 2, 2, 2],
curve: 'straight'
},
title: {
text: `价格走势图`,
align: 'left'
},
xaxis: {
type: 'datetime'
},
yaxis: {
labels: {
formatter: function(value) {
return formatNumber(value, 2);
}
}
},
tooltip: {
enabled: true,
shared: true,
intersect: false,
x: {
format: 'yyyy-MM-dd'
},
y: {
formatter: function(value) {
return formatNumber(value, 2);
}
}
},
legend: {
show: true,
position: 'top',
horizontalAlign: 'left'
},
markers: {
size: 0
},
grid: {
show: true
}
};
$('#price-chart').empty();
const chart = new ApexCharts(document.querySelector("#price-chart"), priceOptions);
chart.render();
} catch (error) {
// Show error message
$('#price-chart').html('<div class="alert alert-danger">图表加载失败: ' + error.message + '</div>');
}
}
// 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 renderPriceChartWithOHLC() {
try {
// Create OHLC data table to display below the chart
let ohlcTableHtml = '<div class="ohlc-data mt-3"><h6>价格数据 (最近5天)</h6><table class="table table-sm table-bordered">';
ohlcTableHtml += '<thead><tr><th>日期</th><th>开盘</th><th>最高</th><th>最低</th><th>收盘</th></tr></thead><tbody>';
// Get last 5 days of data
const recentData = stockData.slice(-5);
recentData.forEach(item => {
ohlcTableHtml += `<tr>
<td>${item.date}</td>
<td>${formatNumber(item.open, 2)}</td>
<td>${formatNumber(item.high, 2)}</td>
<td>${formatNumber(item.low, 2)}</td>
<td>${formatNumber(item.close, 2)}</td>
</tr>`;
});
ohlcTableHtml += '</tbody></table></div>';
// Create price line chart
const closePrices = stockData.map(item => {
// 处理numpy日期格式
let dateStr = item.date;
if (dateStr && typeof dateStr === 'object') {
dateStr = dateStr.toString().split('T')[0];
}
return {
x: new Date(dateStr + 'T00:00:00'), // 标准化日期
y: safeFormat(item.close)
};
});
const ma5Data = stockData.map(item => ({
x: new Date(item.date),
y: parseFloat(item.MA5 || 0)
}));
const ma20Data = stockData.map(item => ({
x: new Date(item.date),
y: parseFloat(item.MA20 || 0)
}));
const ma60Data = stockData.map(item => ({
x: new Date(item.date),
y: parseFloat(item.MA60 || 0)
}));
const priceOptions = {
series: [
{
name: '收盘价',
type: 'line',
data: closePrices
},
{
name: 'MA5',
type: 'line',
data: ma5Data
},
{
name: 'MA20',
type: 'line',
data: ma20Data
},
{
name: 'MA60',
type: 'line',
data: ma60Data
}
],
chart: {
height: 350,
type: 'line',
toolbar: {
show: true
},
animations: {
enabled: false
}
},
stroke: {
width: [3, 2, 2, 2]
},
colors: ['#FF4560', '#008FFB', '#00E396', '#775DD0'],
title: {
text: `价格走势图 (收盘价)`,
align: 'left'
},
xaxis: {
type: 'datetime'
},
yaxis: {
labels: {
formatter: function(value) {
return formatNumber(value, 2);
}
}
},
tooltip: {
enabled: true,
shared: true,
intersect: false,
x: {
format: 'yyyy-MM-dd'
}
},
legend: {
show: true
}
};
// Clear the chart div and prepare it for the chart + table
$('#price-chart').empty();
// Create a container for the chart
$('#price-chart').append('<div id="price-line-chart"></div>');
const chart = new ApexCharts(document.querySelector("#price-line-chart"), priceOptions);
chart.render();
// Append the OHLC table below the chart
$('#price-chart').append(ohlcTableHtml);
} catch (error) {
$('#price-chart').html('<div class="alert alert-danger">图表加载失败: ' + error.message + '</div>');
}
}
// 绘制技术指标图表
function renderIndicatorsChart() {
try {
// Create chart options inline without using variables
const indicatorOptions = {
series: [
{
name: 'MACD',
type: 'line',
data: stockData.map(item => ({
x: new Date(item.date),
y: parseFloat(item.MACD || 0)
}))
},
{
name: 'Signal',
type: 'line',
data: stockData.map(item => ({
x: new Date(item.date),
y: parseFloat(item.Signal || 0)
}))
},
{
name: 'Histogram',
type: 'bar',
data: stockData.map(item => ({
x: new Date(item.date),
y: parseFloat(item.MACD_hist || 0)
}))
},
{
name: 'RSI',
type: 'line',
data: stockData.map(item => ({
x: new Date(item.date),
y: parseFloat(item.RSI || 0)
}))
}
],
chart: {
height: 350,
type: 'line',
stacked: false,
toolbar: {
show: true
},
// Disable animations
animations: {
enabled: false
}
},
stroke: {
width: [3, 3, 0, 3],
curve: 'smooth'
},
xaxis: {
type: 'datetime'
},
yaxis: [
{
title: {
text: 'MACD',
},
seriesName: 'MACD',
labels: {
formatter: function(value) {
return formatNumber(value, 3);
}
}
},
{
show: false,
seriesName: 'Signal'
},
{
show: false,
seriesName: 'Histogram'
},
{
opposite: true,
title: {
text: 'RSI'
},
min: 0,
max: 100,
seriesName: 'RSI',
labels: {
formatter: function(value) {
return formatNumber(value, 2);
}
}
}
],
// Simplified tooltip configuration
tooltip: {
enabled: true,
shared: true,
intersect: false, // Important for preventing null element errors
hideEmptySeries: true,
x: {
format: 'yyyy-MM-dd'
},
y: {
formatter: function(value, { seriesIndex }) {
// Simplified formatter function
if (seriesIndex === 0) return `MACD: ${formatNumber(value, 3)}`;
if (seriesIndex === 1) return `Signal: ${formatNumber(value, 3)}`;
if (seriesIndex === 2) return `Histogram: ${formatNumber(value, 3)}`;
if (seriesIndex === 3) return `RSI: ${formatNumber(value, 2)}`;
return formatNumber(value, 2);
}
}
},
colors: ['#008FFB', '#00E396', '#CED4DC', '#FEB019'],
legend: {
show: true,
position: 'top',
horizontalAlign: 'left',
floating: false
},
// Explicitly set marker options to prevent errors
markers: {
size: 4,
strokeWidth: 0
}
};
$('#indicators-chart').empty();
const chart = new ApexCharts(document.querySelector("#indicators-chart"), indicatorOptions);
chart.render();
} catch (error) {
$('#indicators-chart').html('<div class="alert alert-danger">指标图表加载失败: ' + error.message + '</div>');
}
}
// 添加到script部分
$('#retry-button').click(function() {
// 隐藏错误和重试区域
$('#error-retry').hide();
// 重新加载分析
loadAnalysisResult();
});
</script>
{% endblock %}