|
|
|
|
@ -627,6 +627,623 @@ function renderFuturesGrid(data) {
|
|
|
|
|
`}).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== 交易反思子系统 ====================
|
|
|
|
|
|
|
|
|
|
let trReflCurrentDate = null;
|
|
|
|
|
let trReflData = null;
|
|
|
|
|
let trReflTags = [];
|
|
|
|
|
let trReflExperiences = [];
|
|
|
|
|
let trReflTagState = new Set();
|
|
|
|
|
let trReflCurrentPairId = null;
|
|
|
|
|
let trReflCurrentAIResult = null;
|
|
|
|
|
|
|
|
|
|
function trSwitchSubtab(tab) {
|
|
|
|
|
document.querySelectorAll('#tr-subtabs .tr-subtab').forEach(btn => {
|
|
|
|
|
btn.classList.toggle('active', btn.dataset.subtab === tab);
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll('#trade-review-view > .tr-tab-panel').forEach(panel => {
|
|
|
|
|
panel.classList.toggle('active', panel.id === 'tr-tab-' + tab);
|
|
|
|
|
});
|
|
|
|
|
if (tab === 'experience') trExpLoad();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trSwitchToReflection(date) {
|
|
|
|
|
trSwitchSubtab('reflection');
|
|
|
|
|
trReflSetDate(date);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflInit() {
|
|
|
|
|
const dateInput = document.getElementById('tr-refl-current-date');
|
|
|
|
|
if (!dateInput) return;
|
|
|
|
|
|
|
|
|
|
// 默认日期取统计分析的最新交易日或今天
|
|
|
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
|
|
|
trReflSetDate(today);
|
|
|
|
|
|
|
|
|
|
document.getElementById('tr-refl-prev').addEventListener('click', () => trReflChangeDate(-1));
|
|
|
|
|
document.getElementById('tr-refl-next').addEventListener('click', () => trReflChangeDate(1));
|
|
|
|
|
document.getElementById('tr-refl-edit-daily').addEventListener('click', trReflOpenDailyModal);
|
|
|
|
|
|
|
|
|
|
document.getElementById('tr-refl-edit-form').addEventListener('submit', e => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
trReflSaveEdit();
|
|
|
|
|
});
|
|
|
|
|
document.getElementById('tr-refl-daily-form').addEventListener('submit', e => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
trReflSaveDaily();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
const score = parseInt(star.dataset.score);
|
|
|
|
|
container.dataset.value = score;
|
|
|
|
|
trReflUpdateStars(container, score);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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.querySelectorAll('.tr-exp-viewtab').forEach(btn => {
|
|
|
|
|
btn.addEventListener('click', () => {
|
|
|
|
|
document.querySelectorAll('.tr-exp-viewtab').forEach(b => b.classList.remove('active'));
|
|
|
|
|
btn.classList.add('active');
|
|
|
|
|
trExpRender();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
trReflLoadTags();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflSetDate(date) {
|
|
|
|
|
trReflCurrentDate = date;
|
|
|
|
|
document.getElementById('tr-refl-current-date').textContent = date;
|
|
|
|
|
trReflLoadData();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflChangeDate(days) {
|
|
|
|
|
const d = new Date(trReflCurrentDate);
|
|
|
|
|
d.setDate(d.getDate() + days);
|
|
|
|
|
trReflSetDate(d.toISOString().slice(0, 10));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function trReflLoadData() {
|
|
|
|
|
if (!trReflCurrentDate) return;
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${TR_API_BASE}/daily-trades/${trReflCurrentDate}`);
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
trReflData = json.success ? json.data : null;
|
|
|
|
|
if (!trReflData) trReflData = { pairs: [], unpaired: [], daily_reflection: null, summary: {} };
|
|
|
|
|
trReflRender();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('加载反思数据失败', e);
|
|
|
|
|
trReflData = { pairs: [], unpaired: [], daily_reflection: null, summary: {} };
|
|
|
|
|
trReflRender();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflRender() {
|
|
|
|
|
const data = trReflData || { pairs: [], unpaired: [], daily_reflection: null, summary: {} };
|
|
|
|
|
const summary = data.summary || {};
|
|
|
|
|
const daily = data.daily_reflection || {};
|
|
|
|
|
|
|
|
|
|
const pnl = summary.total_pnl || 0;
|
|
|
|
|
const pnlEl = document.getElementById('tr-refl-stat-pnl');
|
|
|
|
|
pnlEl.textContent = (pnl >= 0 ? '+' : '') + pnl.toFixed(2);
|
|
|
|
|
pnlEl.className = 'tr-refl-statvalue ' + (pnl >= 0 ? 'profit' : 'loss');
|
|
|
|
|
|
|
|
|
|
document.getElementById('tr-refl-stat-trades').textContent = summary.total_trades || 0;
|
|
|
|
|
document.getElementById('tr-refl-stat-winrate').textContent = (summary.win_rate || 0) + '%';
|
|
|
|
|
|
|
|
|
|
const reflectedEl = document.getElementById('tr-refl-stat-reflected');
|
|
|
|
|
const reflected = summary.reflected_count || 0;
|
|
|
|
|
const totalPairs = (data.pairs || []).length;
|
|
|
|
|
if (totalPairs === 0) {
|
|
|
|
|
reflectedEl.innerHTML = '<span class="tr-refl-reflected no">无交易</span>';
|
|
|
|
|
} else if (reflected >= totalPairs) {
|
|
|
|
|
reflectedEl.innerHTML = '<span class="tr-refl-reflected yes">✓ 已完成</span>';
|
|
|
|
|
} else {
|
|
|
|
|
reflectedEl.innerHTML = `<span class="tr-refl-reflected no">${reflected}/${totalPairs}</span>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('tr-refl-emotion').textContent = trReflTextMap.emotion[daily.emotion_state] || daily.emotion_state || '未填写';
|
|
|
|
|
document.getElementById('tr-refl-emotion').className = 'tr-refl-daily-value ' + (daily.emotion_state ? '' : 'empty');
|
|
|
|
|
document.getElementById('tr-refl-discipline').textContent = daily.discipline_score ? daily.discipline_score + '/5' : '未填写';
|
|
|
|
|
document.getElementById('tr-refl-discipline').className = 'tr-refl-daily-value ' + (daily.discipline_score ? '' : 'empty');
|
|
|
|
|
document.getElementById('tr-refl-overall').textContent = trReflTextMap.overall[daily.overall_evaluation] || daily.overall_evaluation || '未填写';
|
|
|
|
|
document.getElementById('tr-refl-overall').className = 'tr-refl-daily-value ' + (daily.overall_evaluation ? '' : 'empty');
|
|
|
|
|
document.getElementById('tr-refl-market').textContent = trReflTextMap.market[daily.market_judgement] || daily.market_judgement || '未填写';
|
|
|
|
|
document.getElementById('tr-refl-market').className = 'tr-refl-daily-value ' + (daily.market_judgement ? '' : 'empty');
|
|
|
|
|
|
|
|
|
|
const pairsContainer = document.getElementById('tr-refl-pairs');
|
|
|
|
|
const pairs = data.pairs || [];
|
|
|
|
|
if (!pairs.length) {
|
|
|
|
|
pairsContainer.innerHTML = '<div class="tr-empty" style="grid-column:1/-1;"><div class="tr-empty-icon">📝</div><div class="tr-empty-text">当日无交易配对</div></div>';
|
|
|
|
|
} else {
|
|
|
|
|
pairsContainer.innerHTML = pairs.map(p => trReflRenderPairCard(p)).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const unpairedBody = document.getElementById('tr-refl-unpaired-body');
|
|
|
|
|
const unpairedEmpty = document.getElementById('tr-refl-unpaired-empty');
|
|
|
|
|
const unpaired = data.unpaired || [];
|
|
|
|
|
if (!unpaired.length) {
|
|
|
|
|
unpairedBody.innerHTML = '';
|
|
|
|
|
unpairedEmpty.style.display = 'flex';
|
|
|
|
|
} 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 `<tr>
|
|
|
|
|
<td>${u.trade_time || '-'}</td>
|
|
|
|
|
<td>${u.variety}${u.symbol_name ? '(' + u.symbol_name + ')' : ''}</td>
|
|
|
|
|
<td>${u.direction || '-'}</td>
|
|
|
|
|
<td>${(u.price || 0).toFixed(2)}</td>
|
|
|
|
|
<td>${u.volume || 0}</td>
|
|
|
|
|
<td class="${pnlCls}">${pnl !== 0 ? (pnl >= 0 ? '+' : '') + pnl.toFixed(2) : '-'}</td>
|
|
|
|
|
</tr>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflRenderPairCard(p) {
|
|
|
|
|
const pnl = p.net_pnl || 0;
|
|
|
|
|
const pnlCls = pnl >= 0 ? 'profit' : 'loss';
|
|
|
|
|
const dirCls = p.direction === '多' ? 'long' : 'short';
|
|
|
|
|
const dirText = p.direction === '多' ? '做多' : '做空';
|
|
|
|
|
const tags = (p.tags || []).map(t => `<span class="tr-refl-tag">${t}</span>`).join('');
|
|
|
|
|
const hasReflection = p.reflection && (p.reflection.entry_reason || p.reflection.free_reflection);
|
|
|
|
|
return `<div class="tr-refl-card ${pnlCls}">
|
|
|
|
|
<div class="tr-refl-card-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="tr-refl-card-symbol">${p.symbol_name || p.variety || p.symbol || '-'}</div>
|
|
|
|
|
<span class="tr-refl-card-dir ${dirCls}">${dirText}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tr-refl-card-pnl ${pnlCls}">${pnl >= 0 ? '+' : ''}${pnl.toFixed(2)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tr-refl-card-nodes">
|
|
|
|
|
开仓:${p.open_date || '-'} ${p.open_time || ''} @ ${(p.open_price || 0).toFixed(2)}<br>
|
|
|
|
|
平仓:${p.close_date || '-'} ${p.close_time || ''} @ ${(p.close_price || 0).toFixed(2)}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tr-refl-card-tags">${tags}</div>
|
|
|
|
|
<div class="tr-refl-card-actions">
|
|
|
|
|
<button class="tr-btn tr-btn-primary" onclick='trReflOpenEditModal(${p.id})'>
|
|
|
|
|
<span>${hasReflection ? '✏️' : '✍️'}</span><span>${hasReflection ? '编辑反思' : '写反思'}</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="tr-btn tr-btn-secondary" onclick='trReflOpenTagsModal(${p.id})'>
|
|
|
|
|
<span>🏷️</span><span>标签</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="tr-btn tr-btn-secondary" onclick='trReflOpenAIModal(${p.id})'>
|
|
|
|
|
<span>🤖</span><span>查看分析</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function trReflLoadTags() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${TR_API_BASE}/tags`);
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
if (json.success) trReflTags = json.data || [];
|
|
|
|
|
} catch (e) { console.error('加载标签失败', e); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 反思编辑 Modal =====
|
|
|
|
|
async function trReflOpenEditModal(pairId) {
|
|
|
|
|
const data = trReflData || { pairs: [] };
|
|
|
|
|
const pair = data.pairs.find(p => p.id === pairId);
|
|
|
|
|
if (!pair) return;
|
|
|
|
|
trReflCurrentPairId = pairId;
|
|
|
|
|
const reflection = pair.reflection || {};
|
|
|
|
|
|
|
|
|
|
document.getElementById('tr-refl-edit-pair-id').value = pairId;
|
|
|
|
|
document.getElementById('tr-refl-entry-reason').value = reflection.entry_reason || '';
|
|
|
|
|
document.getElementById('tr-refl-exit-reason').value = reflection.exit_reason || '';
|
|
|
|
|
document.getElementById('tr-refl-entry-timing').value = reflection.entry_timing || '';
|
|
|
|
|
document.getElementById('tr-refl-exit-timing').value = reflection.exit_timing || '';
|
|
|
|
|
document.getElementById('tr-refl-position').value = reflection.position_management || '';
|
|
|
|
|
document.getElementById('tr-refl-free').value = reflection.free_reflection || '';
|
|
|
|
|
|
|
|
|
|
const score = reflection.discipline_score || 0;
|
|
|
|
|
const container = document.getElementById('tr-refl-discipline-score');
|
|
|
|
|
container.dataset.value = score;
|
|
|
|
|
trReflUpdateStars(container, score);
|
|
|
|
|
|
|
|
|
|
// 渲染标签选择
|
|
|
|
|
const tagPool = document.getElementById('tr-refl-edit-tags');
|
|
|
|
|
const selected = new Set(reflection.tags || pair.tags || []);
|
|
|
|
|
tagPool.innerHTML = trReflTags.map(t => {
|
|
|
|
|
const name = typeof t === 'string' ? t : t.name;
|
|
|
|
|
const active = selected.has(name) ? 'selected' : '';
|
|
|
|
|
return `<span class="tr-tag-chip ${active}" onclick="this.classList.toggle('selected')">${name}</span>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
document.getElementById('tr-refl-edit-modal').classList.add('show');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflCloseEditModal() {
|
|
|
|
|
document.getElementById('tr-refl-edit-modal').classList.remove('show');
|
|
|
|
|
trReflCurrentPairId = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function trReflSaveEdit() {
|
|
|
|
|
const pairId = parseInt(document.getElementById('tr-refl-edit-pair-id').value);
|
|
|
|
|
if (!pairId) return;
|
|
|
|
|
|
|
|
|
|
const selectedTags = Array.from(document.querySelectorAll('#tr-refl-edit-tags .tr-tag-chip.selected')).map(el => el.textContent);
|
|
|
|
|
const body = {
|
|
|
|
|
trade_pair_id: pairId,
|
|
|
|
|
entry_reason: document.getElementById('tr-refl-entry-reason').value,
|
|
|
|
|
exit_reason: document.getElementById('tr-refl-exit-reason').value,
|
|
|
|
|
entry_timing: document.getElementById('tr-refl-entry-timing').value,
|
|
|
|
|
exit_timing: document.getElementById('tr-refl-exit-timing').value,
|
|
|
|
|
position_management: document.getElementById('tr-refl-position').value,
|
|
|
|
|
discipline_score: parseInt(document.getElementById('tr-refl-discipline-score').dataset.value || '0'),
|
|
|
|
|
free_reflection: document.getElementById('tr-refl-free').value,
|
|
|
|
|
tags: selectedTags,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${TR_API_BASE}/reflections`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
if (json.success) {
|
|
|
|
|
showToast('success', '保存成功', '反思已保存');
|
|
|
|
|
trReflCloseEditModal();
|
|
|
|
|
trReflLoadData();
|
|
|
|
|
} else {
|
|
|
|
|
showToast('error', '保存失败', json.message || '请重试');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
showToast('error', '保存失败', e.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 当日反思 Modal =====
|
|
|
|
|
function trReflOpenDailyModal() {
|
|
|
|
|
const daily = (trReflData && trReflData.daily_reflection) || {};
|
|
|
|
|
document.getElementById('tr-refl-daily-emotion').value = daily.emotion_state || '';
|
|
|
|
|
document.getElementById('tr-refl-daily-overall').value = daily.overall_evaluation || '';
|
|
|
|
|
document.getElementById('tr-refl-daily-market').value = daily.market_judgement || '';
|
|
|
|
|
document.getElementById('tr-refl-daily-summary').value = daily.summary || '';
|
|
|
|
|
document.getElementById('tr-refl-daily-improvement').value = daily.improvement || '';
|
|
|
|
|
|
|
|
|
|
const score = daily.discipline_score || 0;
|
|
|
|
|
const container = document.getElementById('tr-refl-daily-discipline-score');
|
|
|
|
|
container.dataset.value = score;
|
|
|
|
|
trReflUpdateStars(container, score);
|
|
|
|
|
|
|
|
|
|
document.getElementById('tr-refl-daily-modal').classList.add('show');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflCloseDailyModal() {
|
|
|
|
|
document.getElementById('tr-refl-daily-modal').classList.remove('show');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function trReflSaveDaily() {
|
|
|
|
|
const body = {
|
|
|
|
|
trade_date: trReflCurrentDate,
|
|
|
|
|
emotion_state: document.getElementById('tr-refl-daily-emotion').value,
|
|
|
|
|
overall_evaluation: document.getElementById('tr-refl-daily-overall').value,
|
|
|
|
|
market_judgement: document.getElementById('tr-refl-daily-market').value,
|
|
|
|
|
discipline_score: parseInt(document.getElementById('tr-refl-daily-discipline-score').dataset.value || '0'),
|
|
|
|
|
summary: document.getElementById('tr-refl-daily-summary').value,
|
|
|
|
|
improvement: document.getElementById('tr-refl-daily-improvement').value,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${TR_API_BASE}/daily-reflections`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
if (json.success) {
|
|
|
|
|
showToast('success', '保存成功', '当日反思已保存');
|
|
|
|
|
trReflCloseDailyModal();
|
|
|
|
|
trReflLoadData();
|
|
|
|
|
} else {
|
|
|
|
|
showToast('error', '保存失败', json.message || '请重试');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
showToast('error', '保存失败', e.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 标签管理 Modal =====
|
|
|
|
|
async function trReflOpenTagsModal(pairId) {
|
|
|
|
|
const data = trReflData || { pairs: [] };
|
|
|
|
|
const pair = data.pairs.find(p => p.id === pairId);
|
|
|
|
|
if (!pair) return;
|
|
|
|
|
trReflCurrentPairId = pairId;
|
|
|
|
|
trReflTagState = new Set((pair.tags || []).map(t => typeof t === 'string' ? t : t.name));
|
|
|
|
|
|
|
|
|
|
document.getElementById('tr-refl-tags-pair-id').value = pairId;
|
|
|
|
|
trReflRenderTagPreset();
|
|
|
|
|
document.getElementById('tr-refl-tags-modal').classList.add('show');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflRenderTagPreset() {
|
|
|
|
|
const container = document.getElementById('tr-refl-tags-preset');
|
|
|
|
|
const grouped = trReflGroupTags(trReflTags);
|
|
|
|
|
container.innerHTML = Object.entries(grouped).map(([category, tags]) => `
|
|
|
|
|
<div class="tr-tag-section">
|
|
|
|
|
<div class="tr-tag-section-title">${category}</div>
|
|
|
|
|
<div class="tr-tag-pool">
|
|
|
|
|
${tags.map(t => {
|
|
|
|
|
const name = typeof t === 'string' ? t : t.name;
|
|
|
|
|
const selected = trReflTagState.has(name) ? 'selected' : '';
|
|
|
|
|
return `<span class="tr-tag-chip ${selected}" onclick="trReflToggleTag('${name.replace(/'/g, "\\'")}')">${name}</span>`;
|
|
|
|
|
}).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflGroupTags(tags) {
|
|
|
|
|
const groups = {};
|
|
|
|
|
tags.forEach(t => {
|
|
|
|
|
const category = (typeof t === 'object' && t.category) ? t.category : '常用';
|
|
|
|
|
if (!groups[category]) groups[category] = [];
|
|
|
|
|
groups[category].push(t);
|
|
|
|
|
});
|
|
|
|
|
return groups;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflToggleTag(tag) {
|
|
|
|
|
if (trReflTagState.has(tag)) trReflTagState.delete(tag);
|
|
|
|
|
else trReflTagState.add(tag);
|
|
|
|
|
trReflRenderTagPreset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflAddCustomTag() {
|
|
|
|
|
const input = document.getElementById('tr-refl-tag-input');
|
|
|
|
|
const val = input.value.trim();
|
|
|
|
|
if (!val) return;
|
|
|
|
|
trReflTagState.add(val);
|
|
|
|
|
input.value = '';
|
|
|
|
|
trReflRenderTagPreset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function trReflSaveTags() {
|
|
|
|
|
const pairId = parseInt(document.getElementById('tr-refl-tags-pair-id').value);
|
|
|
|
|
if (!pairId) return;
|
|
|
|
|
|
|
|
|
|
const currentTags = Array.from(trReflTagState);
|
|
|
|
|
const data = trReflData || { pairs: [] };
|
|
|
|
|
const pair = data.pairs.find(p => p.id === pairId);
|
|
|
|
|
const oldTags = (pair && pair.tags) || [];
|
|
|
|
|
const oldNames = new Set(oldTags.map(t => typeof t === 'string' ? t : t.name));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
for (const tag of currentTags) {
|
|
|
|
|
if (!oldNames.has(tag)) {
|
|
|
|
|
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ tag }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const tag of oldTags) {
|
|
|
|
|
const name = typeof tag === 'string' ? tag : tag.name;
|
|
|
|
|
if (!trReflTagState.has(name)) {
|
|
|
|
|
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ tag: name }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
showToast('success', '保存成功', '标签已更新');
|
|
|
|
|
trReflCloseTagsModal();
|
|
|
|
|
trReflLoadData();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
showToast('error', '保存失败', e.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflCloseTagsModal() {
|
|
|
|
|
document.getElementById('tr-refl-tags-modal').classList.remove('show');
|
|
|
|
|
trReflCurrentPairId = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== AI 分析结果 Modal =====
|
|
|
|
|
async function trReflOpenAIModal(pairId) {
|
|
|
|
|
const data = trReflData || { pairs: [] };
|
|
|
|
|
const pair = data.pairs.find(p => p.id === pairId);
|
|
|
|
|
if (!pair) return;
|
|
|
|
|
trReflCurrentPairId = pairId;
|
|
|
|
|
trReflCurrentAIResult = pair.ai_analysis || null;
|
|
|
|
|
|
|
|
|
|
if (!trReflCurrentAIResult) {
|
|
|
|
|
// 如果没有缓存的 AI 结果,尝试请求分析接口
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${TR_API_BASE}/analyze-trade`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
symbol: pair.symbol,
|
|
|
|
|
open_date: pair.open_date,
|
|
|
|
|
open_time: pair.open_time,
|
|
|
|
|
close_date: pair.close_date,
|
|
|
|
|
close_time: pair.close_time,
|
|
|
|
|
direction: pair.direction,
|
|
|
|
|
open_price: pair.open_price,
|
|
|
|
|
close_price: pair.close_price,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
if (json.success && json.data) {
|
|
|
|
|
trReflCurrentAIResult = json.data;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) { console.error('AI分析请求失败', e); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = trReflCurrentAIResult || {};
|
|
|
|
|
document.getElementById('tr-refl-ai-pair-id').value = pairId;
|
|
|
|
|
document.getElementById('tr-refl-ai-overall').textContent = result.overall_evaluation || result.analysis || '暂无 AI 分析结果';
|
|
|
|
|
document.getElementById('tr-refl-ai-strengths').innerHTML = (result.strengths || []).map(s => `<li>${s}</li>`).join('') || '<li>-</li>';
|
|
|
|
|
document.getElementById('tr-refl-ai-weaknesses').innerHTML = (result.weaknesses || []).map(s => `<li>${s}</li>`).join('') || '<li>-</li>';
|
|
|
|
|
document.getElementById('tr-refl-ai-suggestion').textContent = result.suggestion || result.experience_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 = trReflCurrentAIResult;
|
|
|
|
|
const body = {
|
|
|
|
|
title: `${pair.symbol_name || pair.variety || pair.symbol} ${pair.direction === '多' ? '做多' : '做空'} 经验`,
|
|
|
|
|
content: result.overall_evaluation || result.analysis || '',
|
|
|
|
|
type: pair.net_pnl >= 0 ? 'insight' : 'lesson',
|
|
|
|
|
tags: ['AI分析', ...(pair.tags || [])].slice(0, 5),
|
|
|
|
|
source_trade_pair_id: pairId,
|
|
|
|
|
strengths: result.strengths || [],
|
|
|
|
|
weaknesses: result.weaknesses || [],
|
|
|
|
|
suggestion: result.suggestion || result.experience_suggestion || '',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${TR_API_BASE}/experiences`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
if (json.success) {
|
|
|
|
|
showToast('success', '保存成功', '已保存到经验库');
|
|
|
|
|
trReflCloseAIModal();
|
|
|
|
|
} else {
|
|
|
|
|
showToast('error', '保存失败', json.message || '请重试');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
showToast('error', '保存失败', e.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trReflCloseAIModal() {
|
|
|
|
|
document.getElementById('tr-refl-ai-modal').classList.remove('show');
|
|
|
|
|
trReflCurrentPairId = null;
|
|
|
|
|
trReflCurrentAIResult = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 经验库 =====
|
|
|
|
|
async function trExpLoad() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${TR_API_BASE}/experiences`);
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
trReflExperiences = json.success ? (json.data || []) : [];
|
|
|
|
|
trExpPopulateTagFilter();
|
|
|
|
|
trExpRender();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('加载经验库失败', e);
|
|
|
|
|
trReflExperiences = [];
|
|
|
|
|
trExpRender();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trExpPopulateTagFilter() {
|
|
|
|
|
const select = document.getElementById('tr-exp-tag-filter');
|
|
|
|
|
const allTags = new Set();
|
|
|
|
|
trReflExperiences.forEach(e => (e.tags || []).forEach(t => allTags.add(typeof t === 'string' ? t : t.name)));
|
|
|
|
|
const current = select.value;
|
|
|
|
|
select.innerHTML = '<option value="">全部标签</option>' + Array.from(allTags).sort().map(t => `<option value="${t}">${t}</option>`).join('');
|
|
|
|
|
select.value = current;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 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;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!list.length) {
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
container.className = 'tr-exp-grid';
|
|
|
|
|
empty.style.display = 'flex';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
empty.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
if (view === 'timeline') {
|
|
|
|
|
container.className = '';
|
|
|
|
|
list = [...list].sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
|
|
|
|
|
container.innerHTML = `<div style="display:flex;flex-direction:column;gap:16px;">${list.map(e => trExpRenderCard(e, true)).join('')}</div>`;
|
|
|
|
|
} else if (view === 'category') {
|
|
|
|
|
container.className = '';
|
|
|
|
|
const grouped = {};
|
|
|
|
|
list.forEach(e => {
|
|
|
|
|
const k = e.type || '其他';
|
|
|
|
|
if (!grouped[k]) grouped[k] = [];
|
|
|
|
|
grouped[k].push(e);
|
|
|
|
|
});
|
|
|
|
|
const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' };
|
|
|
|
|
container.innerHTML = Object.entries(grouped).map(([k, items]) => `
|
|
|
|
|
<div style="margin-bottom:24px;">
|
|
|
|
|
<div style="font-size:16px;font-weight:700;margin-bottom:12px;color:var(--text-primary);">${typeNames[k] || k}</div>
|
|
|
|
|
<div class="tr-exp-grid">${items.map(e => trExpRenderCard(e)).join('')}</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
} else {
|
|
|
|
|
container.className = 'tr-exp-grid';
|
|
|
|
|
container.innerHTML = list.map(e => trExpRenderCard(e)).join('');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trExpRenderCard(e, timeline = false) {
|
|
|
|
|
const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' };
|
|
|
|
|
const tags = (e.tags || []).map(t => `<span class="tr-refl-tag">${typeof t === 'string' ? t : t.name}</span>`).join('');
|
|
|
|
|
const date = e.created_at ? e.created_at.slice(0, 10) : '';
|
|
|
|
|
return `<div class="tr-exp-card">
|
|
|
|
|
<div class="tr-exp-card-title">${e.title || '未命名经验'}</div>
|
|
|
|
|
<div class="tr-exp-card-meta">${typeNames[e.type] || e.type || '其他'} ${date ? '· ' + date : ''}</div>
|
|
|
|
|
<div class="tr-exp-card-content">${(e.content || '').slice(0, 120)}${(e.content || '').length > 120 ? '...' : ''}</div>
|
|
|
|
|
<div class="tr-exp-card-tags">${tags}</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 星级评分辅助 =====
|
|
|
|
|
function trReflUpdateStars(container, score) {
|
|
|
|
|
container.querySelectorAll('.tr-form-star').forEach(s => {
|
|
|
|
|
s.classList.toggle('active', parseInt(s.dataset.score) <= score);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 文本映射 =====
|
|
|
|
|
const trReflTextMap = {
|
|
|
|
|
emotion: {
|
|
|
|
|
calm: '平静', confident: '自信', anxious: '焦虑', greedy: '贪婪',
|
|
|
|
|
fearful: '恐惧', impulsive: '冲动', tired: '疲惫'
|
|
|
|
|
},
|
|
|
|
|
overall: {
|
|
|
|
|
excellent: '优秀', good: '良好', average: '一般', poor: '较差', bad: '糟糕'
|
|
|
|
|
},
|
|
|
|
|
market: {
|
|
|
|
|
accurate: '准确', basically: '基本准确', deviated: '有所偏差', wrong: '错误'
|
|
|
|
|
},
|
|
|
|
|
timing: {
|
|
|
|
|
excellent: '优秀', good: '良好', average: '一般', poor: '较差', bad: '糟糕'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ==================== 交易复盘功能 ====================
|
|
|
|
|
|
|
|
|
|
const TR_API_BASE = '/api/v1/trade-review';
|
|
|
|
|
@ -671,8 +1288,16 @@ function initTradeReview() {
|
|
|
|
|
// 批次管理
|
|
|
|
|
document.getElementById('tr-btn-batches').addEventListener('click', trShowBatchModal);
|
|
|
|
|
|
|
|
|
|
// 子 Tab 切换
|
|
|
|
|
document.querySelectorAll('#tr-subtabs .tr-subtab').forEach(btn => {
|
|
|
|
|
btn.addEventListener('click', () => trSwitchSubtab(btn.dataset.subtab));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 初始加载
|
|
|
|
|
trInitDefaultDate();
|
|
|
|
|
|
|
|
|
|
// 初始化交易反思子系统
|
|
|
|
|
trReflInit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function trInitDefaultDate() {
|
|
|
|
|
@ -879,18 +1504,19 @@ function trRenderDailyTable(daily) {
|
|
|
|
|
const closePnl = net + d.total_commission;
|
|
|
|
|
const netCls = net >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative';
|
|
|
|
|
const cpCls = closePnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative';
|
|
|
|
|
return `<tr style="cursor:pointer" onclick="trOpenDailyModal('${d.trade_date}')">
|
|
|
|
|
<td class="tr-link">${d.trade_date}</td>
|
|
|
|
|
<td>${d.total_trades}</td>
|
|
|
|
|
<td class="${cpCls}">${closePnl >= 0 ? '+' : ''}${closePnl.toFixed(2)}</td>
|
|
|
|
|
<td>${d.total_commission.toFixed(2)}</td>
|
|
|
|
|
<td class="${netCls}">${net >= 0 ? '+' : ''}${net.toFixed(2)}</td>
|
|
|
|
|
<td>${d.win_count}</td>
|
|
|
|
|
<td>${d.loss_count}</td>
|
|
|
|
|
<td>${d.win_rate}%</td>
|
|
|
|
|
<td class="tr-pnl-positive">+${d.max_win.toFixed(2)}</td>
|
|
|
|
|
<td class="tr-pnl-negative">${d.max_loss.toFixed(2)}</td>
|
|
|
|
|
<td>${d.variety_count}</td>
|
|
|
|
|
return `<tr>
|
|
|
|
|
<td class="tr-link" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.trade_date}</td>
|
|
|
|
|
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.total_trades}</td>
|
|
|
|
|
<td class="${cpCls}" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${closePnl >= 0 ? '+' : ''}${closePnl.toFixed(2)}</td>
|
|
|
|
|
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.total_commission.toFixed(2)}</td>
|
|
|
|
|
<td class="${netCls}" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${net >= 0 ? '+' : ''}${net.toFixed(2)}</td>
|
|
|
|
|
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.win_count}</td>
|
|
|
|
|
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.loss_count}</td>
|
|
|
|
|
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.win_rate}%</td>
|
|
|
|
|
<td class="tr-pnl-positive" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">+${d.max_win.toFixed(2)}</td>
|
|
|
|
|
<td class="tr-pnl-negative" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.max_loss.toFixed(2)}</td>
|
|
|
|
|
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.variety_count}</td>
|
|
|
|
|
<td><span class="tr-link" onclick="event.stopPropagation(); trSwitchToReflection('${d.trade_date}')">进入反思</span></td>
|
|
|
|
|
</tr>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
|