trading.ai/web/templates/signals.html
2025-10-01 11:07:25 +08:00

463 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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