From 7a0d01541692c22a8b9f8a967fd565de29cf91e9 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 17 Jul 2026 13:41:13 +0800 Subject: [PATCH] Add archive tabs for trades vs review quotes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch hub 内照明心 to a single-column tab layout and auto-show that day's closed trades under the quote form for AI review context. Co-authored-by: Cursor --- manual_trading_hub/hub_ai/prompts.py | 4 +- manual_trading_hub/static/app.css | 101 ++++++++++++---- manual_trading_hub/static/archive.js | 173 ++++++++++++++++++++++++++- manual_trading_hub/static/index.html | 43 ++++--- 4 files changed, 282 insertions(+), 39 deletions(-) diff --git a/manual_trading_hub/hub_ai/prompts.py b/manual_trading_hub/hub_ai/prompts.py index b70e7cf..2e64b90 100644 --- a/manual_trading_hub/hub_ai/prompts.py +++ b/manual_trading_hub/hub_ai/prompts.py @@ -165,7 +165,7 @@ def build_chat_user_prompt( ARCHIVE_QUOTE_REVIEW_INSTRUCTION = """ -【任务】用户从内照明心提交了一条复盘语录,并附上该交易日的档案交易记录(仅供你分析,用户界面不展示明细). +【任务】用户从内照明心提交了一条复盘语录,并附上该交易日的档案交易记录(界面「复盘语录」下方也会展示当日已平仓明细). 请结合语录与交易记录: 1) 帮他核对自述与操作事实是否一致; 2) 指出心态,纪律,执行上的偏差点(若有); @@ -236,7 +236,7 @@ def build_archive_quote_review_prompt( parts = [ f"【复盘交易日】{quote_date}", ARCHIVE_QUOTE_REVIEW_INSTRUCTION, - "【该日交易记录(内照明心档案,用户不可见此段)】", + "【该日交易记录(内照明心档案,与界面「当日已平仓」一致)】", (archive_trades_text or "(该日无交易记录)").strip(), "【用户复盘语录(对话框已展示,请优先回应)】", user_message.strip(), diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 3f3c309..baa0826 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -7956,10 +7956,30 @@ body.funds-fullscreen-open { margin-bottom: 12px; } .archive-layout { - display: grid; - grid-template-columns: minmax(240px, 300px) minmax(0, 1fr); - gap: 14px; - align-items: start; + display: flex; + flex-direction: column; + gap: 12px; + align-items: stretch; +} +.archive-content-tabs { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.archive-content-tab { + border: 1px solid var(--border-soft); + background: transparent; + color: inherit; + padding: 7px 14px; + border-radius: 8px; + cursor: pointer; + font-family: var(--font); + font-size: 0.84rem; +} +.archive-content-tab.is-active { + background: rgba(59, 130, 246, 0.22); + border-color: rgba(59, 130, 246, 0.55); + color: var(--text); } .archive-quotes-panel, .archive-main-panel { @@ -7968,13 +7988,23 @@ body.funds-fullscreen-open { border-radius: var(--radius); min-width: 0; } -.archive-quotes-panel { +.archive-tab-panel { + display: none; +} +.archive-tab-panel.is-active { display: flex; flex-direction: column; +} +.archive-quotes-panel.is-active { gap: 10px; padding: 12px; overflow: visible; } +.archive-main-panel.is-active { + gap: 10px; + padding: 12px; + min-height: 0; +} .archive-panel-head { display: flex; align-items: center; @@ -8009,6 +8039,46 @@ body.funds-fullscreen-open { .archive-quote-form textarea { min-height: 110px; } +.archive-quote-day-trades { + border: 1px solid var(--border-soft); + border-radius: 8px; + background: var(--inset-surface); + padding: 10px; + display: flex; + flex-direction: column; + gap: 8px; +} +.archive-quote-day-trades-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.archive-quote-day-trades-head h3 { + margin: 0; + font-size: 0.84rem; + font-weight: 600; +} +.archive-quote-day-trades-body { + overflow: auto; + max-height: min(280px, 40vh); +} +.archive-quote-day-trades-table { + width: 100%; + border-collapse: collapse; + font-size: 0.78rem; +} +.archive-quote-day-trades-table th, +.archive-quote-day-trades-table td { + padding: 6px 8px; + border-bottom: 1px solid var(--border-soft); + text-align: left; + white-space: nowrap; +} +.archive-quote-day-trades-table th { + color: var(--muted); + font-weight: 500; +} .archive-quotes-list { flex: 0 1 auto; max-height: min(420px, 50vh); @@ -8101,14 +8171,6 @@ body.funds-fullscreen-open { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, var(--border-soft)); } -.archive-main-panel { - display: flex; - flex-direction: column; - gap: 10px; - padding: 12px; - min-height: 100%; - min-height: 0; -} .archive-period-bar { flex-wrap: wrap; } @@ -8841,19 +8903,13 @@ body.funds-fullscreen-open { gap: 12px; min-height: 0; } - #page-archive .archive-quotes-panel { - order: 1; + #page-archive .archive-quotes-panel, + #page-archive .archive-main-panel { flex: 0 0 auto; min-height: 0; max-height: none; overflow: visible; } - #page-archive .archive-main-panel { - order: 2; - flex: 0 0 auto; - min-height: 0; - gap: 10px; - } #page-archive .archive-stats-card { margin-bottom: 10px; } @@ -8861,6 +8917,9 @@ body.funds-fullscreen-open { min-height: 120px; max-height: 42vh; } + #page-archive .archive-quote-day-trades-body { + max-height: 32vh; + } #page-archive .archive-stats-table th, #page-archive .archive-stats-table td { padding: 6px 8px; diff --git a/manual_trading_hub/static/archive.js b/manual_trading_hub/static/archive.js index 3be3f9c..9b9e8e0 100644 --- a/manual_trading_hub/static/archive.js +++ b/manual_trading_hub/static/archive.js @@ -34,6 +34,11 @@ const elQuoteDate = document.getElementById("archive-quote-date"); const elQuoteContent = document.getElementById("archive-quote-content"); const elQuoteSubmit = document.getElementById("archive-quote-submit"); + const elContentTabs = document.getElementById("archive-content-tabs"); + const elPanelTrades = document.getElementById("archive-panel-trades"); + const elPanelQuotes = document.getElementById("archive-panel-quotes"); + const elQuoteDayTradesMeta = document.getElementById("archive-quote-day-trades-meta"); + const elQuoteDayTradesBody = document.getElementById("archive-quote-day-trades-body"); const elChartSection = document.getElementById("archive-chart-section"); const elChartTitle = document.getElementById("archive-chart-title"); const elTfTabs = document.getElementById("archive-tf-tabs"); @@ -65,6 +70,10 @@ let quotes = []; let selectedQuoteId = null; let editingQuoteId = null; + let archiveContentTab = "trades"; + let quoteDayTrades = []; + let quoteDayTradesDay = ""; + let quoteDayTradesReq = 0; let dailyTrades = []; let tradesPage = 0; let dailyStats = { open_count: 0, by_exchange: {} }; @@ -547,9 +556,11 @@ selectedCalendarDay = day; setPeriodMode("today"); if (elTradingDay) elTradingDay.value = day; + if (elQuoteDate) elQuoteDate.value = day; if (elFilterSick) elFilterSick.checked = false; syncPeriodUI(); void loadDailyTrades(); + if (archiveContentTab === "quotes") void loadQuoteDayTrades(); }, }); calendarWidget.ensureMonth(calendarRefDate()); @@ -943,6 +954,146 @@ elQuoteSubmit.textContent = editingQuoteId ? "修改保存" : "添加语录"; } + function syncQuoteDateFromTradingDay(force) { + if (!elQuoteDate) return; + const day = + (elTradingDay && elTradingDay.value) || + selectedCalendarDay || + tradingDay || + ""; + if (!day) return; + if (force || !elQuoteDate.value) { + elQuoteDate.value = day; + } + } + + function setArchiveContentTab(tab) { + const next = tab === "quotes" ? "quotes" : "trades"; + archiveContentTab = next; + const showQuotes = next === "quotes"; + if (elContentTabs) { + elContentTabs.querySelectorAll(".archive-content-tab").forEach(function (btn) { + const on = btn.getAttribute("data-archive-tab") === next; + btn.classList.toggle("is-active", on); + btn.setAttribute("aria-selected", on ? "true" : "false"); + }); + } + if (elPanelTrades) { + elPanelTrades.classList.toggle("is-active", !showQuotes); + elPanelTrades.hidden = showQuotes; + } + if (elPanelQuotes) { + elPanelQuotes.classList.toggle("is-active", showQuotes); + elPanelQuotes.hidden = !showQuotes; + } + if (showQuotes) { + syncQuoteDateFromTradingDay(false); + void loadQuoteDayTrades(); + } else { + requestAnimationFrame(syncTradesLayout); + } + } + + function quoteTagLabel(t) { + if (!t) return "—"; + if (t.behavior_tag_from_journal || t.behavior_tag === "sick") return "犯病"; + if (t.behavior_tag === "emotion") return "情绪"; + return "—"; + } + + function renderQuoteDayTrades() { + if (!elQuoteDayTradesBody) return; + const day = quoteDayTradesDay || (elQuoteDate && elQuoteDate.value) || ""; + if (elQuoteDayTradesMeta) { + elQuoteDayTradesMeta.textContent = day + ? day + " · " + quoteDayTrades.length + " 笔" + : ""; + } + if (!day) { + elQuoteDayTradesBody.innerHTML = + '

选择日期后自动带上该日已平仓记录.

'; + return; + } + if (!quoteDayTrades.length) { + elQuoteDayTradesBody.innerHTML = + '

该日暂无已平仓记录.

'; + return; + } + elQuoteDayTradesBody.innerHTML = + '' + + "" + + "" + + quoteDayTrades + .map(function (t) { + return ( + "" + + "" + + "" + + "" + + '" + + '" + + '" + + "" + ); + }) + .join("") + + "
交易所合约方向开仓平仓盈亏标签
" + + esc(tradeRowExchange(t)) + + "" + + esc(t.symbol || "—") + + "" + + esc(t.direction || "—") + + "' + + fmtDt(t.opened_at) + + "' + + fmtDt(t.closed_at) + + "' + + fmtPnl(t.pnl_amount) + + "" + + esc(quoteTagLabel(t)) + + "
"; + } + + async function loadQuoteDayTrades() { + if (!elQuoteDayTradesBody) return; + const day = elQuoteDate && String(elQuoteDate.value || "").trim(); + if (!day) { + quoteDayTrades = []; + quoteDayTradesDay = ""; + renderQuoteDayTrades(); + return; + } + const req = ++quoteDayTradesReq; + const q = new URLSearchParams(); + q.set("period", "today"); + q.set("trading_day", day); + elQuoteDayTradesBody.innerHTML = '

加载当日已平仓…

'; + if (elQuoteDayTradesMeta) elQuoteDayTradesMeta.textContent = day; + try { + const r = await apiFetch("/api/archive/daily-trades?" + q.toString()); + const j = await r.json(); + if (req !== quoteDayTradesReq) return; + if (!r.ok) { + quoteDayTrades = []; + quoteDayTradesDay = day; + elQuoteDayTradesBody.innerHTML = + '

' + esc(j.detail || "加载失败") + "

"; + return; + } + quoteDayTrades = j.trades || []; + quoteDayTradesDay = day; + renderQuoteDayTrades(); + } catch (_) { + if (req !== quoteDayTradesReq) return; + quoteDayTrades = []; + quoteDayTradesDay = day; + elQuoteDayTradesBody.innerHTML = + '

加载失败

'; + } + } + function resetQuoteForm() { editingQuoteId = null; if (elQuoteContent) elQuoteContent.value = ""; @@ -959,6 +1110,7 @@ elQuoteContent.focus(); } updateQuoteSubmitBtn(); + void loadQuoteDayTrades(); } function selectQuote(id) { @@ -1839,6 +1991,7 @@ renderStats(); renderTrades(); void loadCalendar(); + if (archiveContentTab === "quotes") void loadQuoteDayTrades(); setStatus( (periodLabel || tradingDay || "当日") + " · 列表 " + @@ -1932,7 +2085,12 @@ loadDailyTrades(); }); } - if (elTradingDay) elTradingDay.addEventListener("change", loadDailyTrades); + if (elTradingDay) { + elTradingDay.addEventListener("change", function () { + if (elQuoteDate && elTradingDay.value) elQuoteDate.value = elTradingDay.value; + void loadDailyTrades(); + }); + } if (elDateFrom) elDateFrom.addEventListener("change", loadDailyTrades); if (elDateTo) elDateTo.addEventListener("change", loadDailyTrades); [elFilterProfit, elFilterLoss, elFilterSick].forEach(function (el) { @@ -1967,6 +2125,18 @@ }); } if (elQuoteForm) elQuoteForm.addEventListener("submit", submitQuoteForm); + if (elQuoteDate) { + elQuoteDate.addEventListener("change", function () { + void loadQuoteDayTrades(); + }); + } + if (elContentTabs) { + elContentTabs.addEventListener("click", function (ev) { + const btn = ev.target.closest(".archive-content-tab"); + if (!btn) return; + setArchiveContentTab(btn.getAttribute("data-archive-tab") || "trades"); + }); + } if (elTfTabs) { elTfTabs.addEventListener("click", function (ev) { const btn = ev.target.closest(".archive-tf-btn"); @@ -1999,6 +2169,7 @@ syncPeriodUI(); syncTradesLayout(); bindEvents(); + setArchiveContentTab("trades"); inited = true; } await loadMeta(); diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 9993d51..0872bfc 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -15,7 +15,7 @@ - + @@ -489,19 +489,11 @@
- -
+
+ + +
+
交易日历
@@ -551,6 +543,27 @@
+
@@ -1316,7 +1329,7 @@ - +