| {% 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 %} |