diff --git a/app/static/futures_analysis.html b/app/static/futures_analysis.html index 9dd739d..e36e665 100644 --- a/app/static/futures_analysis.html +++ b/app/static/futures_analysis.html @@ -495,6 +495,7 @@ .tr-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 12px; } .tr-toolbar-left, .tr-toolbar-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .tr-btn { padding: 10px 20px; border-radius: 12px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; display: flex; align-items: center; gap: 8px; transition: all 0.2s; font-family: inherit; } + .tr-btn:disabled { opacity: 0.5; cursor: not-allowed; } .tr-btn-primary { background: var(--color-brand); color: #fff; } .tr-btn-primary:hover { background: #0066d6; box-shadow: 0 4px 10px rgba(0,122,255,0.3); } .tr-btn-secondary { background: #fff; color: var(--text-secondary); box-shadow: var(--shadow-sm); border: 1px solid rgba(0,0,0,0.05); } @@ -647,9 +648,12 @@ /* 未配对交易 */ .tr-refl-unpaired { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; box-shadow: var(--shadow-sm); } + .tr-refl-unpaired-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } + .tr-refl-unpaired-header .tr-refl-section-title { margin-bottom: 0; } .tr-refl-unpaired-table { width: 100%; border-collapse: collapse; font-size: 13px; } .tr-refl-unpaired-table th { background: #F5F5F7; padding: 10px 8px; text-align: left; font-weight: 600; color: var(--text-tertiary); font-size: 12px; } .tr-refl-unpaired-table td { padding: 10px 8px; border-bottom: 1px solid rgba(0,0,0,0.04); color: var(--text-primary); } + .tr-refl-unpaired-table input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--color-brand); cursor: pointer; } /* 经验库 */ .tr-exp-toolbar { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; } @@ -667,6 +671,13 @@ .tr-exp-card-meta { font-size: 12px; color: var(--text-tertiary); margin-bottom: 12px; } .tr-exp-card-content { font-size: 13px; color: var(--text-secondary); line-height: 1.7; margin-bottom: 14px; } .tr-exp-card-tags { display: flex; flex-wrap: wrap; gap: 6px; } + .tr-exp-pagination { display: flex; align-items: center; justify-content: center; gap: 16px; margin-top: 24px; } + .tr-exp-pageinfo { font-size: 13px; color: var(--text-secondary); font-weight: 500; } + .tr-exp-detail-meta { font-size: 12px; color: var(--text-tertiary); margin-bottom: 16px; } + .tr-exp-detail-content { font-size: 14px; color: var(--text-primary); line-height: 1.8; background: #F5F5F7; border-radius: 12px; padding: 16px; margin-bottom: 16px; white-space: pre-wrap; } + .tr-exp-detail-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; } + .tr-exp-detail-source { font-size: 13px; color: var(--text-secondary); } + .tr-exp-detail-source .link { cursor: pointer; } /* Modal 表单 */ .tr-form-group { margin-bottom: 18px; } @@ -691,6 +702,9 @@ .tr-tag-custom .tr-form-input { flex: 1; } /* AI 分析结果 */ + .tr-ai-version-row { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; padding: 12px 16px; background: #F5F5F7; border-radius: 12px; } + .tr-ai-version-label { font-size: 13px; font-weight: 600; color: var(--text-tertiary); } + .tr-ai-version-row .tr-form-select { flex: 1; min-width: 200px; margin: 0; } .tr-ai-result-section { margin-bottom: 18px; } .tr-ai-result-title { font-size: 14px; font-weight: 700; color: var(--color-brand); margin-bottom: 8px; } .tr-ai-result-content { font-size: 14px; color: var(--text-primary); line-height: 1.8; background: #F5F5F7; border-radius: 12px; padding: 14px; } @@ -1244,10 +1258,15 @@
-
未配对交易
+
+
未配对交易
+ +
- +
时间品种方向价格手数盈亏
时间品种开平方向价格手数盈亏
@@ -1282,6 +1301,11 @@
📚
暂无经验记录
+ @@ -1455,6 +1479,25 @@ + +
+
+
+ 经验详情 + +
+
+
+
+
+
+
+ +
+
+
+
+
@@ -1464,6 +1507,10 @@
+
+ + +
综合评价
-
diff --git a/app/static/futures_analysis.js b/app/static/futures_analysis.js index 379b8ae..5016d2b 100644 --- a/app/static/futures_analysis.js +++ b/app/static/futures_analysis.js @@ -634,9 +634,14 @@ let trReflData = null; let trReflTags = []; let trReflExperiences = []; let trReflTagState = new Set(); +let trExpPage = 1; +let trExpPageSize = 12; +let trExpTotal = 0; let trReflCurrentPairId = null; let trReflCurrentReflectionId = null; let trReflCurrentAIResult = null; +let trReflAIHistory = []; +let trReflCurrentAnalysis = null; function trSwitchSubtab(tab) { document.querySelectorAll('#tr-subtabs .tr-subtab').forEach(btn => { @@ -682,6 +687,16 @@ function trReflInit() { }); } + const manualPairBtn = document.getElementById('tr-refl-manual-pair-btn'); + if (manualPairBtn) manualPairBtn.addEventListener('click', trReflManualPair); + + const selectAll = document.getElementById('tr-refl-unpaired-select-all'); + if (selectAll) { + selectAll.addEventListener('change', function() { + document.querySelectorAll('.tr-refl-unpaired-check').forEach(cb => cb.checked = this.checked); + }); + } + document.querySelectorAll('#tr-refl-discipline-score .tr-form-star, #tr-refl-daily-discipline-score .tr-form-star').forEach(star => { star.addEventListener('click', () => { const container = star.parentElement; @@ -691,9 +706,11 @@ function trReflInit() { }); }); - document.getElementById('tr-exp-search').addEventListener('input', trExpRender); - document.getElementById('tr-exp-tag-filter').addEventListener('change', trExpRender); - document.getElementById('tr-exp-type-filter').addEventListener('change', trExpRender); + document.getElementById('tr-exp-search').addEventListener('input', () => { trExpPage = 1; trExpLoad(); }); + document.getElementById('tr-exp-tag-filter').addEventListener('change', () => { trExpPage = 1; trExpLoad(); }); + document.getElementById('tr-exp-type-filter').addEventListener('change', () => { trExpPage = 1; trExpLoad(); }); + document.getElementById('tr-exp-prev').addEventListener('click', () => { if (trExpPage > 1) { trExpPage--; trExpLoad(); } }); + document.getElementById('tr-exp-next').addEventListener('click', () => { if (trExpPage * trExpPageSize < trExpTotal) { trExpPage++; trExpLoad(); } }); document.querySelectorAll('.tr-exp-viewtab').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.tr-exp-viewtab').forEach(b => b.classList.remove('active')); @@ -775,18 +792,22 @@ function trReflRender() { const unpairedBody = document.getElementById('tr-refl-unpaired-body'); const unpairedEmpty = document.getElementById('tr-refl-unpaired-empty'); + const selectAll = document.getElementById('tr-refl-unpaired-select-all'); const unpaired = data.unpaired || []; if (!unpaired.length) { unpairedBody.innerHTML = ''; unpairedEmpty.style.display = 'flex'; + if (selectAll) selectAll.checked = false; } else { unpairedEmpty.style.display = 'none'; unpairedBody.innerHTML = unpaired.map(u => { const pnl = u.net_pnl || u.close_pnl || 0; const pnlCls = pnl > 0 ? 'tr-pnl-positive' : (pnl < 0 ? 'tr-pnl-negative' : ''); return ` + ${u.trade_time || '-'} ${u.variety}${u.symbol_name ? '(' + u.symbol_name + ')' : ''} + ${u.offset || '-'} ${u.direction || '-'} ${(u.price || 0).toFixed(2)} ${u.volume || 0} @@ -796,6 +817,63 @@ function trReflRender() { } } +async function trReflManualPair() { + const checked = Array.from(document.querySelectorAll('.tr-refl-unpaired-check:checked')); + if (!checked.length) { + showToast('warning', '请选择记录', '请至少选择一条未配对交易记录'); + return; + } + + const openRecords = checked.filter(cb => cb.dataset.offset === '开'); + const closeRecords = checked.filter(cb => cb.dataset.offset === '平'); + + if (!openRecords.length) { + showToast('warning', '缺少开仓记录', '手动配对需要至少一条开仓记录(开)'); + return; + } + if (!closeRecords.length) { + showToast('warning', '缺少平仓记录', '手动配对需要至少一条平仓记录(平)'); + return; + } + + const symbols = new Set(checked.map(cb => cb.dataset.symbol)); + if (symbols.size > 1) { + showToast('warning', '品种不一致', '请选择同一品种的开仓和平仓记录'); + return; + } + + const openDirection = openRecords[0].dataset.direction; + const closeDirection = closeRecords[0].dataset.direction; + + // 多头:开=买,平=卖;空头:开=卖,平=买 + const isLong = openDirection === '买' && closeDirection === '卖'; + const isShort = openDirection === '卖' && closeDirection === '买'; + if (!isLong && !isShort) { + showToast('warning', '方向不匹配', '多头配对:开仓为买、平仓为卖;空头配对:开仓为卖、平仓为买'); + return; + } + + const openRecordIds = openRecords.map(cb => parseInt(cb.dataset.id)); + const closeRecordIds = closeRecords.map(cb => parseInt(cb.dataset.id)); + + try { + const res = await fetch(`${TR_API_BASE}/trade-pairs`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ open_record_ids: openRecordIds, close_record_ids: closeRecordIds }), + }); + const json = await res.json(); + if (json.success) { + showToast('success', '配对成功', '交易配对已创建'); + trReflLoadData(); + } else { + showToast('error', '配对失败', json.message || '请重试'); + } + } catch (e) { + showToast('error', '配对失败', e.message); + } +} + function trReflRenderPairCard(p) { const pnl = p.net_pnl || 0; const pnlCls = pnl >= 0 ? 'profit' : 'loss'; @@ -1190,6 +1268,17 @@ async function trReflOpenAIModal(pairId, fetchLatest = true) { const pair = data.pairs.find(p => p.id === pairId); if (!pair) return; trReflCurrentPairId = pairId; + trReflAIHistory = []; + trReflCurrentAnalysis = null; + + // 加载历史版本列表 + try { + const historyRes = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/history`); + const historyJson = await historyRes.json(); + if (historyJson.success && historyJson.data) { + trReflAIHistory = Array.isArray(historyJson.data) ? historyJson.data : (historyJson.data.items || []); + } + } catch (e) { console.error('获取AI分析历史失败', e); } if (fetchLatest) { trReflCurrentAIResult = null; @@ -1221,25 +1310,71 @@ async function trReflOpenAIModal(pairId, fetchLatest = true) { } } catch (e) { console.error('AI重新分析失败', e); } } + + // 如果历史列表为空但拿到最新结果,补充进历史列表 + if (!trReflAIHistory.length && trReflCurrentAIResult) { + trReflAIHistory = [trReflCurrentAIResult]; + } } - const result = trReflCurrentAIResult || {}; + // 优先从历史列表中选中与当前结果匹配的版本,否则默认第一个 + let selected = trReflAIHistory[0] || trReflCurrentAIResult || null; + if (trReflCurrentAIResult && trReflCurrentAIResult.id && trReflAIHistory.length) { + const matched = trReflAIHistory.find(a => a.id === trReflCurrentAIResult.id); + if (matched) selected = matched; + } + trReflCurrentAnalysis = selected; + + trReflRenderAIVersionSelect(); + trReflDisplayAIAnalysis(trReflCurrentAnalysis); + document.getElementById('tr-refl-ai-pair-id').value = pairId; + document.getElementById('tr-refl-ai-modal').classList.add('show'); +} + +function trReflRenderAIVersionSelect() { + const select = document.getElementById('tr-refl-ai-version'); + if (!select) return; + if (!trReflAIHistory.length) { + select.innerHTML = ''; + select.disabled = true; + return; + } + select.disabled = false; + select.innerHTML = trReflAIHistory.map((a, idx) => { + const version = a.version || (trReflAIHistory.length - idx); + const created = a.created_at ? a.created_at.slice(0, 19).replace('T', ' ') : ''; + const selected = (trReflCurrentAnalysis && trReflCurrentAnalysis.id === a.id) ? 'selected' : ''; + return ``; + }).join(''); + select.onchange = trReflSwitchAIVersion; +} + +function trReflSwitchAIVersion() { + const select = document.getElementById('tr-refl-ai-version'); + if (!select) return; + const value = select.value; + const analysis = trReflAIHistory.find(a => String(a.id) === value); + if (!analysis) return; + trReflCurrentAnalysis = analysis; + trReflDisplayAIAnalysis(analysis); +} + +function trReflDisplayAIAnalysis(analysis) { + const result = analysis || {}; document.getElementById('tr-refl-ai-overall').textContent = result.overall_evaluation || result.analysis || '暂无 AI 分析结果(请先保存反思)'; document.getElementById('tr-refl-ai-strengths').innerHTML = (result.strengths || []).map(s => `
  • ${s}
  • `).join('') || '
  • -
  • '; document.getElementById('tr-refl-ai-weaknesses').innerHTML = (result.weaknesses || []).map(s => `
  • ${s}
  • `).join('') || '
  • -
  • '; document.getElementById('tr-refl-ai-suggestion').textContent = result.experience_suggestion || result.suggestion || '暂无建议'; - - document.getElementById('tr-refl-ai-modal').classList.add('show'); } async function trReflSaveExperience() { const pairId = parseInt(document.getElementById('tr-refl-ai-pair-id').value); const data = trReflData || { pairs: [] }; const pair = data.pairs.find(p => p.id === pairId); - if (!pair || !trReflCurrentAIResult) return; + const result = trReflCurrentAnalysis || trReflCurrentAIResult; + if (!pair || !result) return; - const result = trReflCurrentAIResult; const analysisId = result.id; if (!analysisId) { showToast('error', '保存失败', '缺少分析记录ID'); @@ -1274,19 +1409,38 @@ function trReflCloseAIModal() { document.getElementById('tr-refl-ai-modal').classList.remove('show'); trReflCurrentPairId = null; trReflCurrentAIResult = null; + trReflCurrentAnalysis = null; + trReflAIHistory = []; } // ===== 经验库 ===== async function trExpLoad() { try { - const res = await fetch(`${TR_API_BASE}/experiences`); + const params = new URLSearchParams(); + params.set('page', trExpPage); + params.set('page_size', trExpPageSize); + const keyword = document.getElementById('tr-exp-search').value.trim(); + const tag = document.getElementById('tr-exp-tag-filter').value; + const type = document.getElementById('tr-exp-type-filter').value; + if (keyword) params.set('keyword', keyword); + if (tag) params.set('tag', tag); + if (type) params.set('type', type); + + const res = await fetch(`${TR_API_BASE}/experiences?${params.toString()}`); const json = await res.json(); - trReflExperiences = json.success ? (json.data || []) : []; + if (json.success && json.data) { + trReflExperiences = json.data.items || json.data || []; + trExpTotal = json.data.total || trReflExperiences.length; + } else { + trReflExperiences = []; + trExpTotal = 0; + } trExpPopulateTagFilter(); trExpRender(); } catch (e) { console.error('加载经验库失败', e); trReflExperiences = []; + trExpTotal = 0; trExpRender(); } } @@ -1303,30 +1457,33 @@ function trExpPopulateTagFilter() { function trExpRender() { const container = document.getElementById('tr-exp-container'); const empty = document.getElementById('tr-exp-empty'); - const keyword = document.getElementById('tr-exp-search').value.toLowerCase(); - const tag = document.getElementById('tr-exp-tag-filter').value; - const type = document.getElementById('tr-exp-type-filter').value; + const pagination = document.getElementById('tr-exp-pagination'); + const pageInfo = document.getElementById('tr-exp-pageinfo'); const view = document.querySelector('.tr-exp-viewtab.active').dataset.view; - let list = trReflExperiences.filter(e => { - const matchKeyword = !keyword || (e.title || '').toLowerCase().includes(keyword) || (e.content || '').toLowerCase().includes(keyword); - const matchTag = !tag || (e.tags || []).some(t => (typeof t === 'string' ? t : t.name) === tag); - const matchType = !type || e.type === type; - return matchKeyword && matchTag && matchType; - }); + const list = trReflExperiences; if (!list.length) { container.innerHTML = ''; container.className = 'tr-exp-grid'; empty.style.display = 'flex'; + if (pagination) pagination.style.display = 'none'; return; } empty.style.display = 'none'; + const totalPages = Math.max(1, Math.ceil(trExpTotal / trExpPageSize)); + if (pagination) { + pagination.style.display = 'flex'; + pageInfo.textContent = `第 ${trExpPage} / ${totalPages} 页,共 ${trExpTotal} 条`; + document.getElementById('tr-exp-prev').disabled = trExpPage <= 1; + document.getElementById('tr-exp-next').disabled = trExpPage >= totalPages; + } + if (view === 'timeline') { container.className = ''; - list = [...list].sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)); - container.innerHTML = `
    ${list.map(e => trExpRenderCard(e, true)).join('')}
    `; + const sorted = [...list].sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)); + container.innerHTML = `
    ${sorted.map(e => trExpRenderCard(e, true)).join('')}
    `; } else if (view === 'category') { container.className = ''; const grouped = {}; @@ -1352,7 +1509,7 @@ function trExpRenderCard(e, timeline = false) { const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' }; const tags = (e.tags || []).map(t => `${typeof t === 'string' ? t : t.name}`).join(''); const date = e.created_at ? e.created_at.slice(0, 10) : ''; - return `
    + return `
    ${e.title || '未命名经验'}
    ${typeNames[e.type] || e.type || '其他'} ${date ? '· ' + date : ''}
    ${(e.content || '').slice(0, 120)}${(e.content || '').length > 120 ? '...' : ''}
    @@ -1360,6 +1517,39 @@ function trExpRenderCard(e, timeline = false) {
    `; } +function trExpOpenDetailModal(id) { + const e = trReflExperiences.find(item => item.id === id); + if (!e) return; + + const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' }; + const tags = (e.tags || []).map(t => `${typeof t === 'string' ? t : t.name}`).join(''); + const created = e.created_at ? e.created_at.slice(0, 19).replace('T', ' ') : '-'; + const sourceDate = e.source_trade_date || (e.source_pair ? e.source_pair.trade_date : ''); + + document.getElementById('tr-exp-detail-title').textContent = e.title || '经验详情'; + document.getElementById('tr-exp-detail-meta').textContent = `${typeNames[e.type] || e.type || '其他'} · 创建于 ${created}${sourceDate ? ' · 来源交易日期 ' + sourceDate : ''}`; + document.getElementById('tr-exp-detail-content').textContent = e.content || ''; + document.getElementById('tr-exp-detail-tags').innerHTML = tags || '无标签'; + + const sourceEl = document.getElementById('tr-exp-detail-source'); + if (e.source_pair_id) { + sourceEl.innerHTML = `查看原交易`; + } else { + sourceEl.innerHTML = ''; + } + + document.getElementById('tr-exp-detail-modal').classList.add('show'); +} + +function trExpCloseDetailModal() { + document.getElementById('tr-exp-detail-modal').classList.remove('show'); +} + +function trExpViewSourcePair(pairId) { + console.log('查看原交易配对:', pairId); + alert(`查看原交易配对:${pairId}`); +} + // ===== 星级评分辅助 ===== function trReflUpdateStars(container, score) { container.querySelectorAll('.tr-form-star').forEach(s => {