+
+
+
+
综合评价
-
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 => {