463 lines
25 KiB
HTML
463 lines
25 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}交易信号 - AI 智能选股大师{% endblock %}
|
||
|
||
{% block content %}
|
||
<!-- 页面标题和筛选 -->
|
||
<div class="row mb-4">
|
||
<div class="col-12">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<!-- 操作按钮组 -->
|
||
<div class="d-flex gap-2 align-items-center">
|
||
<button id="clearSignalsBtn" class="btn btn-outline-danger btn-sm">
|
||
<i class="fas fa-trash me-1"></i>清空信号
|
||
</button>
|
||
<button id="runAnalysisBtn" class="btn btn-outline-success btn-sm">
|
||
<i class="fas fa-play me-1"></i>立即分析
|
||
</button>
|
||
|
||
<!-- 分析状态显示 -->
|
||
<div id="analysisStatus" class="ms-3" style="display: none;">
|
||
<div class="d-flex align-items-center">
|
||
<div class="spinner-border spinner-border-sm text-primary me-2" role="status">
|
||
<span class="visually-hidden">分析中...</span>
|
||
</div>
|
||
<div class="analysis-status-text">
|
||
<small class="text-muted fw-bold">正在分析中...</small>
|
||
<div class="mt-1">
|
||
<div class="progress" style="width: 150px; height: 6px;">
|
||
<div id="analysisProgress" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
|
||
</div>
|
||
<small id="analysisInfo" class="text-muted">启动中...</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选表单 -->
|
||
<form method="GET" class="d-flex gap-2 align-items-center">
|
||
<select name="strategy" class="form-select form-select-sm">
|
||
<option value="">所有策略</option>
|
||
<option value="K线形态策略" {% if strategy_name == 'K线形态策略' %}selected{% endif %}>K线形态策略</option>
|
||
</select>
|
||
|
||
<select name="timeframe" class="form-select form-select-sm">
|
||
<option value="">所有周期</option>
|
||
<option value="daily" {% if timeframe == 'daily' or not timeframe %}selected{% endif %}>日线</option>
|
||
<option value="weekly" {% if timeframe == 'weekly' %}selected{% endif %}>周线</option>
|
||
</select>
|
||
|
||
<select name="days" class="form-select form-select-sm">
|
||
<option value="7" {% if days == 7 %}selected{% endif %}>最近7天</option>
|
||
<option value="15" {% if days == 15 %}selected{% endif %}>最近15天</option>
|
||
<option value="30" {% if days == 30 %}selected{% endif %}>最近30天</option>
|
||
<option value="90" {% if days == 90 %}selected{% endif %}>最近90天</option>
|
||
</select>
|
||
|
||
<select name="per_page" class="form-select form-select-sm">
|
||
<option value="20" {% if per_page == 20 %}selected{% endif %}>每页20条</option>
|
||
<option value="50" {% if per_page == 50 %}selected{% endif %}>每页50条</option>
|
||
<option value="100" {% if per_page == 100 %}selected{% endif %}>每页100条</option>
|
||
</select>
|
||
|
||
<button type="submit" class="btn btn-primary btn-sm text-nowrap">
|
||
<i class="fas fa-filter me-1"></i>筛选
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 信号表格 -->
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<div class="card fade-in">
|
||
<div class="card-header">
|
||
<h5 class="mb-0 text-primary fw-bold">
|
||
<i class="fas fa-table me-2"></i>详细信号数据
|
||
</h5>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
{% if signals_grouped %}
|
||
<div class="table-container">
|
||
{% for scan_hour, signals in signals_grouped.items() %}
|
||
<!-- 扫描时间分组标题 -->
|
||
<div class="scan-date-header bg-light px-3 py-2">
|
||
<h6 class="mb-0 text-primary fw-bold">
|
||
<i class="fas fa-clock me-2"></i>扫描时间: {{ scan_hour.strftime('%Y-%m-%d %H:%M') }}
|
||
<span class="badge bg-primary ms-2">{{ signals|length }} 条信号</span>
|
||
</h6>
|
||
</div>
|
||
|
||
<!-- 该扫描时间下的信号表格 -->
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th style="width: 250px;">股票</th>
|
||
<th style="width: 150px;">策略</th>
|
||
<th style="width: 100px;">周期</th>
|
||
<th style="width: 500px;">时间线流程</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for signal in signals %}
|
||
<tr>
|
||
<td>
|
||
<div class="d-flex align-items-center">
|
||
<a href="https://xueqiu.com/S/{% if signal.stock_code.endswith('.SZ') %}SZ{% elif signal.stock_code.endswith('.SH') %}SH{% elif signal.stock_code.startswith('0') or signal.stock_code.startswith('3') %}SZ{% else %}SH{% endif %}{{ signal.stock_code.split('.')[0] }}" target="_blank" class="stock-code-link me-2">
|
||
<div class="stock-code-badge">{{ signal.stock_code }}</div>
|
||
</a>
|
||
<div class="stock-name text-truncate">{{ signal.stock_name or '未知' }}</div>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<span class="badge bg-primary">{{ signal.strategy_name }}</span>
|
||
{% if signal.new_high_confirmed %}
|
||
<div class="mt-1">
|
||
<small class="badge bg-success">创新高回踩确认</small>
|
||
</div>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<span class="badge bg-{% if signal.timeframe == 'daily' %}primary{% else %}success{% endif %}">
|
||
{{ '日线' if signal.timeframe == 'daily' else '周线' }}
|
||
</span>
|
||
</td>
|
||
<td style="min-width: 500px; padding: 12px 20px;">
|
||
{% if signal.new_high_confirmed %}
|
||
<!-- 新格式:优化的创新高回踩确认时间线 -->
|
||
<div class="timeline-enhanced-wide">
|
||
<div class="timeline-flow">
|
||
<div class="timeline-step-wide">
|
||
<div class="timeline-marker-wide pattern-marker">
|
||
<i class="fas fa-chart-line"></i>
|
||
</div>
|
||
<div class="timeline-content-wide">
|
||
<div class="timeline-title">📅 模式识别</div>
|
||
<div class="timeline-date-wide">{{ signal.signal_date | datetime_format('%m-%d') }}</div>
|
||
<div class="timeline-info">突破价: {{ signal.breakout_price | currency }}元</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="timeline-arrow">
|
||
<i class="fas fa-arrow-right"></i>
|
||
</div>
|
||
|
||
{% if signal.new_high_date %}
|
||
<div class="timeline-step-wide">
|
||
<div class="timeline-marker-wide new-high-marker">
|
||
<i class="fas fa-rocket"></i>
|
||
</div>
|
||
<div class="timeline-content-wide">
|
||
<div class="timeline-title">🚀 创新高</div>
|
||
<div class="timeline-date-wide highlight">{{ signal.new_high_date | datetime_format('%m-%d') }}</div>
|
||
{% if signal.new_high_price %}
|
||
<div class="timeline-info">{{ signal.new_high_price | currency }}元</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="timeline-arrow">
|
||
<i class="fas fa-arrow-right"></i>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if signal.confirmation_date %}
|
||
<div class="timeline-step-wide">
|
||
<div class="timeline-marker-wide confirmation-marker">
|
||
<i class="fas fa-check-circle"></i>
|
||
</div>
|
||
<div class="timeline-content-wide">
|
||
<div class="timeline-title">✅ 回踩确认</div>
|
||
<div class="timeline-date-wide highlight">{{ signal.confirmation_date | datetime_format('%m-%d') }}</div>
|
||
{% if signal.confirmation_days %}
|
||
<div class="timeline-info">用时{{ signal.confirmation_days }}天</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 技术指标概要 -->
|
||
<div class="timeline-summary mt-2">
|
||
<span class="badge bg-{% if signal.breakout_pct and signal.breakout_pct > 3 %}success{% elif signal.breakout_pct and signal.breakout_pct > 1 %}warning{% else %}secondary{% endif %} me-1 badge-sm">
|
||
突破{{ signal.breakout_pct | percentage }}
|
||
</span>
|
||
<span class="badge bg-{% if signal.final_yang_entity_ratio and signal.final_yang_entity_ratio > 0.6 %}success{% elif signal.final_yang_entity_ratio and signal.final_yang_entity_ratio > 0.4 %}warning{% else %}secondary{% endif %} me-1 badge-sm">
|
||
实体{{ signal.final_yang_entity_ratio | percentage }}
|
||
</span>
|
||
<span class="badge bg-{% if signal.above_ema20 %}success{% else %}secondary{% endif %} me-1 badge-sm">
|
||
EMA20{{ '✅' if signal.above_ema20 else '❌' }}
|
||
</span>
|
||
{% if signal.pullback_distance %}
|
||
<span class="badge bg-{% if signal.pullback_distance > -2 %}success{% elif signal.pullback_distance > -5 %}warning{% else %}danger{% endif %} me-1 badge-sm">
|
||
回踩{{ signal.pullback_distance | currency }}%
|
||
</span>
|
||
{% endif %}
|
||
<span class="badge bg-light text-muted me-1 badge-sm">
|
||
{{ signal.scan_time | datetime_format('%m-%d %H:%M') }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
{% else %}
|
||
<!-- 旧格式:兼容显示 -->
|
||
<div class="timeline-simple">
|
||
<div class="text-muted">{{ signal.signal_date | datetime_format('%Y-%m-%d') }}</div>
|
||
<div class="text-success fw-bold">{{ signal.breakout_price | currency }}元</div>
|
||
</div>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
{% if total_pages > 1 %}
|
||
<nav aria-label="信号分页" class="mt-4">
|
||
<ul class="pagination justify-content-center">
|
||
<!-- 上一页 -->
|
||
<li class="page-item {% if not has_prev %}disabled{% endif %}">
|
||
<a class="page-link" href="{% if has_prev %}{{ url_for('signals', page=current_page-1, strategy=strategy_name, timeframe=timeframe, days=days, per_page=per_page) }}{% else %}#{% endif %}">
|
||
<i class="fas fa-chevron-left"></i> 上一页
|
||
</a>
|
||
</li>
|
||
|
||
<!-- 页码 -->
|
||
{% set start_page = [1, current_page - 2]|max %}
|
||
{% set end_page = [total_pages, current_page + 2]|min %}
|
||
|
||
{% if start_page > 1 %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="{{ url_for('signals', page=1, strategy=strategy_name, timeframe=timeframe, days=days, per_page=per_page) }}">1</a>
|
||
</li>
|
||
{% if start_page > 2 %}
|
||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||
{% endif %}
|
||
{% endif %}
|
||
|
||
{% for page_num in range(start_page, end_page + 1) %}
|
||
<li class="page-item {% if page_num == current_page %}active{% endif %}">
|
||
<a class="page-link" href="{{ url_for('signals', page=page_num, strategy=strategy_name, timeframe=timeframe, days=days, per_page=per_page) }}">{{ page_num }}</a>
|
||
</li>
|
||
{% endfor %}
|
||
|
||
{% if end_page < total_pages %}
|
||
{% if end_page < total_pages - 1 %}
|
||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||
{% endif %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="{{ url_for('signals', page=total_pages, strategy=strategy_name, timeframe=timeframe, days=days, per_page=per_page) }}">{{ total_pages }}</a>
|
||
</li>
|
||
{% endif %}
|
||
|
||
<!-- 下一页 -->
|
||
<li class="page-item {% if not has_next %}disabled{% endif %}">
|
||
<a class="page-link" href="{% if has_next %}{{ url_for('signals', page=current_page+1, strategy=strategy_name, timeframe=timeframe, days=days, per_page=per_page) }}{% else %}#{% endif %}">
|
||
下一页 <i class="fas fa-chevron-right"></i>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
{% endif %}
|
||
|
||
{% else %}
|
||
<div class="empty-state">
|
||
<i class="fas fa-signal"></i>
|
||
<h4>暂无信号数据</h4>
|
||
<p>请尝试调整筛选条件或稍后再试</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
$(document).ready(function() {
|
||
// 表格行点击高亮
|
||
$('tbody tr').on('click', function() {
|
||
$(this).toggleClass('table-active');
|
||
});
|
||
|
||
// 工具提示
|
||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||
|
||
// 如果没有timeframe参数,自动设置为daily并重新加载
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
if (!urlParams.has('timeframe')) {
|
||
urlParams.set('timeframe', 'daily');
|
||
window.location.search = urlParams.toString();
|
||
}
|
||
|
||
// 清空信号按钮
|
||
$('#clearSignalsBtn').on('click', function() {
|
||
if (confirm('确认要清空最近7天的信号数据吗?此操作不可恢复!')) {
|
||
const btn = $(this);
|
||
const originalText = btn.html();
|
||
|
||
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-1"></i>清空中...');
|
||
|
||
$.ajax({
|
||
url: '/api/clear_signals',
|
||
method: 'POST',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({
|
||
days: 7,
|
||
strategy_name: ''
|
||
}),
|
||
success: function(response) {
|
||
if (response.success) {
|
||
alert(`清空成功!删除了 ${response.deleted_count} 条信号记录`);
|
||
location.reload();
|
||
} else {
|
||
alert('清空失败:' + response.error);
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
alert('清空失败:网络错误或服务器异常');
|
||
console.error('清空信号失败:', error);
|
||
},
|
||
complete: function() {
|
||
btn.prop('disabled', false).html(originalText);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 全局变量
|
||
let analysisStatusInterval = null;
|
||
|
||
// 检查分析状态
|
||
function checkAnalysisStatus() {
|
||
$.ajax({
|
||
url: '/api/analysis_status',
|
||
method: 'GET',
|
||
success: function(response) {
|
||
if (response.success && response.data) {
|
||
updateAnalysisStatusUI(response.data);
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error('获取分析状态失败:', error);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 用于防止重复刷新的标志
|
||
let hasRefreshedAfterCompletion = false;
|
||
|
||
// 更新分析状态UI
|
||
function updateAnalysisStatusUI(status) {
|
||
const $statusDiv = $('#analysisStatus');
|
||
const $progress = $('#analysisProgress');
|
||
const $info = $('#analysisInfo');
|
||
const $runBtn = $('#runAnalysisBtn');
|
||
|
||
if (status.is_running) {
|
||
// 显示分析状态
|
||
$statusDiv.show();
|
||
$runBtn.prop('disabled', true).html('<i class="fas fa-cog fa-spin me-1"></i>分析中...');
|
||
|
||
// 更新进度条
|
||
$progress.css('width', status.progress + '%');
|
||
|
||
// 更新信息文本
|
||
const runningTime = Math.floor(status.running_time / 60);
|
||
const remainingTime = Math.max(0, Math.ceil((status.stock_count / 200 * 5) - (status.running_time / 60)));
|
||
|
||
if (status.progress >= 95) {
|
||
$info.text(`分析即将完成... ${status.progress}%`);
|
||
} else {
|
||
$info.text(`进度: ${status.progress}% | 已运行: ${runningTime}分钟 | 预计剩余: ${remainingTime}分钟`);
|
||
}
|
||
|
||
// 开始轮询(如果还没开始)
|
||
if (!analysisStatusInterval) {
|
||
analysisStatusInterval = setInterval(checkAnalysisStatus, 3000); // 每3秒检查一次
|
||
}
|
||
} else {
|
||
// 分析已完成,隐藏分析状态
|
||
$statusDiv.hide();
|
||
$runBtn.prop('disabled', false).html('<i class="fas fa-play me-1"></i>立即分析');
|
||
|
||
// 停止轮询
|
||
if (analysisStatusInterval) {
|
||
clearInterval(analysisStatusInterval);
|
||
analysisStatusInterval = null;
|
||
}
|
||
|
||
// 如果是刚完成分析且还没刷新过,则刷新页面
|
||
if (status.progress >= 95 && !hasRefreshedAfterCompletion) {
|
||
hasRefreshedAfterCompletion = true;
|
||
|
||
// 显示完成提示
|
||
$statusDiv.show();
|
||
$statusDiv.html(`
|
||
<div class="d-flex align-items-center">
|
||
<i class="fas fa-check-circle text-success me-2"></i>
|
||
<small class="text-success fw-bold">分析完成!正在加载新结果...</small>
|
||
</div>
|
||
`);
|
||
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 1500);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 立即分析按钮
|
||
$('#runAnalysisBtn').on('click', function() {
|
||
const stockCount = prompt('请输入要扫描的股票数量 (默认200):', '200');
|
||
|
||
if (stockCount === null) return; // 用户取消
|
||
|
||
const count = parseInt(stockCount) || 200;
|
||
|
||
if (count <= 0 || count > 1000) {
|
||
alert('股票数量必须在 1-1000 之间');
|
||
return;
|
||
}
|
||
|
||
if (confirm(`确认要立即分析 ${count} 只热门股票吗?分析可能需要几分钟时间`)) {
|
||
$.ajax({
|
||
url: '/api/run_analysis',
|
||
method: 'POST',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({
|
||
stock_count: count
|
||
}),
|
||
success: function(response) {
|
||
if (response.success) {
|
||
// 重置刷新标志
|
||
hasRefreshedAfterCompletion = false;
|
||
|
||
// 立即开始状态轮询
|
||
checkAnalysisStatus();
|
||
alert(`分析任务已启动!正在后台扫描 ${response.stock_count} 只股票\n\n启动时间: ${response.start_time}\n\n页面将自动显示分析进度`);
|
||
} else {
|
||
alert('启动分析失败:' + response.error);
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
alert('启动分析失败:网络错误或服务器异常');
|
||
console.error('启动分析失败:', error);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 页面加载时检查是否有正在运行的分析
|
||
checkAnalysisStatus();
|
||
});
|
||
</script>
|
||
{% endblock %} |