|
{% extends "layout.html" %}
|
|
|
|
{% block title %}风险监控 - 智能分析系统{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-3">
|
|
<div id="alerts-container"></div>
|
|
|
|
<div class="row 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 py-2">
|
|
<ul class="nav nav-tabs" id="risk-tabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="stock-risk-tab" data-bs-toggle="tab" data-bs-target="#stock-risk" type="button" role="tab" aria-controls="stock-risk" aria-selected="true">个股风险</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="portfolio-risk-tab" data-bs-toggle="tab" data-bs-target="#portfolio-risk" type="button" role="tab" aria-controls="portfolio-risk" aria-selected="false">组合风险</button>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content mt-3" id="risk-tabs-content">
|
|
<div class="tab-pane fade show active" id="stock-risk" role="tabpanel" aria-labelledby="stock-risk-tab">
|
|
<form id="stock-risk-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">
|
|
<button type="submit" class="btn btn-primary btn-sm w-100">
|
|
<i class="fas fa-search"></i> 分析风险
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="tab-pane fade" id="portfolio-risk" role="tabpanel" aria-labelledby="portfolio-risk-tab">
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle"></i> 分析投资组合的整体风险,识别高风险股票和风险集中度。
|
|
</div>
|
|
<button id="analyze-portfolio-btn" class="btn btn-primary btn-sm">
|
|
<i class="fas fa-briefcase"></i> 分析我的投资组合
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="loading-panel" class="text-center py-5" style="display: none;">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-3 mb-0">正在分析风险,请稍候...</p>
|
|
</div>
|
|
|
|
|
|
<div id="stock-risk-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 d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">风险概览</h5>
|
|
<span id="risk-level-badge" class="badge"></span>
|
|
</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">
|
|
<div id="risk-gauge-chart" style="height: 120px;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<h6>风险预警</h6>
|
|
<div id="risk-alerts" class="mt-2">
|
|
|
|
</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="risk-radar-chart" style="height: 220px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
<div class="col-md-6">
|
|
<h6>波动率指标</h6>
|
|
<p><span class="text-muted">当前波动率:</span> <span id="current-volatility" class="fw-bold"></span></p>
|
|
<p><span class="text-muted">变化率:</span> <span id="volatility-change" class="fw-bold"></span></p>
|
|
<p><span class="text-muted">风险等级:</span> <span id="volatility-risk-level" class="fw-bold"></span></p>
|
|
<p class="small text-muted" id="volatility-comment"></p>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div id="volatility-chart" style="height: 150px;"></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">
|
|
<h6>趋势指标</h6>
|
|
<p><span class="text-muted">当前趋势:</span> <span id="current-trend" class="fw-bold"></span></p>
|
|
<p><span class="text-muted">均线关系:</span> <span id="ma-relationship" class="fw-bold"></span></p>
|
|
<p><span class="text-muted">风险等级:</span> <span id="trend-risk-level" class="fw-bold"></span></p>
|
|
<p class="small text-muted" id="trend-comment"></p>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div id="trend-chart" style="height: 150px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
<div class="col-md-6">
|
|
<h6>反转信号</h6>
|
|
<p><span class="text-muted">反转信号数:</span> <span id="reversal-signals" class="fw-bold"></span></p>
|
|
<p><span class="text-muted">可能方向:</span> <span id="reversal-direction" class="fw-bold"></span></p>
|
|
<p><span class="text-muted">风险等级:</span> <span id="reversal-risk-level" class="fw-bold"></span></p>
|
|
<p class="small text-muted" id="reversal-comment"></p>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div id="reversal-chart" style="height: 150px;"></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">
|
|
<h6>成交量指标</h6>
|
|
<p><span class="text-muted">成交量比率:</span> <span id="volume-ratio" class="fw-bold"></span></p>
|
|
<p><span class="text-muted">成交量模式:</span> <span id="volume-pattern" class="fw-bold"></span></p>
|
|
<p><span class="text-muted">风险等级:</span> <span id="volume-risk-level" class="fw-bold"></span></p>
|
|
<p class="small text-muted" id="volume-comment"></p>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div id="volume-chart" style="height: 150px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div id="portfolio-risk-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 d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">组合风险概览</h5>
|
|
<span id="portfolio-risk-level-badge" class="badge"></span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-md-7">
|
|
<h3 class="mb-0 fs-4">我的投资组合</h3>
|
|
<p class="text-muted mb-0 small">包含 <span id="portfolio-stock-count">0</span> 只股票</p>
|
|
</div>
|
|
<div class="col-md-5 text-end">
|
|
<div id="portfolio-risk-gauge-chart" style="height: 120px;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<h6>风险集中度</h6>
|
|
<p><span class="text-muted">行业集中度:</span> <span id="industry-concentration" class="fw-bold"></span></p>
|
|
<p><span class="text-muted">高风险股票占比:</span> <span id="high-risk-concentration" class="fw-bold"></span></p>
|
|
<div id="portfolio-risk-alerts" class="mt-2">
|
|
|
|
</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="industry-distribution-chart" style="height: 220px;"></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 class="table-responsive">
|
|
<table class="table table-sm table-striped table-hover mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>代码</th>
|
|
<th>名称</th>
|
|
<th>风险评分</th>
|
|
<th>风险等级</th>
|
|
<th>权重</th>
|
|
<th>主要风险</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="high-risk-stocks-table">
|
|
|
|
</tbody>
|
|
</table>
|
|
</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 class="table-responsive">
|
|
<table class="table table-sm table-striped table-hover mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>代码</th>
|
|
<th>名称</th>
|
|
<th>预警类型</th>
|
|
<th>风险等级</th>
|
|
<th>预警信息</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="risk-alerts-table">
|
|
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
$(document).ready(function() {
|
|
|
|
$('#stock-risk-form').submit(function(e) {
|
|
e.preventDefault();
|
|
const stockCode = $('#stock-code').val().trim();
|
|
const marketType = $('#market-type').val();
|
|
|
|
if (!stockCode) {
|
|
showError('请输入股票代码!');
|
|
return;
|
|
}
|
|
|
|
analyzeStockRisk(stockCode, marketType);
|
|
});
|
|
|
|
|
|
$('#analyze-portfolio-btn').click(function() {
|
|
analyzePortfolioRisk();
|
|
});
|
|
});
|
|
|
|
function analyzeStockRisk(stockCode, marketType) {
|
|
$('#loading-panel').show();
|
|
$('#stock-risk-result').hide();
|
|
$('#portfolio-risk-result').hide();
|
|
|
|
$.ajax({
|
|
url: '/api/risk_analysis',
|
|
type: 'POST',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify({
|
|
stock_code: stockCode,
|
|
market_type: marketType
|
|
}),
|
|
success: function(response) {
|
|
$('#loading-panel').hide();
|
|
renderStockRiskAnalysis(response, stockCode);
|
|
$('#stock-risk-result').show();
|
|
},
|
|
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 analyzePortfolioRisk() {
|
|
|
|
const savedPortfolio = localStorage.getItem('portfolio');
|
|
if (!savedPortfolio) {
|
|
showError('您的投资组合为空,请先添加股票到投资组合');
|
|
return;
|
|
}
|
|
|
|
const portfolio = JSON.parse(savedPortfolio);
|
|
if (portfolio.length === 0) {
|
|
showError('您的投资组合为空,请先添加股票到投资组合');
|
|
return;
|
|
}
|
|
|
|
$('#loading-panel').show();
|
|
$('#stock-risk-result').hide();
|
|
$('#portfolio-risk-result').hide();
|
|
|
|
$.ajax({
|
|
url: '/api/portfolio_risk',
|
|
type: 'POST',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify({
|
|
portfolio: portfolio
|
|
}),
|
|
success: function(response) {
|
|
$('#loading-panel').hide();
|
|
renderPortfolioRiskAnalysis(response, portfolio);
|
|
$('#portfolio-risk-result').show();
|
|
},
|
|
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 renderStockRiskAnalysis(data, stockCode) {
|
|
|
|
$('#stock-name').text(data.stock_name || stockCode);
|
|
$('#stock-info').text(data.industry || '未知行业');
|
|
|
|
|
|
const riskScore = data.total_risk_score || 0;
|
|
const riskLevel = data.risk_level || '未知';
|
|
const riskLevelBadgeClass = getRiskLevelBadgeClass(riskLevel);
|
|
$('#risk-level-badge').text(riskLevel).removeClass().addClass(`badge ${riskLevelBadgeClass}`);
|
|
|
|
|
|
renderRiskAlerts(data.alerts || []);
|
|
|
|
|
|
renderRiskGaugeChart(riskScore);
|
|
|
|
|
|
renderRiskRadarChart(data);
|
|
|
|
|
|
const volatilityRisk = data.volatility_risk || {};
|
|
$('#current-volatility').text(formatPercent(volatilityRisk.value || 0, 2));
|
|
|
|
const volatilityChange = volatilityRisk.change || 0;
|
|
const volatilityChangeClass = volatilityChange >= 0 ? 'trend-up' : 'trend-down';
|
|
$('#volatility-change').text(formatPercent(volatilityChange, 2)).addClass(volatilityChangeClass);
|
|
|
|
$('#volatility-risk-level').text(volatilityRisk.risk_level || '未知');
|
|
$('#volatility-comment').text('波动率反映价格波动的剧烈程度,高波动率意味着高风险');
|
|
|
|
|
|
const trendRisk = data.trend_risk || {};
|
|
$('#current-trend').text(trendRisk.trend || '未知');
|
|
$('#ma-relationship').text(trendRisk.ma_relationship || '未知');
|
|
$('#trend-risk-level').text(trendRisk.risk_level || '未知');
|
|
$('#trend-comment').text('下降趋势中的股票有更高的风险,特别是在空头排列时');
|
|
|
|
|
|
const reversalRisk = data.reversal_risk || {};
|
|
$('#reversal-signals').text(reversalRisk.reversal_signals || 0);
|
|
$('#reversal-direction').text(reversalRisk.direction || '未知');
|
|
$('#reversal-risk-level').text(reversalRisk.risk_level || '未知');
|
|
$('#reversal-comment').text('多个技术指标同时出现反转信号,意味着趋势可能即将改变');
|
|
|
|
|
|
const volumeRisk = data.volume_risk || {};
|
|
$('#volume-ratio').text(formatNumber(volumeRisk.volume_ratio || 0, 2));
|
|
$('#volume-pattern').text(volumeRisk.pattern || '未知');
|
|
$('#volume-risk-level').text(volumeRisk.risk_level || '未知');
|
|
$('#volume-comment').text('成交量异常变化常常预示价格波动,尤其是量价背离时');
|
|
|
|
|
|
renderVolatilityChart([5.2, 3.8, 4.5, 7.2, 6.3]);
|
|
renderTrendChart([110, 108, 106, 102, 98]);
|
|
renderReversalChart([55, 60, 65, 72, 68]);
|
|
renderVolumeChart([1.2, 0.8, 1.5, 2.8, 2.1]);
|
|
}
|
|
|
|
function renderPortfolioRiskAnalysis(data, portfolio) {
|
|
|
|
$('#portfolio-stock-count').text(portfolio.length);
|
|
|
|
|
|
const riskScore = data.portfolio_risk_score || 0;
|
|
const riskLevel = data.risk_level || '未知';
|
|
const riskLevelBadgeClass = getRiskLevelBadgeClass(riskLevel);
|
|
$('#portfolio-risk-level-badge').text(riskLevel).removeClass().addClass(`badge ${riskLevelBadgeClass}`);
|
|
|
|
|
|
const riskConcentration = data.risk_concentration || {};
|
|
$('#industry-concentration').text(`${riskConcentration.max_industry || '未知'} (${formatPercent(riskConcentration.max_industry_weight || 0, 1)})`);
|
|
$('#high-risk-concentration').text(formatPercent(riskConcentration.high_risk_weight || 0, 1));
|
|
|
|
|
|
renderPortfolioRiskAlerts(data.alerts || []);
|
|
|
|
|
|
renderPortfolioRiskGaugeChart(riskScore);
|
|
|
|
|
|
renderIndustryDistributionChart(portfolio);
|
|
|
|
|
|
renderHighRiskStocksTable(data.high_risk_stocks || []);
|
|
|
|
|
|
renderRiskAlertsTable(data.alerts || []);
|
|
}
|
|
|
|
function renderRiskAlerts(alerts) {
|
|
let html = '';
|
|
|
|
if (alerts.length === 0) {
|
|
html = '<div class="alert alert-success">未发现显著风险因素</div>';
|
|
} else {
|
|
alerts.forEach(alert => {
|
|
const alertClass = getRiskAlertClass(alert.level);
|
|
html += `
|
|
<div class="alert ${alertClass} mb-2">
|
|
<strong>${alert.type}风险:</strong> ${alert.message}
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
$('#risk-alerts').html(html);
|
|
}
|
|
|
|
function renderPortfolioRiskAlerts(alerts) {
|
|
let html = '';
|
|
|
|
if (alerts.length === 0) {
|
|
html = '<div class="alert alert-success">投资组合风险均衡,未发现显著风险集中</div>';
|
|
} else {
|
|
let alertCount = 0;
|
|
alerts.forEach(alert => {
|
|
if (alertCount < 3) {
|
|
const alertClass = getRiskAlertClass(alert.level);
|
|
html += `
|
|
<div class="alert ${alertClass} mb-2">
|
|
<strong>${alert.stock_code}:</strong> ${alert.message}
|
|
</div>
|
|
`;
|
|
alertCount++;
|
|
}
|
|
});
|
|
|
|
if (alerts.length > 3) {
|
|
html += `<p class="text-muted small">还有 ${alerts.length - 3} 条风险预警,请查看下方详情表格</p>`;
|
|
}
|
|
}
|
|
|
|
$('#portfolio-risk-alerts').html(html);
|
|
}
|
|
|
|
function renderRiskGaugeChart(score) {
|
|
const options = {
|
|
series: [score],
|
|
chart: {
|
|
height: 120,
|
|
type: 'radialBar',
|
|
},
|
|
plotOptions: {
|
|
radialBar: {
|
|
hollow: {
|
|
size: '70%',
|
|
},
|
|
dataLabels: {
|
|
show: true,
|
|
name: {
|
|
show: false
|
|
},
|
|
value: {
|
|
fontSize: '16px',
|
|
fontWeight: 'bold',
|
|
offsetY: 5
|
|
}
|
|
},
|
|
track: {
|
|
background: '#f2f2f2'
|
|
}
|
|
}
|
|
},
|
|
fill: {
|
|
colors: [getRiskColor(score)]
|
|
},
|
|
labels: ['风险分数']
|
|
};
|
|
|
|
const chart = new ApexCharts(document.querySelector("#risk-gauge-chart"), options);
|
|
chart.render();
|
|
}
|
|
|
|
function renderPortfolioRiskGaugeChart(score) {
|
|
const options = {
|
|
series: [score],
|
|
chart: {
|
|
height: 120,
|
|
type: 'radialBar',
|
|
},
|
|
plotOptions: {
|
|
radialBar: {
|
|
hollow: {
|
|
size: '70%',
|
|
},
|
|
dataLabels: {
|
|
show: true,
|
|
name: {
|
|
show: false
|
|
},
|
|
value: {
|
|
fontSize: '16px',
|
|
fontWeight: 'bold',
|
|
offsetY: 5
|
|
}
|
|
},
|
|
track: {
|
|
background: '#f2f2f2'
|
|
}
|
|
}
|
|
},
|
|
fill: {
|
|
colors: [getRiskColor(score)]
|
|
},
|
|
labels: ['风险分数']
|
|
};
|
|
|
|
const chart = new ApexCharts(document.querySelector("#portfolio-risk-gauge-chart"), options);
|
|
chart.render();
|
|
}
|
|
|
|
function renderRiskRadarChart(data) {
|
|
const volatilityRisk = data.volatility_risk?.score || 0;
|
|
const trendRisk = data.trend_risk?.score || 0;
|
|
const reversalRisk = data.reversal_risk?.score || 0;
|
|
const volumeRisk = data.volume_risk?.score || 0;
|
|
|
|
const options = {
|
|
series: [{
|
|
name: '风险评分',
|
|
data: [volatilityRisk, trendRisk, reversalRisk, volumeRisk]
|
|
}],
|
|
chart: {
|
|
height: 220,
|
|
type: 'radar',
|
|
toolbar: {
|
|
show: false
|
|
}
|
|
},
|
|
xaxis: {
|
|
categories: ['波动率风险', '趋势风险', '反转风险', '成交量风险']
|
|
},
|
|
fill: {
|
|
opacity: 0.7,
|
|
colors: ['#dc3545']
|
|
},
|
|
markers: {
|
|
size: 4
|
|
},
|
|
title: {
|
|
text: '风险雷达图',
|
|
align: 'center',
|
|
style: {
|
|
fontSize: '14px'
|
|
}
|
|
}
|
|
};
|
|
|
|
const chart = new ApexCharts(document.querySelector("#risk-radar-chart"), options);
|
|
chart.render();
|
|
}
|
|
|
|
function renderIndustryDistributionChart(portfolio) {
|
|
|
|
const industries = {};
|
|
let totalWeight = 0;
|
|
|
|
portfolio.forEach(stock => {
|
|
const industry = stock.industry || '未知';
|
|
const weight = stock.weight || 1;
|
|
|
|
if (industries[industry]) {
|
|
industries[industry] += weight;
|
|
} else {
|
|
industries[industry] = weight;
|
|
}
|
|
|
|
totalWeight += weight;
|
|
});
|
|
|
|
const series = [];
|
|
const labels = [];
|
|
|
|
for (const industry in industries) {
|
|
if (industries.hasOwnProperty(industry)) {
|
|
series.push(industries[industry]);
|
|
labels.push(industry);
|
|
}
|
|
}
|
|
|
|
const options = {
|
|
series: series,
|
|
chart: {
|
|
height: 220,
|
|
type: 'pie',
|
|
},
|
|
labels: labels,
|
|
colors: ['#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b', '#858796', '#5a5c69', '#6f42c1'],
|
|
legend: {
|
|
position: 'bottom'
|
|
},
|
|
tooltip: {
|
|
y: {
|
|
formatter: function(value) {
|
|
return value + ' (' + ((value / totalWeight) * 100).toFixed(1) + '%)';
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const chart = new ApexCharts(document.querySelector("#industry-distribution-chart"), options);
|
|
chart.render();
|
|
}
|
|
|
|
function renderVolatilityChart(data) {
|
|
const options = {
|
|
series: [{
|
|
name: '波动率(%)',
|
|
data: data
|
|
}],
|
|
chart: {
|
|
height: 150,
|
|
type: 'line',
|
|
toolbar: {
|
|
show: false
|
|
}
|
|
},
|
|
stroke: {
|
|
curve: 'smooth',
|
|
width: 3
|
|
},
|
|
xaxis: {
|
|
labels: {
|
|
show: false
|
|
}
|
|
},
|
|
yaxis: {
|
|
labels: {
|
|
formatter: function(val) {
|
|
return val.toFixed(1) + '%';
|
|
},
|
|
style: {
|
|
fontSize: '10px'
|
|
}
|
|
}
|
|
},
|
|
colors: ['#dc3545'],
|
|
tooltip: {
|
|
y: {
|
|
formatter: function(value) {
|
|
return value.toFixed(2) + '%';
|
|
}
|
|
}
|
|
},
|
|
markers: {
|
|
size: 3
|
|
}
|
|
};
|
|
|
|
const chart = new ApexCharts(document.querySelector("#volatility-chart"), options);
|
|
chart.render();
|
|
}
|
|
|
|
function renderTrendChart(data) {
|
|
const options = {
|
|
series: [{
|
|
name: '价格',
|
|
data: data
|
|
}, {
|
|
name: 'MA20',
|
|
data: [109, 107, 105, 103, 101]
|
|
}],
|
|
chart: {
|
|
height: 150,
|
|
type: 'line',
|
|
toolbar: {
|
|
show: false
|
|
}
|
|
},
|
|
stroke: {
|
|
curve: 'straight',
|
|
width: [3, 2]
|
|
},
|
|
xaxis: {
|
|
labels: {
|
|
show: false
|
|
}
|
|
},
|
|
yaxis: {
|
|
labels: {
|
|
formatter: function(val) {
|
|
return val.toFixed(0);
|
|
},
|
|
style: {
|
|
fontSize: '10px'
|
|
}
|
|
}
|
|
},
|
|
colors: ['#dc3545', '#007bff'],
|
|
tooltip: {
|
|
y: {
|
|
formatter: function(value) {
|
|
return value.toFixed(2);
|
|
}
|
|
}
|
|
},
|
|
markers: {
|
|
size: 3
|
|
},
|
|
legend: {
|
|
show: false
|
|
}
|
|
};
|
|
|
|
const chart = new ApexCharts(document.querySelector("#trend-chart"), options);
|
|
chart.render();
|
|
}
|
|
|
|
function renderReversalChart(data) {
|
|
const options = {
|
|
series: [{
|
|
name: 'RSI',
|
|
data: data
|
|
}],
|
|
chart: {
|
|
height: 150,
|
|
type: 'line',
|
|
toolbar: {
|
|
show: false
|
|
}
|
|
},
|
|
stroke: {
|
|
curve: 'smooth',
|
|
width: 3
|
|
},
|
|
xaxis: {
|
|
labels: {
|
|
show: false
|
|
}
|
|
},
|
|
yaxis: {
|
|
min: 0,
|
|
max: 100,
|
|
labels: {
|
|
formatter: function(val) {
|
|
return val.toFixed(0);
|
|
},
|
|
style: {
|
|
fontSize: '10px'
|
|
}
|
|
}
|
|
},
|
|
colors: ['#ffc107'],
|
|
tooltip: {
|
|
y: {
|
|
formatter: function(value) {
|
|
return value.toFixed(2);
|
|
}
|
|
}
|
|
},
|
|
markers: {
|
|
size: 3
|
|
},
|
|
annotations: {
|
|
yaxis: [{
|
|
y: 70,
|
|
borderColor: '#dc3545',
|
|
label: {
|
|
text: '超买',
|
|
style: {
|
|
color: '#fff',
|
|
background: '#dc3545'
|
|
}
|
|
}
|
|
}, {
|
|
y: 30,
|
|
borderColor: '#28a745',
|
|
label: {
|
|
text: '超卖',
|
|
style: {
|
|
color: '#fff',
|
|
background: '#28a745'
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
};
|
|
|
|
const chart = new ApexCharts(document.querySelector("#reversal-chart"), options);
|
|
chart.render();
|
|
}
|
|
|
|
function renderVolumeChart(data) {
|
|
const options = {
|
|
series: [{
|
|
name: '成交量比率',
|
|
data: data
|
|
}],
|
|
chart: {
|
|
height: 150,
|
|
type: 'bar',
|
|
toolbar: {
|
|
show: false
|
|
}
|
|
},
|
|
xaxis: {
|
|
labels: {
|
|
show: false
|
|
}
|
|
},
|
|
yaxis: {
|
|
labels: {
|
|
formatter: function(val) {
|
|
return val.toFixed(1) + 'x';
|
|
},
|
|
style: {
|
|
fontSize: '10px'
|
|
}
|
|
}
|
|
},
|
|
colors: ['#4e73df'],
|
|
tooltip: {
|
|
y: {
|
|
formatter: function(value) {
|
|
return value.toFixed(2) + 'x';
|
|
}
|
|
}
|
|
},
|
|
plotOptions: {
|
|
bar: {
|
|
columnWidth: '50%'
|
|
}
|
|
},
|
|
dataLabels: {
|
|
enabled: false
|
|
}
|
|
};
|
|
|
|
const chart = new ApexCharts(document.querySelector("#volume-chart"), options);
|
|
chart.render();
|
|
}
|
|
|
|
function renderHighRiskStocksTable(highRiskStocks) {
|
|
let html = '';
|
|
|
|
if (highRiskStocks.length === 0) {
|
|
html = '<tr><td colspan="7" class="text-center">未发现高风险股票</td></tr>';
|
|
} else {
|
|
highRiskStocks.forEach(stock => {
|
|
const riskScoreClass = getRiskScoreClass(stock.risk_score);
|
|
const riskLevelBadgeClass = getRiskLevelBadgeClass(stock.risk_level);
|
|
|
|
html += `
|
|
<tr>
|
|
<td>${stock.stock_code}</td>
|
|
<td>${stock.stock_name || '未知'}</td>
|
|
<td><span class="badge ${riskScoreClass}">${stock.risk_score}</span></td>
|
|
<td><span class="badge ${riskLevelBadgeClass}">${stock.risk_level}</span></td>
|
|
<td>${formatPercent(stock.weight || 0, 1)}</td>
|
|
<td>${stock.main_risk || '未知'}</td>
|
|
<td>
|
|
<a href="/stock_detail/${stock.stock_code}" class="btn btn-sm btn-outline-info me-1">
|
|
<i class="fas fa-chart-line"></i>
|
|
</a>
|
|
<a href="/risk_monitor?stock=${stock.stock_code}" class="btn btn-sm btn-outline-danger">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
$('#high-risk-stocks-table').html(html);
|
|
}
|
|
|
|
function renderRiskAlertsTable(alerts) {
|
|
let html = '';
|
|
|
|
if (alerts.length === 0) {
|
|
html = '<tr><td colspan="5" class="text-center">暂无风险预警</td></tr>';
|
|
} else {
|
|
alerts.forEach(alert => {
|
|
const riskLevelBadgeClass = getRiskLevelBadgeClass(alert.level);
|
|
|
|
html += `
|
|
<tr>
|
|
<td>${alert.stock_code}</td>
|
|
<td>${alert.stock_name || '未知'}</td>
|
|
<td>${alert.type}</td>
|
|
<td><span class="badge ${riskLevelBadgeClass}">${alert.level}</span></td>
|
|
<td>${alert.message}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
$('#risk-alerts-table').html(html);
|
|
}
|
|
|
|
function getRiskLevelBadgeClass(level) {
|
|
switch (level) {
|
|
case '极高':
|
|
return 'bg-danger';
|
|
case '高':
|
|
return 'bg-warning text-dark';
|
|
case '中等':
|
|
return 'bg-info text-dark';
|
|
case '低':
|
|
return 'bg-success';
|
|
case '极低':
|
|
return 'bg-secondary';
|
|
default:
|
|
return 'bg-secondary';
|
|
}
|
|
}
|
|
|
|
function getRiskAlertClass(level) {
|
|
switch (level) {
|
|
case '高':
|
|
return 'alert-danger';
|
|
case '中':
|
|
return 'alert-warning';
|
|
case '低':
|
|
return 'alert-info';
|
|
default:
|
|
return 'alert-secondary';
|
|
}
|
|
}
|
|
|
|
function getRiskScoreClass(score) {
|
|
if (score >= 80) return 'bg-danger';
|
|
if (score >= 60) return 'bg-warning text-dark';
|
|
if (score >= 40) return 'bg-info text-dark';
|
|
return 'bg-success';
|
|
}
|
|
|
|
function getRiskColor(score) {
|
|
if (score >= 80) return '#dc3545';
|
|
if (score >= 60) return '#ffc107';
|
|
if (score >= 40) return '#17a2b8';
|
|
return '#28a745';
|
|
}
|
|
</script>
|
|
{% endblock %} |