diff --git a/app/__pycache__/main.cpython-311.pyc b/app/__pycache__/main.cpython-311.pyc index deb0419..1969ebb 100644 Binary files a/app/__pycache__/main.cpython-311.pyc and b/app/__pycache__/main.cpython-311.pyc differ diff --git a/app/analysis_db.py b/app/analysis_db.py index 8593935..26f98ca 100644 --- a/app/analysis_db.py +++ b/app/analysis_db.py @@ -36,5 +36,5 @@ def init_analysis_db(): # 确保导入所有模型类,使其注册到 AnalysisBase from app import analysis_models # 直接导入 analysis_models 模块中的所有类 - from app.analysis_models import FuturesAnalysis, WatchedSymbol, AIModelConfig, AnalysisSettings, AIAnalysisCache + from app.analysis_models import FuturesAnalysis, WatchedSymbol, AIModelConfig, AnalysisSettings, AIAnalysisCache, ReviewDate, SymbolRanking, TradingPlan AnalysisBase.metadata.create_all(bind=analysis_engine) diff --git a/app/analysis_models.py b/app/analysis_models.py index 9557f0b..ca1a651 100644 --- a/app/analysis_models.py +++ b/app/analysis_models.py @@ -100,3 +100,57 @@ class AIAnalysisCache(AnalysisBase): def __repr__(self): return f"" + + +class ReviewDate(AnalysisBase): + """复盘日期表""" + __tablename__ = "review_dates" + + id = Column(Integer, primary_key=True, autoincrement=True) + review_date = Column(String(16), nullable=False, unique=True, index=True, comment="复盘日期 YYYY-MM-DD") + week_day = Column(String(8), nullable=True, comment="星期") + created_at = Column(DateTime, nullable=False, default=datetime.now) + + def __repr__(self): + return f"" + + +class SymbolRanking(AnalysisBase): + """品种排名表""" + __tablename__ = "symbol_rankings" + + id = Column(Integer, primary_key=True, autoincrement=True) + review_date_id = Column(Integer, nullable=False, index=True, comment="关联复盘日期ID") + symbol = Column(String(32), nullable=False, comment="品种合约代码") + name = Column(String(64), nullable=True, comment="品种名称") + rank_type = Column(String(32), nullable=False, comment="排名类型: volume/amplitude/change/open_interest") + rank = Column(Integer, nullable=False, comment="排名") + value = Column(String(64), nullable=True, comment="数值(如成交量、振幅等)") + price = Column(String(32), nullable=True, comment="价格") + change_pct = Column(String(16), nullable=True, comment="涨跌幅") + + def __repr__(self): + return f"" + + +class TradingPlan(AnalysisBase): + """交易计划表""" + __tablename__ = "trading_plans" + + id = Column(Integer, primary_key=True, autoincrement=True) + review_date_id = Column(Integer, nullable=False, index=True, comment="关联复盘日期ID") + symbol = Column(String(32), nullable=False, comment="品种合约代码") + name = Column(String(64), nullable=True, comment="品种名称") + plan_type = Column(String(16), nullable=False, comment="计划类型: long/short") + score = Column(Integer, nullable=False, comment="评分 0-100") + logic = Column(Text, nullable=True, comment="多空逻辑") + reason = Column(Text, nullable=True, comment="入选理由") + entry_price = Column(String(64), nullable=True, comment="入场价位") + stop_loss = Column(String(32), nullable=True, comment="止损价位") + take_profit = Column(String(64), nullable=True, comment="止盈价位") + confidence = Column(String(16), nullable=True, comment="置信度: 高/中高/中/低") + position_suggestion = Column(String(32), nullable=True, comment="仓位建议") + created_at = Column(DateTime, nullable=False, default=datetime.now) + + def __repr__(self): + return f"" diff --git a/app/api/__pycache__/data.cpython-311.pyc b/app/api/__pycache__/data.cpython-311.pyc index f68cc5f..a1badf7 100644 Binary files a/app/api/__pycache__/data.cpython-311.pyc and b/app/api/__pycache__/data.cpython-311.pyc differ diff --git a/app/api/data.py b/app/api/data.py index 5d36ea1..e02a212 100644 --- a/app/api/data.py +++ b/app/api/data.py @@ -3,6 +3,7 @@ """ import logging from typing import Optional +from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, UploadFile, File from sqlalchemy.orm import Session @@ -222,3 +223,26 @@ def cache_status(symbol: str, db: Session = Depends(get_db)): "cached_periods": periods_info, "status": "ok", } + + +@router.get("/latest-timestamps") +def get_latest_timestamps(db: Session = Depends(get_db)): + """ + 获取所有品种的最新数据时间戳。 + """ + from app.models import SymbolTimestamp + + timestamps = db.query(SymbolTimestamp).all() + + data = [] + for ts in timestamps: + data.append({ + "symbol": ts.symbol, + "data_type": ts.data_type, + "last_refresh_at": ts.last_refresh_at.isoformat() if ts.last_refresh_at else None + }) + + return { + "success": True, + "data": data + } diff --git a/app/main.py b/app/main.py index 28947b2..abf9d9b 100644 --- a/app/main.py +++ b/app/main.py @@ -13,7 +13,7 @@ from fastapi.responses import FileResponse, RedirectResponse from app.database import engine, Base from app.user_models import Base as UserBase from app.config import HOST, PORT, LOG_LEVEL -from app.api import data, tasks, config, futures_analysis, ai_config, auth +from app.api import data, tasks, config, futures_analysis, ai_config, auth, review_plan from app.services.scheduler import start_scheduler, stop_scheduler # 配置日志 @@ -120,6 +120,7 @@ app.include_router(tasks.router, prefix="/api/v1") app.include_router(config.router, prefix="/api/v1") app.include_router(futures_analysis.router, prefix="/api/v1") app.include_router(ai_config.router, prefix="/api/v1") +app.include_router(review_plan.router, prefix="/api/v1") @app.get("/futures-analysis") @@ -134,6 +135,12 @@ def ai_config_page(): return FileResponse(str(STATIC_DIR / "ai_config.html")) +@app.get("/review-plan") +def review_plan_page(): + """复盘计划页面""" + return FileResponse(str(STATIC_DIR / "review_plan.html")) + + @app.get("/api/v1/health") def health(): return {"status": "ok", "service": "market-data-buffer"} diff --git a/app/static/futures_analysis.html b/app/static/futures_analysis.html index acabc4a..2a13940 100644 --- a/app/static/futures_analysis.html +++ b/app/static/futures_analysis.html @@ -182,6 +182,13 @@ /* 详情视图样式 */ .view { display: none; } .view.active { display: block; } + #review-view.active { display: block; } + #watched-view.active { display: block; } + + .watched-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; padding: 20px 24px; background: #fff; border-radius: 20px; box-shadow: var(--shadow-sm); } + .watched-header h2 { font-size: 20px; font-weight: 700; } + .watched-count { font-size: 14px; color: var(--text-secondary); background: #F5F5F7; padding: 6px 14px; border-radius: 8px; font-weight: 500; } + .empty-hint { font-size: 13px; color: var(--text-tertiary); margin-top: 8px; } .detail-actions { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; } .back-btn, .refresh-btn { @@ -328,6 +335,58 @@ .header { padding: 0 16px; } .container { padding: 16px; } } + + /* 复盘计划样式 */ + .review-toolbar { display: flex; gap: 16px; margin-bottom: 24px; align-items: center; justify-content: center; } + .date-selector { background: #fff; border: 1px solid rgba(0,0,0,0.05); border-radius: 12px; padding: 0 16px; height: 44px; font-size: 14px; color: var(--text-primary); box-shadow: var(--shadow-sm); cursor: pointer; min-width: 180px; font-family: inherit; outline: none; } + .date-selector:hover, .date-selector:focus { border-color: var(--color-brand); } + .summary-header { background: #fff; border-radius: 20px; padding: 20px 24px; box-shadow: var(--shadow-sm); margin-bottom: 24px; display: flex; justify-content: space-between; align-items: center; } + .summary-header h2 { font-size: 20px; font-weight: 700; } + .current-date { font-size: 14px; color: var(--text-secondary); background: #F5F5F7; padding: 6px 14px; border-radius: 8px; font-weight: 500; } + .summary-layout { display: grid; grid-template-columns: 1fr 360px; gap: 20px; } + .summary-left { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } + .rank-section { background: #fff; border-radius: 16px; padding: 16px; box-shadow: var(--shadow-sm); } + .rank-title { font-size: 13px; font-weight: 600; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid rgba(0,0,0,0.05); } + .rank-list { display: flex; flex-direction: column; gap: 6px; } + .rank-row { display: flex; justify-content: space-between; align-items: center; padding: 7px 0; border-bottom: 1px dashed rgba(0,0,0,0.04); cursor: pointer; } + .rank-row:last-child { border-bottom: none; } + .rank-rank { width: 20px; font-size: 12px; font-weight: 700; color: var(--text-tertiary); text-align: center; } + .rank-rank.top { color: var(--color-brand); } + .rank-name { flex: 1; margin-left: 8px; font-size: 13px; font-weight: 500; } + .rank-val { font-size: 13px; font-weight: 600; } + .rank-val.up { color: var(--color-up); } + .rank-val.down { color: var(--color-down); } + .summary-right { display: flex; flex-direction: column; gap: 16px; } + .plan-header { font-size: 15px; font-weight: 700; color: var(--color-ai); } + .plan-list { display: flex; flex-direction: column; gap: 6px; } + .plan-list-item { background: #fff; border-radius: 12px; padding: 12px 16px; border: 1px solid rgba(0,0,0,0.05); cursor: pointer; display: flex; justify-content: space-between; align-items: center; } + .plan-list-item:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); } + .plan-list-item.expanded { border-color: var(--color-ai); box-shadow: var(--shadow-md); } + .plan-list-left { display: flex; align-items: center; gap: 10px; } + .plan-list-code { font-size: 14px; font-weight: 700; } + .plan-badge { font-size: 10px; padding: 4px 12px; border-radius: 9999px; font-weight: 700; } + .plan-badge.long { background: rgba(52,199,89,0.15); color: var(--color-down); } + .plan-badge.short { background: rgba(255,59,48,0.15); color: var(--color-up); } + .plan-list-score { font-size: 12px; font-weight: 600; color: var(--color-ai); background: #F8F5FF; padding: 2px 8px; border-radius: 6px; } + .plan-list-arrow { font-size: 12px; color: var(--text-tertiary); transition: transform .2s; } + .plan-list-item.expanded .plan-list-arrow { transform: rotate(180deg); } + .plan-detail { max-height: 0; overflow: hidden; transition: max-height .35s ease; } + .plan-detail.open { max-height: 600px; } + .plan-card { background: #fff; border-radius: 16px; padding: 20px; box-shadow: var(--shadow-md); margin-bottom: 16px; border: 1px solid rgba(0,0,0,0.05); position: relative; } + .plan-card::before { content: "AI"; position: absolute; top: -9px; left: 16px; background: var(--color-ai); color: #fff; font-size: 10px; padding: 2px 8px; border-radius: 8px; font-weight: 600; } + .plan-ai { background: #F8F5FF; border-radius: 12px; padding: 12px; margin-bottom: 12px; font-size: 12px; line-height: 1.5; color: var(--text-secondary); } + .plan-ai strong { color: var(--color-ai); } + .plan-targets { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-bottom: 12px; } + .plan-target { background: #F5F5F7; border-radius: 10px; padding: 10px 8px; text-align: center; } + .plan-target-label { font-size: 10px; color: var(--text-tertiary); margin-bottom: 4px; } + .plan-target-val { font-size: 14px; font-weight: 700; } + .plan-target-val.green { color: var(--color-down); } + .plan-target-val.red { color: var(--color-up); } + .plan-target-val.neutral { color: var(--text-primary); } + .plan-note { font-size: 10px; color: var(--text-tertiary); text-align: center; margin-top: 8px; } + .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 400px; gap: 16px; } + .empty-icon { font-size: 64px; opacity: 0.3; } + .empty-text { font-size: 16px; color: var(--text-tertiary); } @@ -339,6 +398,7 @@ 自选 市场概览 风险预警 + 复盘计划
LIVE @@ -370,7 +430,6 @@
- @@ -545,6 +604,75 @@
+ + +
+
+

我的自选

+
0 个品种
+
+
+ +
+ +
+ + +
+
+ + +
+ + + +
+
📊
+
请选择日期或点击"复盘与计划"按钮
+
+
diff --git a/app/static/futures_analysis.js b/app/static/futures_analysis.js index 31c6fc9..3aaef2a 100644 --- a/app/static/futures_analysis.js +++ b/app/static/futures_analysis.js @@ -124,6 +124,25 @@ function initEventListeners() { } }); }); + + // 导航项点击事件 + document.querySelectorAll('.nav-item[data-page]').forEach(navItem => { + navItem.addEventListener('click', function(e) { + e.preventDefault(); + const page = this.dataset.page; + + document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active')); + this.classList.add('active'); + + if (page === 'review') { + showReviewView(); + } else if (page === 'watched') { + showWatchedView(); + } else { + showListView(); + } + }); + }); } function closeModal(modalId) { @@ -133,10 +152,38 @@ function closeModal(modalId) { function showListView() { document.getElementById('list-view').classList.add('active'); document.getElementById('detail-view').classList.remove('active'); + document.getElementById('review-view').classList.remove('active'); + document.getElementById('watched-view').classList.remove('active'); + if (klineChart) { + klineChart.dispose(); + klineChart = null; + } +} + +function showWatchedView() { + document.getElementById('list-view').classList.remove('active'); + document.getElementById('detail-view').classList.remove('active'); + document.getElementById('review-view').classList.remove('active'); + document.getElementById('watched-view').classList.add('active'); if (klineChart) { klineChart.dispose(); klineChart = null; } + + renderWatchedList(); +} + +function showReviewView() { + document.getElementById('list-view').classList.remove('active'); + document.getElementById('detail-view').classList.remove('active'); + document.getElementById('review-view').classList.add('active'); + if (klineChart) { + klineChart.dispose(); + klineChart = null; + } + + // 初始化复盘计划功能 + initReviewPlan(); } async function showDetailView(symbol) { @@ -231,7 +278,6 @@ async function loadWatchedSymbols() { const data = await response.json(); if (data.success) { watchedSymbols = data.data.map(s => s.symbol); - document.getElementById('count-watched').textContent = watchedSymbols.length; } } catch (error) { console.error('加载自选列表失败:', error); @@ -263,14 +309,13 @@ async function toggleWatch(symbol, name, event) { showToast('success', '已添加自选', `${name}(${symbol}) 已添加到自选列表`); } } - document.getElementById('count-watched').textContent = watchedSymbols.length; // 重新渲染当前视图 const activePill = document.querySelector('.pill.active'); const category = activePill ? activePill.dataset.category : 'all'; - if (category === 'watched') { - filterByCategory('watched'); - } else { + + // 如果当前视图不是自选页面,重新渲染主网格 + if (category !== 'watched') { renderFuturesGrid(getCurrentFilteredData()); } } catch (error) { @@ -283,8 +328,13 @@ function toggleFav(symbol, name, event) { event.stopPropagation(); // 调用原有的toggleWatch函数处理API请求和数据更新 - // toggleWatch 会重新渲染网格,星标状态会根据 watchedSymbols 自动更新 toggleWatch(symbol, name, event); + + // 如果当前在自选页面,刷新自选列表 + const watchedView = document.getElementById('watched-view'); + if (watchedView && watchedView.classList.contains('active')) { + setTimeout(() => renderWatchedList(), 200); + } } function getCurrentFilteredData() { @@ -295,9 +345,6 @@ function getCurrentFilteredData() { function filterDataByCategory(data, category) { if (category === 'all') return data; - if (category === 'watched') { - return data.filter(item => watchedSymbols.includes(item.symbol)); - } const categoryMap = { 'energy': ['SC', 'FU', 'LU', 'BU', 'RU', 'NR'], 'metal': ['AU', 'AG', 'CU', 'AL', 'ZN', 'NI', 'SN', 'PB', 'SS', 'RB', 'HC', 'I', 'J', 'JM', 'AO', 'SI', 'LC', 'PS'], @@ -1728,8 +1775,16 @@ async function runAIAnalysis(forceRefresh = false) { `; + // 设置5分钟超时 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 300000); // 5分钟 = 300000毫秒 + try { - const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}?force_refresh=${forceRefresh}`); + const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}?force_refresh=${forceRefresh}`, { + signal: controller.signal + }); + clearTimeout(timeoutId); + const data = await response.json(); if (data.success) { @@ -1745,13 +1800,24 @@ async function runAIAnalysis(forceRefresh = false) { showToast('error', '分析失败', data.error || 'AI分析失败'); } } catch (error) { + clearTimeout(timeoutId); console.error('AI分析请求失败:', error); - content.innerHTML = ` -
-

网络错误,请检查网络连接

-
- `; - showToast('error', '请求失败', '网络错误,请稍后重试'); + + if (error.name === 'AbortError') { + content.innerHTML = ` +
+

分析超时,请稍后重试或联系管理员

+
+ `; + showToast('error', '请求超时', 'AI分析耗时过长,请稍后重试'); + } else { + content.innerHTML = ` +
+

网络错误,请检查网络连接

+
+ `; + showToast('error', '请求失败', '网络错误,请稍后重试'); + } } finally { btn.disabled = false; btn.textContent = '智能分析'; @@ -2122,3 +2188,470 @@ function showAIDetailModal() { document.getElementById('ai-analysis-modal').classList.add('active'); } + +// ==================== 复盘计划功能 ==================== + +const REVIEW_API_BASE = '/api/v1/review'; +const DATA_API_BASE = '/api/v1/data'; +let currentReviewDateId = null; + +function initReviewPlan() { + const reviewDateSelector = document.getElementById('review-date-selector'); + const btnReviewPlan = document.getElementById('btn-review-plan'); + + if (reviewDateSelector) { + reviewDateSelector.addEventListener('change', function(e) { + const selectedDate = e.target.value; + if (selectedDate) { + loadReviewByDate(selectedDate); + } else { + hideReviewSummary(); + } + }); + } + + if (btnReviewPlan) { + btnReviewPlan.addEventListener('click', function() { + handleReviewAndPlan(); + }); + } + + loadReviewDates(); +} + +function handleReviewAndPlan() { + const now = new Date(); + const hour = now.getHours(); + const dayOfWeek = now.getDay(); + + const isTradingTime = checkIsTradingTime(dayOfWeek, hour); + + if (isTradingTime) { + alert('当前时间在交易时间内,请在每日收盘后进行复盘与计划。\n可复盘与计划的时间为:\n1. 非交易日(周六、周日)\n2. 交易日的 0:00 - 9:00\n3. 交易日的 15:00 - 21:00'); + return; + } + + checkDataConsistency(); +} + +function checkIsTradingTime(dayOfWeek, hour) { + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + + if (isWeekend) { + return false; + } + + // 交易日 9:00 之前可以复盘 + if (hour < 9) { + return false; + } + + // 交易日 15:00 - 21:00 可以复盘 + if (hour >= 15 && hour < 21) { + return false; + } + + return true; +} + +function checkDataConsistency() { + const today = new Date(); + const todayStr = today.toISOString().split('T')[0]; + + fetch(`${DATA_API_BASE}/latest-timestamps`) + .then(res => res.json()) + .then(data => { + if (!data.success || !data.data || data.data.length === 0) { + alert('交易数据库中没有数据,请先进行数据更新'); + return; + } + + let latestDate = null; + data.data.forEach(item => { + if (item.last_refresh_at) { + const dateStr = item.last_refresh_at.split('T')[0]; + if (!latestDate || dateStr > latestDate) { + latestDate = dateStr; + } + } + }); + + if (!latestDate) { + alert('交易数据库中没有数据,请先进行数据更新'); + return; + } + + if (latestDate < todayStr) { + alert(`交易数据库中的最新数据日期为 ${latestDate},与当前日期 ${todayStr} 不一致。\n请先进行数据更新,确保数据是最新的。`); + return; + } + + loadOrCreateReviewPlan(todayStr); + }) + .catch(err => { + console.error('检查数据一致性失败:', err); + alert('无法获取交易数据状态,请稍后重试'); + }); +} + +function loadOrCreateReviewPlan(todayStr) { + fetch(`${REVIEW_API_BASE}/dates?limit=1`) + .then(res => res.json()) + .then(data => { + if (data.success && data.data.length > 0) { + const latestReview = data.data[0]; + if (latestReview.review_date === todayStr) { + currentReviewDateId = latestReview.id; + loadReviewSummary(currentReviewDateId); + } else { + createReviewPlan(todayStr); + } + } else { + createReviewPlan(todayStr); + } + }) + .catch(err => { + console.error('加载复盘日期失败:', err); + createReviewPlan(todayStr); + }); +} + +function createReviewPlan(todayStr) { + const weekDays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]; + const now = new Date(); + const weekDay = weekDays[now.getDay() - 1] || ""; + + alert('开始执行复盘与交易计划生成...'); + + fetch(`${REVIEW_API_BASE}/plans/batch`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + review_date: todayStr, + week_day: weekDay, + rankings: [], + plans: [] + }) + }) + .then(res => res.json()) + .then(data => { + if (data.success) { + currentReviewDateId = data.data.review_date_id; + loadReviewDates(); + loadReviewSummary(currentReviewDateId); + alert('复盘与计划创建成功'); + } else { + alert('创建复盘计划失败'); + } + }) + .catch(err => { + console.error('创建复盘计划失败:', err); + alert('创建失败,请稍后重试'); + }); +} + +function loadReviewDates() { + fetch(`${REVIEW_API_BASE}/dates?limit=20`) + .then(res => res.json()) + .then(data => { + if (data.success && data.data.length > 0) { + const lastDate = data.data[0]; + document.getElementById('review-date-selector').value = lastDate.review_date; + currentReviewDateId = lastDate.id; + loadReviewSummary(lastDate.id); + } + }) + .catch(err => { + console.error('加载复盘日期失败:', err); + }); +} + +function loadReviewByDate(selectedDate) { + fetch(`${REVIEW_API_BASE}/dates?limit=100`) + .then(res => res.json()) + .then(data => { + if (data.success && data.data.length > 0) { + const matchedReview = data.data.find(d => d.review_date === selectedDate); + if (matchedReview) { + currentReviewDateId = matchedReview.id; + loadReviewSummary(currentReviewDateId); + } else { + hideReviewSummary(); + document.getElementById('review-summary-content').style.display = 'block'; + document.getElementById('review-empty-state').style.display = 'none'; + document.getElementById('review-rank-volume').innerHTML = ''; + document.getElementById('review-rank-amplitude').innerHTML = ''; + document.getElementById('review-rank-change').innerHTML = ''; + document.getElementById('review-rank-oi').innerHTML = ''; + document.getElementById('review-plan-list').innerHTML = '
该日期暂无复盘数据
'; + } + } else { + hideReviewSummary(); + } + }) + .catch(err => { + console.error('加载复盘日期失败:', err); + }); +} + +function loadReviewSummary(reviewDateId) { + fetch(`${REVIEW_API_BASE}/summary/${reviewDateId}`) + .then(res => res.json()) + .then(data => { + if (data.success) { + renderReviewSummary(data.data); + } else { + alert('加载复盘数据失败'); + } + }) + .catch(err => { + console.error('加载复盘数据失败:', err); + alert('加载失败'); + }); +} + +function renderReviewSummary(data) { + document.getElementById('review-current-date').textContent = `${data.review_date} (${data.week_day})`; + document.getElementById('review-summary-content').style.display = 'block'; + document.getElementById('review-empty-state').style.display = 'none'; + + renderReviewRankings(data.rankings); + renderReviewPlans(data.plans, data.review_date); +} + +function renderReviewRankings(rankings) { + const rankTypes = { + 'volume': 'review-rank-volume', + 'amplitude': 'review-rank-amplitude', + 'change': 'review-rank-change', + 'open_interest': 'review-rank-oi' + }; + + for (const [type, elementId] of Object.entries(rankTypes)) { + const container = document.getElementById(elementId); + container.innerHTML = ''; + + if (rankings[type] && rankings[type].items) { + rankings[type].items.forEach(item => { + const row = document.createElement('div'); + row.className = 'rank-row'; + + const isTop = item.rank <= 3; + const changeClass = item.change_pct && item.change_pct.startsWith('+') ? 'up' : + item.change_pct && item.change_pct.startsWith('-') ? 'down' : ''; + + row.innerHTML = ` +
${item.rank}
+
${item.symbol} ${item.name}
+
${item.value || item.price || ''}
+ `; + + container.appendChild(row); + }); + } + } +} + +function renderReviewPlans(plans, reviewDate) { + const container = document.getElementById('review-plan-list'); + container.innerHTML = ''; + + if (!plans || plans.length === 0) { + container.innerHTML = '
暂无交易计划
'; + return; + } + + plans.forEach((plan, index) => { + const item = document.createElement('div'); + item.className = 'plan-list-item'; + item.onclick = function() { toggleReviewPlan(this); }; + + const planTypeText = plan.plan_type === 'long' ? '做多' : '做空'; + const planTypeClass = plan.plan_type === 'long' ? 'long' : 'short'; + + item.innerHTML = ` +
+ ${plan.symbol} ${plan.name} + ${planTypeText} +
+
+ ${plan.score} + +
+ `; + + const detail = document.createElement('div'); + detail.className = 'plan-detail'; + + const generatedDate = plan.created_at ? new Date(plan.created_at).toLocaleString('zh-CN') : reviewDate; + + detail.innerHTML = ` +
+
+ 评分: ${plan.score}/100
+ 多空逻辑: ${plan.logic || '暂无'}
+ 入选理由: ${plan.reason || '暂无'} +
+
+
+
入场
+
${plan.entry_price || '-'}
+
+
+
止损
+
${plan.stop_loss || '-'}
+
+
+
止盈
+
${plan.take_profit || '-'}
+
+
+
置信度: ${plan.confidence || '-'} | 仓位建议: ${plan.position_suggestion || '-'}
+
计划生成时间: ${generatedDate}
+
+ `; + + container.appendChild(item); + container.appendChild(detail); + }); +} + +function toggleReviewPlan(element) { + const detail = element.nextElementSibling; + const isOpen = detail.classList.contains('open'); + + document.querySelectorAll('#review-plan-list .plan-detail.open').forEach(d => { + d.classList.remove('open'); + d.previousElementSibling.classList.remove('expanded'); + }); + + if (!isOpen) { + detail.classList.add('open'); + element.classList.add('expanded'); + } +} + +function hideReviewSummary() { + document.getElementById('review-summary-content').style.display = 'none'; + document.getElementById('review-empty-state').style.display = 'flex'; + currentReviewDateId = null; +} + +function clearReviewData() { + if (confirm('确定要清除所有复盘与交易计划数据吗?此操作不可恢复。')) { + fetch(`${REVIEW_API_BASE}/clear`, { + method: 'DELETE' + }) + .then(res => res.json()) + .then(data => { + if (data.success) { + alert('复盘数据已清除'); + document.getElementById('review-date-selector').value = ''; + hideReviewSummary(); + loadReviewDates(); + } else { + alert(data.message || '清除失败'); + } + }) + .catch(err => { + console.error('清除数据失败:', err); + alert('清除失败,请稍后重试'); + }); + } +} + +// ==================== 自选品种功能 ==================== + +function renderWatchedList() { + const grid = document.getElementById('watched-grid'); + const emptyState = document.getElementById('watched-empty'); + const totalCountEl = document.getElementById('watched-total-count'); + + if (watchedSymbols.length === 0) { + grid.style.display = 'none'; + emptyState.style.display = 'flex'; + totalCountEl.textContent = '0 个品种'; + return; + } + + grid.style.display = 'grid'; + emptyState.style.display = 'none'; + totalCountEl.textContent = `${watchedSymbols.length} 个品种`; + + const watchedData = allFuturesData.filter(item => watchedSymbols.includes(item.symbol)); + + grid.innerHTML = watchedData.map(item => { + const code = item.symbol.replace(/[0-9]/g, '').substring(0, 2); + const changeIcon = item.change >= 0 ? '↑' : '↓'; + const changeSign = item.change >= 0 ? '+' : ''; + const pctSign = item.changePct >= 0 ? '+' : ''; + + let actionPillClass = 'watch'; + let actionPillText = '观望'; + if (item.suggestion?.includes('做多') || item.suggestion?.includes('试多')) { + actionPillClass = 'do-more'; + actionPillText = '做多'; + } else if (item.suggestion?.includes('做空') || item.suggestion?.includes('试空')) { + actionPillClass = 'do-more'; + actionPillText = '做空'; + } + + const priceClass = item.change >= 0 ? 'up' : 'down'; + const changeClass = item.change >= 0 ? 'up' : 'down'; + + const successRate = item.successRate || 0; + const trendScore = item.trendScore || 0; + const successColor = successRate >= 70 ? 'var(--color-down)' : successRate >= 60 ? 'var(--color-neutral)' : 'var(--color-up)'; + const trendColor = trendScore >= 70 ? 'var(--color-down)' : trendScore >= 50 ? 'var(--color-neutral)' : 'var(--color-up)'; + + const periods = item.periods || {}; + const resistance = item.resistance ? formatNumber(item.resistance) : '--'; + const support = item.support ? formatNumber(item.support) : '--'; + + const isWatched = true; + + return ` +
+
+
+
${code}
+
+
${item.name}
+
${item.symbol}
+
+
+
+
¥${formatNumber(item.price)}
+
${changeIcon} ${changeSign}${formatNumber(item.change)} (${pctSign}${item.changePct.toFixed(2)}%)
+
+
+
${actionPillText}
+
+
+
成功率 ${successRate}%
+
+
+
+
趋势评分 ${trendScore}
+
+
+
+
+
5M
+
15M
+
30M
+
1H
+
+ +
+ `}).join(''); +} + diff --git a/config/symbols_config.json b/config/symbols_config.json index 1328e9d..0a29178 100644 --- a/config/symbols_config.json +++ b/config/symbols_config.json @@ -1,6 +1,6 @@ { "futures": { - "燃油": "FU2606", + "燃油": "FU2609", "低硫燃油": "LU2607", "沪镍": "NI2606", "沪锡": "SN2606", diff --git a/data/buffer.db b/data/buffer.db index 4051e5b..5c7c711 100644 Binary files a/data/buffer.db and b/data/buffer.db differ diff --git a/data/futures_analysis.db b/data/futures_analysis.db index 48c9f78..ac7ddee 100644 Binary files a/data/futures_analysis.db and b/data/futures_analysis.db differ