fix: 修复复盘计划bug

master^2
Lxy 4 weeks ago
parent 90cccbf1df
commit 8a1918fc8d

@ -42,3 +42,28 @@ def init_analysis_db():
SymbolScoreV2, TradingPlanV2, SectorHeat, ReviewPlanV2,
)
AnalysisBase.metadata.create_all(bind=analysis_engine)
# 迁移为已有表添加新增列SQLite 不支持 IF NOT EXISTS用异常处理
_migrate_add_columns()
def _migrate_add_columns():
"""为已有表添加新增列(兼容旧数据库)"""
import sqlite3
conn = sqlite3.connect(str(ANALYSIS_DB_PATH))
cursor = conn.cursor()
migrations = [
("symbol_scores_v2", "data_date", "VARCHAR(16)"),
("review_plans_v2", "actual_data_date", "VARCHAR(16)"),
("review_plans_v2", "data_date_matches", "INTEGER"),
]
for table, column, col_type in migrations:
try:
cursor.execute(f"ALTER TABLE {table} ADD COLUMN {column} {col_type}")
except sqlite3.OperationalError:
pass # 列已存在
conn.commit()
conn.close()

@ -200,6 +200,8 @@ class SymbolScoreV2(AnalysisBase):
s2 = Column(Float, nullable=True, comment="支撑位2")
# 排名
rank = Column(Integer, nullable=True, comment="综合排名")
# 实际数据日期
data_date = Column(String(16), nullable=True, comment="实际数据日期 YYYY-MM-DD日线最后一根K线的日期")
def __repr__(self):
return f"<SymbolScoreV2 {self.symbol} score={self.composite_score}>"
@ -267,6 +269,8 @@ class ReviewPlanV2(AnalysisBase):
neutral_count = Column(Integer, nullable=True, comment="震荡品种数")
opportunity_count = Column(Integer, nullable=True, comment="交易机会数")
risk_warnings = Column(JSON, nullable=True, comment="风险提示列表")
actual_data_date = Column(String(16), nullable=True, comment="实际数据日期 YYYY-MM-DD")
data_date_matches = Column(Integer, nullable=True, comment="数据日期是否与复盘日期一致 1=是 0=否")
created_at = Column(DateTime, nullable=False, default=datetime.now)
def __repr__(self):

@ -3,6 +3,7 @@ V2 交易计划生成器 - 5维度综合评分 + 多周期共振分析
"""
import json
import logging
from collections import Counter
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from datetime import datetime
@ -54,6 +55,21 @@ def load_symbols_config() -> Dict[str, str]:
return data.get("futures", {})
def _get_candle_date(candle: dict) -> Optional[str]:
"""从K线数据中提取日期字符串 (YYYY-MM-DD)"""
dt_str = candle.get("datetime") or candle.get("time")
if not dt_str:
return None
try:
if isinstance(dt_str, str):
dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
else:
dt = dt_str
return dt.strftime("%Y-%m-%d")
except Exception:
return None
def _get_candle_values(candles: List[dict]) -> Tuple[list, list, list, list, list]:
"""从K线列表提取 OHLCV"""
opens, highs, lows, closes, volumes = [], [], [], [], []
@ -322,6 +338,9 @@ def generate_plan(db: Session, review_date_str: str, week_day: str) -> dict:
prev_close = d_closes[-2] if len(d_closes) >= 2 else today_close
today_volume = d_volumes[-1]
# 提取实际数据日期日线最后一根K线的日期
data_date = _get_candle_date(daily[-1])
# 近5日均量
recent_vols = d_volumes[-6:-1] if len(d_volumes) >= 6 else d_volumes[:-1]
avg_vol_5 = sum(recent_vols) / len(recent_vols) if recent_vols else today_volume
@ -364,6 +383,7 @@ def generate_plan(db: Session, review_date_str: str, week_day: str) -> dict:
raw_scores.append({
"symbol": symbol,
"name": name,
"data_date": data_date,
"close_price": today_close,
"prev_close": prev_close,
"high_price": today_high,
@ -488,6 +508,12 @@ def generate_plan(db: Session, review_date_str: str, week_day: str) -> dict:
bear_count = sum(1 for s in raw_scores if s["trend_score"] < 30)
neutral_count = len(raw_scores) - bull_count - bear_count
# 统计实际数据日期(取出现最多的日期作为基准数据日期)
date_counts = Counter(s.get("data_date") for s in raw_scores if s.get("data_date"))
actual_data_date = date_counts.most_common(1)[0][0] if date_counts else review_date_str
# 判断数据日期是否与复盘日期一致
data_date_matches = (actual_data_date == review_date_str)
# 核心结论
top3 = raw_scores[:3]
top_names = [f"{s['symbol']} {s['name']}" for s in top3]
@ -516,10 +542,18 @@ def generate_plan(db: Session, review_date_str: str, week_day: str) -> dict:
f"量比超2.5倍,需确认量能可持续性"
)
# 构建数据基准描述
if data_date_matches:
data_basis = f"{actual_data_date} 收盘 ({len(symbol_data)} 品种) | 多周期共振分析"
else:
data_basis = f"复盘日期: {review_date_str} | 数据截至: {actual_data_date} 收盘 ({len(symbol_data)} 品种) | 多周期共振分析"
return {
"review_date": review_date_str,
"week_day": week_day,
"data_basis": f"{review_date_str} 收盘 ({len(symbol_data)} 品种) | 多周期共振分析",
"actual_data_date": actual_data_date,
"data_date_matches": data_date_matches,
"data_basis": data_basis,
"core_conclusion": core_conclusion,
"bull_count": bull_count,
"bear_count": bear_count,
@ -617,6 +651,8 @@ def save_plan_to_db(db: Session, plan_data: dict) -> int:
existing.neutral_count = plan_data.get("neutral_count")
existing.opportunity_count = plan_data.get("opportunity_count")
existing.risk_warnings = plan_data.get("risk_warnings")
existing.actual_data_date = plan_data.get("actual_data_date")
existing.data_date_matches = 1 if plan_data.get("data_date_matches") else 0
else:
review_plan = ReviewPlanV2(
review_date=review_date_str,
@ -628,6 +664,8 @@ def save_plan_to_db(db: Session, plan_data: dict) -> int:
neutral_count=plan_data.get("neutral_count"),
opportunity_count=plan_data.get("opportunity_count"),
risk_warnings=plan_data.get("risk_warnings"),
actual_data_date=plan_data.get("actual_data_date"),
data_date_matches=1 if plan_data.get("data_date_matches") else 0,
)
db.add(review_plan)
db.flush()
@ -667,6 +705,7 @@ def save_plan_to_db(db: Session, plan_data: dict) -> int:
s1=pivots.get("s1"),
s2=pivots.get("s2"),
rank=s.get("rank"),
data_date=s.get("data_date"),
)
db.add(score_record)
@ -736,6 +775,8 @@ def get_plan_data(db: Session, review_plan_id: int) -> Optional[dict]:
"review_date": plan_meta.review_date,
"week_day": plan_meta.week_day,
"data_basis": plan_meta.data_basis,
"actual_data_date": plan_meta.actual_data_date,
"data_date_matches": bool(plan_meta.data_date_matches),
"core_conclusion": plan_meta.core_conclusion,
"bull_count": plan_meta.bull_count,
"bear_count": plan_meta.bear_count,
@ -755,6 +796,7 @@ def _score_to_dict(s: SymbolScoreV2) -> dict:
"id": s.id,
"symbol": s.symbol,
"name": s.name,
"data_date": s.data_date,
"close_price": s.close_price,
"prev_close": s.prev_close,
"high_price": s.high_price,

@ -410,6 +410,45 @@
.rv-tag-bear { background: rgba(255,59,48,0.12); color: var(--color-up); }
.rv-tag-neutral { background: #F5F5F7; color: var(--text-tertiary); }
/* 列表表格样式(重点关注/规避) */
.rv-list-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.rv-list-table th { background: #F5F5F7; padding: 12px; text-align: left; font-weight: 600; color: var(--text-tertiary); font-size: 12px; }
.rv-list-table td { padding: 10px 12px; border-bottom: 1px solid rgba(0,0,0,0.04); color: var(--text-primary); }
.rv-list-table tbody tr { cursor: pointer; transition: background 0.15s; }
.rv-yellow-table tbody tr:hover { background: rgba(255,149,0,0.05); }
.rv-red-table tbody tr:hover { background: rgba(255,59,48,0.05); }
/* 排序控件 */
.rv-sort-controls { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.rv-sort-btn { background: #fff; color: var(--text-secondary); border: 1px solid rgba(0,0,0,0.08); border-radius: 9999px; padding: 6px 14px; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.rv-sort-btn:hover { background: #F5F5F7; border-color: var(--color-brand); }
.rv-sort-btn.active { background: var(--color-brand); color: #fff; border-color: var(--color-brand); }
/* 弹窗样式 */
.rv-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(8px); z-index: 2000; display: none; align-items: center; justify-content: center; }
.rv-modal-overlay.show { display: flex; }
.rv-modal { background: #fff; border-radius: 20px; width: 90%; max-width: 700px; max-height: 85vh; overflow-y: auto; box-shadow: 0 25px 50px rgba(0,0,0,0.25); animation: rv-modal-in 0.25s ease; }
@keyframes rv-modal-in { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
.rv-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; border-bottom: 1px solid rgba(0,0,0,0.06); }
.rv-modal-title { font-size: 18px; font-weight: 700; color: var(--text-primary); }
.rv-modal-close { width: 32px; height: 32px; border-radius: 50%; border: none; background: #F5F5F7; font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); transition: all 0.2s; }
.rv-modal-close:hover { background: #E5E5E7; }
.rv-modal-body { padding: 20px 24px; }
.rv-modal-section { margin-bottom: 20px; }
.rv-modal-section-title { font-size: 12px; font-weight: 600; color: var(--text-tertiary); margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
.rv-modal-metrics-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.rv-modal-metric { background: #F5F5F7; border-radius: 10px; padding: 12px; text-align: center; }
.rv-modal-metric .mm-label { font-size: 11px; color: var(--text-tertiary); display: block; margin-bottom: 4px; }
.rv-modal-metric .mm-val { font-size: 16px; font-weight: 700; color: var(--text-primary); }
.rv-modal-plan-box { background: #F5F5F7; border-radius: 12px; padding: 14px; }
.rv-modal-plan-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 13px; }
.rv-modal-plan-row .mp-label { color: var(--text-tertiary); }
.rv-modal-plan-row .mp-val { font-weight: 600; color: var(--text-primary); }
.rv-modal-plan-row .mp-val.stop { color: var(--color-up); }
.rv-modal-plan-row .mp-val.target { color: var(--color-down); }
.rv-modal-pivots { display: flex; flex-wrap: wrap; gap: 8px; }
.rv-data-date-tag { display: inline-block; font-size: 10px; color: var(--text-tertiary); background: #F5F5F7; padding: 2px 6px; border-radius: 4px; margin-left: 4px; }
.rv-ranking-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.rv-ranking-table th { background: #F5F5F7; padding: 12px; text-align: left; font-weight: 600; color: var(--text-tertiary); font-size: 12px; text-transform: uppercase; }
.rv-ranking-table td { padding: 10px 12px; border-bottom: 1px solid rgba(0,0,0,0.04); color: var(--text-primary); }
@ -744,18 +783,61 @@
<!-- 重点关注 -->
<section id="rv-yellow-section" class="rv-section" style="display: none;">
<h2 class="rv-section-title">👀 重点关注品种</h2>
<div id="rv-yellow-list" class="rv-watch-list"></div>
<div style="overflow-x: auto;">
<table class="rv-list-table rv-yellow-table">
<thead>
<tr>
<th>#</th>
<th>品种</th>
<th>收盘价</th>
<th>涨跌幅</th>
<th>综合评分</th>
<th>振幅%</th>
<th>量比</th>
<th>方向</th>
<th>数据日期</th>
</tr>
</thead>
<tbody id="rv-yellow-list"></tbody>
</table>
</div>
</section>
<!-- 规避 -->
<section id="rv-red-section" class="rv-section" style="display: none;">
<h2 class="rv-section-title">🔴 规避品种</h2>
<div id="rv-red-list" class="rv-avoid-list"></div>
<div style="overflow-x: auto;">
<table class="rv-list-table rv-red-table">
<thead>
<tr>
<th>#</th>
<th>品种</th>
<th>收盘价</th>
<th>涨跌幅</th>
<th>综合评分</th>
<th>振幅%</th>
<th>量比</th>
<th>方向</th>
<th>数据日期</th>
</tr>
</thead>
<tbody id="rv-red-list"></tbody>
</table>
</div>
</section>
<!-- 全品种排名 -->
<section id="rv-table-section" class="rv-section" style="display: none;">
<h2 class="rv-section-title">📊 全品种综合排名</h2>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h2 class="rv-section-title" style="margin-bottom: 0;">📊 全品种综合排名</h2>
<div class="rv-sort-controls" id="rv-sort-controls">
<span style="font-size: 12px; color: var(--text-tertiary); margin-right: 6px;">排序:</span>
<button class="rv-sort-btn active" data-sort="composite_score" data-order="desc">综合评分</button>
<button class="rv-sort-btn" data-sort="activity_score" data-order="desc">活跃度</button>
<button class="rv-sort-btn" data-sort="volume_ratio" data-order="desc">量比</button>
<button class="rv-sort-btn" data-sort="direction_tag" data-order="asc">方向</button>
</div>
</div>
<div style="overflow-x: auto;">
<table id="rv-ranking-table" class="rv-ranking-table"></table>
</div>
@ -788,6 +870,19 @@
</div>
</div>
<!-- 复盘品种详情弹窗(移到 review-view 外部) -->
<div id="rv-detail-modal" class="rv-modal-overlay" onclick="if(event.target===this)rvCloseDetailModal()">
<div class="rv-modal">
<div class="rv-modal-header">
<span id="rv-modal-title" class="rv-modal-title">品种详情</span>
<button class="rv-modal-close" onclick="rvCloseDetailModal()"></button>
</div>
<div id="rv-modal-body" class="rv-modal-body">
<!-- 由 JS 动态填充 -->
</div>
</div>
</div>
<!-- AI分析详情对话框 -->
<div class="modal-overlay" id="ai-analysis-modal">
<div class="modal-content modal-large">

@ -2211,6 +2211,8 @@ function initReviewPlan() {
rvHandleClear();
});
rvSetupSortButtons();
rvLoadLatestPlan();
}
@ -2462,18 +2464,19 @@ function rvRenderGreenSection(plans, scores) {
const score = scores.find(s => s.symbol === p.symbol) || {};
const dirText = p.direction === 'long' ? '做多' : '做空';
const dirColor = p.direction === 'long' ? 'var(--color-down)' : 'var(--color-up)';
const dataDateTag = score.data_date ? `<span class="rv-data-date-tag">${score.data_date}</span>` : '';
return `
<div class="rv-opp-card">
<div class="rv-opp-card" onclick="rvShowDetail('${p.symbol}','${(p.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<div class="rv-opp-header">
<span class="rv-opp-symbol">${p.symbol} ${p.name}</span>
<span class="rv-opp-score">${p.composite_score}</span>
</div>
<div class="rv-opp-info">
<div class="rv-opp-info-item"><span class="rv-opp-info-label">方向</span><span style="color: ${dirColor}; font-weight: 600;">${dirText}</span></div>
<div class="rv-opp-info-item"><span class="rv-opp-info-label">收盘价</span><span>${score.close_price || '-'} ${dataDateTag}</span></div>
<div class="rv-opp-info-item"><span class="rv-opp-info-label">涨跌幅</span><span>${score.change_pct || 0}%</span></div>
<div class="rv-opp-info-item"><span class="rv-opp-info-label">量比</span><span>${score.volume_ratio || '-'}</span></div>
<div class="rv-opp-info-item"><span class="rv-opp-info-label">振幅</span><span>${score.amplitude_pct || 0}%</span></div>
</div>
<div class="rv-score-bars">
${rvScoreBar(p.amplitude_score || 0, '振幅')}
@ -2497,8 +2500,8 @@ function rvRenderGreenSection(plans, scores) {
function rvRenderYellowSection(scores) {
const section = document.getElementById('rv-yellow-section');
const list = document.getElementById('rv-yellow-list');
if (!section || !list) return;
const tbody = document.getElementById('rv-yellow-list');
if (!section || !tbody) return;
const yellowItems = scores.filter(s => s.category === 'yellow');
if (yellowItems.length === 0) {
@ -2506,22 +2509,30 @@ function rvRenderYellowSection(scores) {
return;
}
list.innerHTML = yellowItems.map(s => `
<div class="rv-watch-item">
<span class="rv-item-symbol">${s.symbol} ${s.name}</span>
<span class="rv-item-score">${s.composite_score}</span>
${rvDirectionTag(s.direction_tag)}
<span style="color: ${s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)'};">${s.change_pct >= 0 ? '+' : ''}${s.change_pct}%</span>
</div>
`).join('');
tbody.innerHTML = yellowItems.map((s, i) => {
const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)';
return `
<tr onclick="rvShowDetail('${s.symbol}','${(s.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<td>${i + 1}</td>
<td><strong>${s.symbol}</strong> ${s.name}</td>
<td>${s.close_price || '-'}${s.data_date ? '<span class="rv-data-date-tag">' + s.data_date + '</span>' : ''}</td>
<td style="color: ${changeColor};">${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}%</td>
<td><strong>${s.composite_score}</strong></td>
<td>${s.amplitude_pct || 0}%</td>
<td>${s.volume_ratio || '-'}</td>
<td>${rvDirectionTag(s.direction_tag)}</td>
<td style="font-size:11px;color:var(--text-tertiary);">${s.data_date || '-'}</td>
</tr>
`;
}).join('');
section.style.display = '';
}
function rvRenderRedSection(scores) {
const section = document.getElementById('rv-red-section');
const list = document.getElementById('rv-red-list');
if (!section || !list) return;
const tbody = document.getElementById('rv-red-list');
if (!section || !tbody) return;
const redItems = scores.filter(s => s.category === 'red');
if (redItems.length === 0) {
@ -2529,18 +2540,29 @@ function rvRenderRedSection(scores) {
return;
}
list.innerHTML = redItems.map(s => `
<div class="rv-avoid-item">
<span class="rv-item-symbol">${s.symbol} ${s.name}</span>
<span class="rv-item-score">${s.composite_score}</span>
${rvDirectionTag(s.direction_tag)}
<span style="color: ${s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)'};">${s.change_pct >= 0 ? '+' : ''}${s.change_pct}%</span>
</div>
`).join('');
tbody.innerHTML = redItems.map((s, i) => {
const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)';
return `
<tr onclick="rvShowDetail('${s.symbol}','${(s.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<td>${i + 1}</td>
<td><strong>${s.symbol}</strong> ${s.name}</td>
<td>${s.close_price || '-'}${s.data_date ? '<span class="rv-data-date-tag">' + s.data_date + '</span>' : ''}</td>
<td style="color: ${changeColor};">${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}%</td>
<td><strong>${s.composite_score}</strong></td>
<td>${s.amplitude_pct || 0}%</td>
<td>${s.volume_ratio || '-'}</td>
<td>${rvDirectionTag(s.direction_tag)}</td>
<td style="font-size:11px;color:var(--text-tertiary);">${s.data_date || '-'}</td>
</tr>
`;
}).join('');
section.style.display = '';
}
let rvCurrentSortField = 'composite_score';
let rvCurrentSortOrder = 'desc';
function rvRenderRankingTable(scores) {
const section = document.getElementById('rv-table-section');
const table = document.getElementById('rv-ranking-table');
@ -2551,6 +2573,25 @@ function rvRenderRankingTable(scores) {
return;
}
// 排序
const sorted = [...scores].sort((a, b) => {
let va = a[rvCurrentSortField];
let vb = b[rvCurrentSortField];
// 方向排序特殊处理
if (rvCurrentSortField === 'direction_tag') {
const dirOrder = { '强多': 1, '偏多': 2, '多头共振': 1, '震荡': 3, '偏空': 4, '强空': 5, '空头共振': 5 };
va = dirOrder[va] || 3;
vb = dirOrder[vb] || 3;
}
if (va == null) va = -Infinity;
if (vb == null) vb = -Infinity;
if (rvCurrentSortOrder === 'asc') return va > vb ? 1 : va < vb ? -1 : 0;
return va < vb ? 1 : va > vb ? -1 : 0;
});
let html = `
<thead>
<tr>
@ -2564,20 +2605,21 @@ function rvRenderRankingTable(scores) {
<th>趋势分</th>
<th>方向</th>
<th>活跃度</th>
<th>数据日期</th>
</tr>
</thead>
<tbody>
`;
scores.forEach(s => {
sorted.forEach((s, i) => {
const scoreColor = rvScoreBarColor(s.composite_score);
const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)';
html += `
<tr>
<td>${s.rank || '-'}</td>
<tr onclick="rvShowDetail('${s.symbol}','${(s.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<td>${i + 1}</td>
<td><strong>${s.symbol}</strong> ${s.name}</td>
<td>${s.close_price || '-'}</td>
<td>${s.close_price || '-'}${s.data_date ? '<span class="rv-data-date-tag">' + s.data_date + '</span>' : ''}</td>
<td>
<strong>${s.composite_score}</strong>
<div class="rv-table-score-bar">
@ -2590,6 +2632,7 @@ function rvRenderRankingTable(scores) {
<td>${s.trend_score || 0}</td>
<td>${rvDirectionTag(s.direction_tag)}</td>
<td>${s.activity_score || 0}</td>
<td style="font-size:11px;color:var(--text-tertiary);">${s.data_date || '-'}</td>
</tr>
`;
});
@ -2599,6 +2642,47 @@ function rvRenderRankingTable(scores) {
section.style.display = '';
}
let rvSortButtonsInitialized = false;
function rvSetupSortButtons() {
if (rvSortButtonsInitialized) return;
const controls = document.getElementById('rv-sort-controls');
if (!controls) return;
controls.addEventListener('click', function (e) {
const btn = e.target.closest('.rv-sort-btn');
if (!btn) return;
const field = btn.dataset.sort;
const defaultOrder = btn.dataset.order || 'desc';
// 切换排序方向
if (rvCurrentSortField === field) {
rvCurrentSortOrder = rvCurrentSortOrder === 'desc' ? 'asc' : 'desc';
} else {
rvCurrentSortField = field;
rvCurrentSortOrder = defaultOrder;
}
// 更新按钮状态
controls.querySelectorAll('.rv-sort-btn').forEach(b => {
b.classList.remove('active');
b.textContent = b.textContent.replace(/ [▲▼]$/, '');
});
btn.classList.add('active');
const arrow = rvCurrentSortOrder === 'desc' ? ' ▼' : ' ▲';
btn.textContent = btn.textContent + arrow;
// 重新渲染
if (rvCurrentPlanData) {
rvRenderRankingTable(rvCurrentPlanData.scores);
}
});
rvSortButtonsInitialized = true;
}
function rvRenderDetails(scores) {
const section = document.getElementById('rv-details-section');
const grid = document.getElementById('rv-details-grid');
@ -2692,6 +2776,94 @@ function rvHideAllSections() {
});
}
// ==================== 复盘品种详情弹窗 ====================
function rvShowDetail(symbol, name) {
const scores = rvCurrentPlanData ? rvCurrentPlanData.scores : [];
const plans = rvCurrentPlanData ? rvCurrentPlanData.plans : [];
const s = scores.find(x => x.symbol === symbol);
if (!s) return;
const plan = plans.find(p => p.symbol === symbol);
const modal = document.getElementById('rv-detail-modal');
if (!modal) return;
document.getElementById('rv-modal-title').textContent = `${symbol} ${name || s.name || ''}`;
const body = document.getElementById('rv-modal-body');
// 交易计划部分
let planHtml = '';
if (plan) {
const dirText = plan.direction === 'long' ? '做多' : '做空';
const dirColor = plan.direction === 'long' ? 'var(--color-down)' : 'var(--color-up)';
planHtml = `
<div class="rv-modal-section">
<div class="rv-modal-section-title">交易计划</div>
<div class="rv-modal-plan-box">
<div class="rv-modal-plan-row"><span class="mp-label">方向</span><span class="mp-val" style="color:${dirColor}">${dirText}</span></div>
<div class="rv-modal-plan-row"><span class="mp-label">入场区间</span><span class="mp-val">${rvFormatPrice(plan.entry_low, s.close_price)} ~ ${rvFormatPrice(plan.entry_high, s.close_price)}</span></div>
<div class="rv-modal-plan-row"><span class="mp-label">止损位</span><span class="mp-val stop">${rvFormatPrice(plan.stop_loss, s.close_price)}</span></div>
<div class="rv-modal-plan-row"><span class="mp-label">目标一</span><span class="mp-val target">${rvFormatPrice(plan.target1, s.close_price)}</span></div>
<div class="rv-modal-plan-row"><span class="mp-label">目标二</span><span class="mp-val target">${rvFormatPrice(plan.target2, s.close_price)}</span></div>
<div class="rv-modal-plan-row"><span class="mp-label">触发条件</span><span class="mp-val">${plan.trigger || '-'}</span></div>
</div>
</div>
`;
}
const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)';
body.innerHTML = `
<div class="rv-modal-section">
<div class="rv-modal-section-title">基本信息</div>
<div class="rv-modal-metrics-grid">
<div class="rv-modal-metric"><span class="mm-label">收盘价</span><span class="mm-val">${s.close_price || '-'}${s.data_date ? '<span class="rv-data-date-tag">' + s.data_date + '</span>' : ''}</span></div>
<div class="rv-modal-metric"><span class="mm-label">涨跌幅</span><span class="mm-val" style="color:${changeColor}">${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}%</span></div>
<div class="rv-modal-metric"><span class="mm-label">振幅</span><span class="mm-val">${s.amplitude_pct || 0}%</span></div>
<div class="rv-modal-metric"><span class="mm-label">量比</span><span class="mm-val">${s.volume_ratio || '-'}</span></div>
<div class="rv-modal-metric"><span class="mm-label">综合评分</span><span class="mm-val" style="color:${rvScoreBarColor(s.composite_score)}"><b>${s.composite_score}</b></span></div>
<div class="rv-modal-metric"><span class="mm-label">排名</span><span class="mm-val">#${s.rank || '-'}</span></div>
</div>
</div>
${planHtml}
<div class="rv-modal-section">
<div class="rv-modal-section-title">评分明细</div>
<div class="rv-modal-metrics-grid">
<div class="rv-modal-metric"><span class="mm-label">振幅评分</span><span class="mm-val">${s.amplitude_score || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">量能评分</span><span class="mm-val">${s.volume_score || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">趋势评分</span><span class="mm-val">${s.trend_score || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">活跃度</span><span class="mm-val">${s.activity_score || 0}</span></div>
</div>
</div>
<div class="rv-modal-section">
<div class="rv-modal-section-title">多周期趋势</div>
<div class="rv-modal-metrics-grid">
<div class="rv-modal-metric"><span class="mm-label">60分钟</span><span class="mm-val">${s.trend_60m || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">15分钟</span><span class="mm-val">${s.trend_15m || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">5分钟</span><span class="mm-val">${s.trend_5m || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">方向</span><span class="mm-val">${rvDirectionTag(s.direction_tag)}</span></div>
</div>
</div>
<div class="rv-modal-section">
<div class="rv-modal-section-title">枢轴点位</div>
<div class="rv-modal-pivots">
<span class="rv-pivot-tag resist">R2: ${rvFormatPrice(s.r2, s.close_price)}</span>
<span class="rv-pivot-tag resist">R1: ${rvFormatPrice(s.r1, s.close_price)}</span>
<span class="rv-pivot-tag pivot">P: ${rvFormatPrice(s.pivot, s.close_price)}</span>
<span class="rv-pivot-tag support">S1: ${rvFormatPrice(s.s1, s.close_price)}</span>
<span class="rv-pivot-tag support">S2: ${rvFormatPrice(s.s2, s.close_price)}</span>
</div>
</div>
`;
modal.classList.add('show');
}
function rvCloseDetailModal() {
const modal = document.getElementById('rv-detail-modal');
if (modal) modal.classList.remove('show');
}
// ==================== 自选品种功能 ====================
function renderWatchedList() {

@ -371,6 +371,28 @@ a:hover {
}
/* ===== Section Titles ===== */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid var(--s2);
}
.section-header .section-title {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.section-badge {
font-size: 14px;
margin-right: 6px;
}
.section-title {
font-size: 22px;
font-weight: 800;
@ -1522,4 +1544,218 @@ a:hover {
width: 40px;
height: 40px;
}
.modal-metrics-grid {
grid-template-columns: repeat(2, 1fr);
}
.section-header {
flex-direction: column;
align-items: flex-start;
}
.sort-controls {
width: 100%;
}
}
/* ===== 数据日期标注 ===== */
.data-date-notice {
background: rgba(245, 158, 11, 0.15);
border: 1px solid rgba(245, 158, 11, 0.4);
border-radius: 8px;
padding: 10px 16px;
margin: 10px 0;
color: var(--yw);
font-size: 0.9rem;
text-align: center;
}
.data-date-tag {
display: inline-block;
font-size: 0.7rem;
color: var(--t2);
background: var(--s2);
padding: 1px 6px;
border-radius: 4px;
margin-left: 4px;
vertical-align: middle;
white-space: nowrap;
}
.data-date-cell {
font-size: 0.75rem;
color: var(--t2);
white-space: nowrap;
}
/* ===== 排序控件 ===== */
.sort-controls {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.sort-label {
font-size: 13px;
color: var(--t2);
margin-right: 4px;
}
.sort-btn {
background: var(--s2);
color: var(--t2);
border: 1px solid transparent;
border-radius: 100px;
padding: 5px 14px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all .2s;
white-space: nowrap;
}
.sort-btn:hover {
background: #475569;
color: var(--tx);
}
.sort-btn.active {
background: var(--ac);
color: #fff;
border-color: var(--ac);
}
/* ===== 列表表格(重点关注/规避) ===== */
.list-table-wrap {
overflow-x: auto;
margin-bottom: 20px;
}
.list-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.list-table thead th {
background: var(--s1);
color: var(--t2);
font-weight: 600;
padding: 10px 12px;
text-align: left;
white-space: nowrap;
border-bottom: 2px solid var(--s2);
font-size: 12px;
}
.list-table tbody tr {
border-bottom: 1px solid var(--s2);
transition: background .15s;
}
.list-table tbody tr:hover {
background: rgba(59, 130, 246, 0.06);
}
.list-table tbody td {
padding: 10px 12px;
vertical-align: middle;
}
.yellow-table tbody tr:hover {
background: rgba(245, 158, 11, 0.06);
}
.red-table tbody tr:hover {
background: rgba(239, 68, 68, 0.06);
}
/* ===== 弹窗指标网格 ===== */
.modal-metrics-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.modal-metric {
background: var(--s2);
border-radius: 10px;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.mm-label {
font-size: 11px;
color: var(--t2);
}
.mm-val {
font-size: 15px;
font-weight: 700;
color: var(--tx);
}
/* ===== 弹窗交易计划 ===== */
.modal-plan-box {
background: var(--s2);
border-radius: 12px;
padding: 14px;
display: flex;
flex-direction: column;
gap: 8px;
}
.modal-plan-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.mp-label {
font-size: 12px;
color: var(--t2);
}
.mp-val {
font-size: 14px;
font-weight: 700;
color: var(--tx);
}
.mp-val.stop {
color: var(--rd);
}
.mp-val.target {
color: var(--gn);
}
/* ===== 弹窗枢轴点位 ===== */
.modal-pivots {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* ===== 绿色卡片可点击样式 ===== */
.opp-card {
transition: transform .15s, box-shadow .15s;
}
.opp-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
/* ===== 排名表格行可点击 ===== */
.ranking-table tbody tr {
transition: background .15s;
}
.ranking-table tbody tr:hover {
background: rgba(59, 130, 246, 0.06);
}

@ -90,8 +90,25 @@
<span class="section-badge badge-yellow"></span>
<h2 class="section-title">重点关注品种</h2>
</div>
<div id="yellow-list" class="item-list">
<div class="list-table-wrap">
<table class="list-table yellow-table">
<thead>
<tr>
<th>#</th>
<th>品种</th>
<th>收盘价</th>
<th>涨跌幅</th>
<th>综合评分</th>
<th>振幅%</th>
<th>量比</th>
<th>方向</th>
<th>数据日期</th>
</tr>
</thead>
<tbody id="yellow-list-body">
<!-- 由 JS 动态填充 -->
</tbody>
</table>
</div>
</section>
@ -101,8 +118,25 @@
<span class="section-badge badge-red"></span>
<h2 class="section-title">规避品种</h2>
</div>
<div id="red-list" class="item-list">
<div class="list-table-wrap">
<table class="list-table red-table">
<thead>
<tr>
<th>#</th>
<th>品种</th>
<th>收盘价</th>
<th>涨跌幅</th>
<th>综合评分</th>
<th>振幅%</th>
<th>量比</th>
<th>方向</th>
<th>数据日期</th>
</tr>
</thead>
<tbody id="red-list-body">
<!-- 由 JS 动态填充 -->
</tbody>
</table>
</div>
</section>
@ -110,6 +144,13 @@
<section id="table-section" class="content-section table-section">
<div class="section-header">
<h2 class="section-title">全品种排名</h2>
<div class="sort-controls" id="sort-controls">
<span class="sort-label">排序:</span>
<button class="sort-btn active" data-sort="composite_score" data-order="desc">综合评分</button>
<button class="sort-btn" data-sort="activity_score" data-order="desc">活跃度</button>
<button class="sort-btn" data-sort="volume_ratio" data-order="desc">量比</button>
<button class="sort-btn" data-sort="direction_tag" data-order="asc">方向</button>
</div>
</div>
<div class="table-wrapper">
<table id="ranking-table" class="ranking-table">
@ -119,10 +160,13 @@
<th>品种</th>
<th>最新价</th>
<th>涨跌幅</th>
<th>成交量</th>
<th>持仓量</th>
<th>综合评分</th>
<th>振幅%</th>
<th>量比</th>
<th>趋势</th>
<th>信号</th>
<th>方向</th>
<th>活跃度</th>
<th>数据日期</th>
</tr>
</thead>
<tbody>
@ -166,6 +210,19 @@
</div>
</main>
<!-- 品种详情弹窗 -->
<div id="detail-modal" class="modal-overlay" onclick="if(event.target===this)closeModal()">
<div class="modal">
<div class="modal-header">
<span id="modal-title" class="modal-title">品种详情</span>
<button class="modal-close" onclick="closeModal()"></button>
</div>
<div id="modal-body" class="modal-body">
<!-- 由 JS 动态填充 -->
</div>
</div>
</div>
<script src="/static/review_plan.js"></script>
</body>
</html>

@ -26,6 +26,8 @@ function setupEventListeners() {
document.getElementById('btn-clear').addEventListener('click', function () {
handleClear();
});
setupSortButtons();
}
// ==================== 生成复盘 ====================
@ -191,11 +193,18 @@ function renderHero(meta) {
const el = document.getElementById('hero-section');
if (!el) return;
// 当数据日期与复盘日期不一致时,显示提示
let dataDateNotice = '';
if (meta.actual_data_date && !meta.data_date_matches) {
dataDateNotice = `<div class="data-date-notice">⚠ 当前页面收盘价、涨跌幅为 <b>${meta.actual_data_date}</b>(最近交易日)数据,非 ${meta.review_date} 当日数据</div>`;
}
el.innerHTML = `
<div class="hero-title">
<h1>${meta.review_date} <span class="hero-weekday">${meta.week_day || ''}</span></h1>
</div>
<div class="hero-subtitle">${meta.data_basis || ''}</div>
${dataDateNotice}
<div class="hero-conclusion">${meta.core_conclusion || ''}</div>
`;
showSection('hero-section');
@ -264,8 +273,11 @@ function renderGreenSection(plans, scores) {
const el = document.getElementById('green-section');
if (!el) return;
const grid = document.getElementById('green-grid');
if (!grid) return;
if (!plans || plans.length === 0) {
el.innerHTML = `<div class="section-title green-title">🟢 交易机会(绿色区)</div><div class="empty-hint">暂无交易机会</div>`;
grid.innerHTML = `<div class="empty-hint">暂无交易机会</div>`;
showSection('green-section');
return;
}
@ -277,16 +289,17 @@ function renderGreenSection(plans, scores) {
const s = scoreMap[plan.symbol] || {};
const dirClass = plan.direction === 'long' ? 'dir-long' : 'dir-short';
const dirText = plan.direction === 'long' ? '做多' : '做空';
const dataDateTag = s.data_date ? `<span class="data-date-tag" title="数据日期">${s.data_date}</span>` : '';
return `
<div class="opp-card green-card">
<div class="opp-card green-card" onclick="showDetail('${plan.symbol}','${(plan.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<div class="opp-card-header">
<a href="javascript:void(0)" class="symbol-link" onclick="showDetail('${plan.symbol}','${plan.name || ''}')">${plan.symbol} ${plan.name || ''}</a>
<span class="symbol-link">${plan.symbol} ${plan.name || ''}</span>
<span class="score-badge">${plan.composite_score}</span>
</div>
<div class="opp-card-rows">
<div class="opp-row"><span class="opp-label">方向</span><span class="${dirClass}">${dirText}</span></div>
<div class="opp-row"><span class="opp-label">收盘价</span><span>${formatPrice(s.close_price)}</span></div>
<div class="opp-row"><span class="opp-label">收盘价</span><span>${formatPrice(s.close_price)} ${dataDateTag}</span></div>
<div class="opp-row"><span class="opp-label">涨跌幅</span>${formatChange(s.change_pct)}</div>
<div class="opp-row"><span class="opp-label">量比</span><span>${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}</span></div>
</div>
@ -306,142 +319,164 @@ function renderGreenSection(plans, scores) {
</div>`;
}).join('');
el.innerHTML = `
<div class="section-title green-title">🟢 交易机会绿色区</div>
<div class="opp-grid">${cards}</div>
`;
grid.innerHTML = cards;
showSection('green-section');
}
// ==================== 黄色关注区 ====================
function renderYellowSection(scores) {
const el = document.getElementById('yellow-section');
if (!el) return;
const tbody = document.getElementById('yellow-list-body');
if (!tbody) return;
const yellowItems = (scores || []).filter(s => s.category === 'yellow');
if (yellowItems.length === 0) {
el.innerHTML = `<div class="section-title yellow-title">🟡 关注列表(黄色区)</div><div class="empty-hint">暂无关注品种</div>`;
tbody.innerHTML = `<tr><td colspan="9" class="empty-hint">暂无关注品种</td></tr>`;
showSection('yellow-section');
return;
}
const items = yellowItems.map(s => `
<div class="watch-item yellow-item">
<div class="watch-left">
<a href="javascript:void(0)" class="symbol-link" onclick="showDetail('${s.symbol}','${s.name || ''}')">${s.symbol} ${s.name || ''}</a>
${directionTag(s.direction_tag)}
</div>
<div class="watch-mid">
${scoreBar(s.composite_score)}
</div>
<div class="watch-right">
<span class="watch-score">${s.composite_score}</span>
${formatChange(s.change_pct)}
</div>
</div>
tbody.innerHTML = yellowItems.map((s, i) => `
<tr onclick="showDetail('${s.symbol}','${(s.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<td>${i + 1}</td>
<td><span class="symbol-link">${s.symbol} ${s.name || ''}</span></td>
<td>${formatPrice(s.close_price)}</td>
<td>${formatChange(s.change_pct)}</td>
<td><div class="table-score-bar-wrap">${scoreBar(s.composite_score)}<span class="table-score-num">${s.composite_score}</span></div></td>
<td>${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'}</td>
<td>${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}</td>
<td>${directionTag(s.direction_tag)}</td>
<td class="data-date-cell">${s.data_date || '-'}</td>
</tr>
`).join('');
el.innerHTML = `
<div class="section-title yellow-title">🟡 关注列表黄色区</div>
<div class="watch-list">${items}</div>
`;
showSection('yellow-section');
}
// ==================== 红色风险区 ====================
function renderRedSection(scores) {
const el = document.getElementById('red-section');
if (!el) return;
const tbody = document.getElementById('red-list-body');
if (!tbody) return;
const redItems = (scores || []).filter(s => s.category === 'red');
if (redItems.length === 0) {
el.innerHTML = `<div class="section-title red-title">🔴 规避列表(红色区)</div><div class="empty-hint">暂无规避品种</div>`;
tbody.innerHTML = `<tr><td colspan="9" class="empty-hint">暂无规避品种</td></tr>`;
showSection('red-section');
return;
}
const items = redItems.map(s => `
<div class="watch-item red-item">
<div class="watch-left">
<a href="javascript:void(0)" class="symbol-link" onclick="showDetail('${s.symbol}','${s.name || ''}')">${s.symbol} ${s.name || ''}</a>
${directionTag(s.direction_tag)}
</div>
<div class="watch-mid">
${scoreBar(s.composite_score)}
</div>
<div class="watch-right">
<span class="watch-score">${s.composite_score}</span>
${formatChange(s.change_pct)}
</div>
</div>
tbody.innerHTML = redItems.map((s, i) => `
<tr onclick="showDetail('${s.symbol}','${(s.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<td>${i + 1}</td>
<td><span class="symbol-link">${s.symbol} ${s.name || ''}</span></td>
<td>${formatPrice(s.close_price)}</td>
<td>${formatChange(s.change_pct)}</td>
<td><div class="table-score-bar-wrap">${scoreBar(s.composite_score)}<span class="table-score-num">${s.composite_score}</span></div></td>
<td>${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'}</td>
<td>${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}</td>
<td>${directionTag(s.direction_tag)}</td>
<td class="data-date-cell">${s.data_date || '-'}</td>
</tr>
`).join('');
el.innerHTML = `
<div class="section-title red-title">🔴 规避列表红色区</div>
<div class="watch-list">${items}</div>
`;
showSection('red-section');
}
// ==================== 排名表格 ====================
// ==================== 排名表格(支持多列排序) ====================
let currentSortField = 'composite_score';
let currentSortOrder = 'desc';
function renderRankingTable(scores) {
const el = document.getElementById('ranking-table');
if (!el) return;
const table = document.getElementById('ranking-table');
if (!table) return;
const tbody = table.querySelector('tbody');
if (!tbody) return;
if (!scores || scores.length === 0) {
el.innerHTML = `<div class="section-title">📊 综合排名</div><div class="empty-hint">暂无排名数据</div>`;
showSection('ranking-table');
tbody.innerHTML = `<tr><td colspan="11" class="empty-hint">暂无排名数据</td></tr>`;
showSection('table-section');
return;
}
const rows = scores.map((s, i) => {
const rank = s.rank || (i + 1);
// 排序
const sorted = [...scores].sort((a, b) => {
let va = a[currentSortField];
let vb = b[currentSortField];
// 方向排序特殊处理
if (currentSortField === 'direction_tag') {
const dirOrder = { '强多': 1, '偏多': 2, '多头共振': 1, '震荡': 3, '偏空': 4, '强空': 5, '空头共振': 5 };
va = dirOrder[va] || 3;
vb = dirOrder[vb] || 3;
}
if (va == null) va = -Infinity;
if (vb == null) vb = -Infinity;
if (currentSortOrder === 'asc') return va > vb ? 1 : va < vb ? -1 : 0;
return va < vb ? 1 : va > vb ? -1 : 0;
});
tbody.innerHTML = sorted.map((s, i) => {
const rank = i + 1;
const rankClass = rank <= 3 ? 'rank-top' : '';
return `
<tr>
<tr onclick="showDetail('${s.symbol}','${(s.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<td class="${rankClass}">${rank}</td>
<td><a href="javascript:void(0)" class="symbol-link" onclick="showDetail('${s.symbol}','${s.name || ''}')">${s.symbol}</a></td>
<td>${s.name || ''}</td>
<td><span class="symbol-link">${s.symbol} ${s.name || ''}</span></td>
<td>${formatPrice(s.close_price)}</td>
<td>${formatChange(s.change_pct)}</td>
<td><div class="table-score-bar-wrap">${scoreBar(s.composite_score)}<span class="table-score-num">${s.composite_score}</span></div></td>
<td>${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'}</td>
<td>${formatChange(s.change_pct)}</td>
<td>${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}</td>
<td>${s.trend_score != null ? s.trend_score : '-'}</td>
<td>${directionTag(s.direction_tag)}</td>
<td>${s.activity_score != null ? s.activity_score : '-'}</td>
<td class="data-date-cell">${s.data_date || '-'}</td>
</tr>`;
}).join('');
el.innerHTML = `
<div class="section-title">📊 综合排名</div>
<div class="table-wrap">
<table class="ranking-tbl">
<thead>
<tr>
<th>#</th>
<th>代码</th>
<th>名称</th>
<th>收盘价</th>
<th>综合评分</th>
<th>振幅%</th>
<th>涨跌幅</th>
<th>量比</th>
<th>趋势</th>
<th>方向</th>
<th>活跃度</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`;
showSection('ranking-table');
showSection('table-section');
}
function setupSortButtons() {
const controls = document.getElementById('sort-controls');
if (!controls) return;
controls.addEventListener('click', function (e) {
const btn = e.target.closest('.sort-btn');
if (!btn) return;
const field = btn.dataset.sort;
const defaultOrder = btn.dataset.order || 'desc';
// 切换排序方向
if (currentSortField === field) {
currentSortOrder = currentSortOrder === 'desc' ? 'asc' : 'desc';
} else {
currentSortField = field;
currentSortOrder = defaultOrder;
}
// 更新按钮状态
controls.querySelectorAll('.sort-btn').forEach(b => {
b.classList.remove('active');
b.textContent = b.textContent.replace(/ [▲▼]$/, '');
});
btn.classList.add('active');
const arrow = currentSortOrder === 'desc' ? ' ▼' : ' ▲';
btn.textContent = btn.textContent + arrow;
// 重新渲染
if (currentPlanData) {
renderRankingTable(currentPlanData.scores);
}
});
}
// ==================== 品种详情卡片 ====================
@ -471,7 +506,7 @@ function renderDetails(scores) {
<span class="detail-score">${s.composite_score}</span>
</div>
<div class="detail-metrics">
<div class="detail-metric"><span class="dm-label">收盘价</span><span class="dm-val">${formatPrice(s.close_price)}</span></div>
<div class="detail-metric"><span class="dm-label">收盘价</span><span class="dm-val">${formatPrice(s.close_price)}${s.data_date ? ' <span class="data-date-tag" title="">' + s.data_date + '</span>' : ''}</span></div>
<div class="detail-metric"><span class="dm-label">振幅</span><span class="dm-val">${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'}</span></div>
<div class="detail-metric"><span class="dm-label">涨跌幅</span><span class="dm-val">${formatChange(s.change_pct)}</span></div>
<div class="detail-metric"><span class="dm-label">量比</span><span class="dm-val">${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}</span></div>
@ -543,20 +578,51 @@ function renderSectors(sectors) {
function showDetail(symbol, name) {
const scores = currentPlanData ? currentPlanData.scores : [];
const plans = currentPlanData ? currentPlanData.plans : [];
const s = scores.find(x => x.symbol === symbol);
if (!s) return;
// 查找对应的交易计划
const plan = plans.find(p => p.symbol === symbol);
const modal = document.getElementById('detail-modal');
if (!modal) return;
document.getElementById('modal-title').textContent = `${symbol} ${name || s.name || ''}`;
const body = document.getElementById('modal-body');
// 交易计划部分(仅绿色品种有)
let planHtml = '';
if (plan) {
const dirClass = plan.direction === 'long' ? 'dir-long' : 'dir-short';
const dirText = plan.direction === 'long' ? '做多' : '做空';
planHtml = `
<div class="modal-section">
<div class="modal-section-title">交易计划</div>
<div class="modal-plan-box">
<div class="modal-plan-row"><span class="mp-label">方向</span><span class="${dirClass} mp-val">${dirText}</span></div>
<div class="modal-plan-row"><span class="mp-label">入场区间</span><span class="mp-val">${formatPrice(plan.entry_low)} - ${formatPrice(plan.entry_high)}</span></div>
<div class="modal-plan-row"><span class="mp-label">止损</span><span class="mp-val stop">${formatPrice(plan.stop_loss)}</span></div>
<div class="modal-plan-row"><span class="mp-label">目标1</span><span class="mp-val target">${formatPrice(plan.target1)}</span></div>
<div class="modal-plan-row"><span class="mp-label">目标2</span><span class="mp-val target">${formatPrice(plan.target2)}</span></div>
<div class="modal-plan-row"><span class="mp-label">触发条件</span><span class="mp-val">${plan.trigger || '-'}</span></div>
</div>
</div>`;
}
const pivotTags = [];
if (s.pivot != null) pivotTags.push(`<span class="pivot-tag pivot">Pivot ${formatPrice(s.pivot)}</span>`);
if (s.r1 != null) pivotTags.push(`<span class="pivot-tag r1">R1 ${formatPrice(s.r1)}</span>`);
if (s.r2 != null) pivotTags.push(`<span class="pivot-tag r2">R2 ${formatPrice(s.r2)}</span>`);
if (s.s1 != null) pivotTags.push(`<span class="pivot-tag s1">S1 ${formatPrice(s.s1)}</span>`);
if (s.s2 != null) pivotTags.push(`<span class="pivot-tag s2">S2 ${formatPrice(s.s2)}</span>`);
body.innerHTML = `
<div class="modal-section">
<div class="modal-section-title">基本信息</div>
<div class="modal-metrics-grid">
<div class="modal-metric"><span class="mm-label">收盘价</span><span class="mm-val">${formatPrice(s.close_price)}</span></div>
<div class="modal-metric"><span class="mm-label">收盘价</span><span class="mm-val">${formatPrice(s.close_price)}${s.data_date ? ' <span class="data-date-tag">' + s.data_date + '</span>' : ''}</span></div>
<div class="modal-metric"><span class="mm-label">振幅</span><span class="mm-val">${s.amplitude_pct != null ? s.amplitude_pct.toFixed(2) + '%' : '-'}</span></div>
<div class="modal-metric"><span class="mm-label">涨跌幅</span><span class="mm-val">${formatChange(s.change_pct)}</span></div>
<div class="modal-metric"><span class="mm-label">量比</span><span class="mm-val">${s.volume_ratio != null ? s.volume_ratio.toFixed(2) : '-'}</span></div>
@ -564,6 +630,7 @@ function showDetail(symbol, name) {
<div class="modal-metric"><span class="mm-label">排名</span><span class="mm-val">#${s.rank || '-'}</span></div>
</div>
</div>
${planHtml}
<div class="modal-section">
<div class="modal-section-title">评分明细</div>
<div class="modal-metrics-grid">
@ -584,13 +651,7 @@ function showDetail(symbol, name) {
</div>
<div class="modal-section">
<div class="modal-section-title">枢轴点位</div>
<div class="modal-pivots">
${s.r2 != null ? `<span class="pivot-tag r2">R2 ${formatPrice(s.r2)}</span>` : ''}
${s.r1 != null ? `<span class="pivot-tag r1">R1 ${formatPrice(s.r1)}</span>` : ''}
${s.pivot != null ? `<span class="pivot-tag pivot">Pivot ${formatPrice(s.pivot)}</span>` : ''}
${s.s1 != null ? `<span class="pivot-tag s1">S1 ${formatPrice(s.s1)}</span>` : ''}
${s.s2 != null ? `<span class="pivot-tag s2">S2 ${formatPrice(s.s2)}</span>` : ''}
</div>
<div class="modal-pivots">${pivotTags.join('')}</div>
</div>
`;
@ -657,7 +718,7 @@ function hideAllSections() {
const ids = [
'hero-section', 'stats-grid', 'risk-banner',
'green-section', 'yellow-section', 'red-section',
'ranking-table', 'details-section', 'sectors-section',
'table-section', 'details-section', 'sectors-section',
'summary-content'
];
ids.forEach(id => hideSection(id));

Binary file not shown.

Binary file not shown.
Loading…
Cancel
Save