/**
 * 内照明心:复盘语录 + 当日交易记录 + 按需 K 线.
 */
(function () {
  const page = document.getElementById("page-archive");
  if (!page) return;

  const elExchange = document.getElementById("archive-exchange");
  const elFilterProfit = document.getElementById("archive-filter-profit");
  const elFilterLoss = document.getElementById("archive-filter-loss");
  const elFilterSick = document.getElementById("archive-filter-sick");
  const elPeriodTabs = document.getElementById("archive-period-tabs");
  const elTradingDay = document.getElementById("archive-trading-day");
  const elPeriodRangeWrap = document.getElementById("archive-period-range-wrap");
  const elDateFrom = document.getElementById("archive-date-from");
  const elDateTo = document.getElementById("archive-date-to");
  const elSearch = document.getElementById("archive-search");
  const elBtnChartToggle = document.getElementById("archive-btn-chart-toggle");
  const elBtnRefresh = document.getElementById("archive-btn-refresh");
  const elBtnSync = document.getElementById("archive-btn-sync");
  const elStatus = document.getElementById("archive-status");
  const elStats = document.getElementById("archive-stats");
  const elStatsCharts = document.getElementById("archive-stats-charts");
  const elStatsVizSub = document.getElementById("archive-stats-viz-sub");
  const elCalSummarySub = document.getElementById("archive-cal-summary-sub");
  const elCalendarWrap = document.getElementById("archive-calendar-wrap");
  const elCalendar = document.getElementById("archive-calendar");
  const elCalTitle = document.getElementById("archive-cal-title");
  const elCalPrev = document.getElementById("archive-cal-prev");
  const elCalNext = document.getElementById("archive-cal-next");
  const elQuotesList = document.getElementById("archive-quotes-list");
  const elQuotesCount = document.getElementById("archive-quotes-count");
  const elQuoteForm = document.getElementById("archive-quote-form");
  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 elPanelViz = document.getElementById("archive-panel-viz");
  const elPanelCalendar = document.getElementById("archive-panel-calendar");
  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 elBtnChartClose = document.getElementById("archive-btn-chart-close");
  const elCalDayTradesMeta = document.getElementById("archive-calendar-day-trades-meta");
  const elCalDayTradesBody = document.getElementById("archive-calendar-day-trades-body");
  const elTfTabs = document.getElementById("archive-tf-tabs");
  const elViewMode = document.getElementById("archive-view-mode");
  const elJumpAt = document.getElementById("archive-jump-at");
  const elBtnJump = document.getElementById("archive-btn-jump");
  const elBtnReloadChart = document.getElementById("archive-btn-reload-chart");
  const elChartHost = document.getElementById("archive-chart");
  const elMarkAuto = document.getElementById("archive-mark-auto");
  const elTrades = document.getElementById("archive-trades");
  const elTradesSection = document.getElementById("archive-trades-section");
  const elTradesPager = document.getElementById("archive-trades-pager");
  const elTradesPrev = document.getElementById("archive-trades-prev");
  const elTradesNext = document.getElementById("archive-trades-next");
  const elTradesPageLabel = document.getElementById("archive-trades-page-label");
  const ARCHIVE_MARK_AUTO_KEY = "hubArchiveMarkAuto";
  const TRADES_PAGE_SIZE = 5;
  const TRADES_VISIBLE_ROWS_CHART_OPEN = 5;

  const TF_MS = {
    "5m": 5 * 60_000,
    "15m": 15 * 60_000,
    "1h": 60 * 60_000,
    "4h": 4 * 60 * 60_000,
  };
  const CHART_TZ_OFFSET_SEC = 8 * 60 * 60;

  let meta = null;
  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: {} };
  let periodMode = "today";
  let periodLabel = "";
  let dateFrom = "";
  let dateTo = "";
  let tradingDay = "";
  let selected = null;
  let trades = [];
  let selectedTradeKey = null;
  let timeframe = "15m";
  let chart = null;
  let candleSeries = null;
  let volumeSeries = null;
  let inited = false;
  let markAuto = true;
  let lastCandles = [];
  let chartExchangeSymbol = "";
  let chartMarketType = "swap";
  let searchTimer = null;
  let calendarWidget = null;
  let selectedCalendarDay = "";

  function esc(s) {
    return String(s == null ? "" : s)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  function loadMarkAutoPref() {
    try {
      const raw = localStorage.getItem(ARCHIVE_MARK_AUTO_KEY);
      if (raw === "0" || raw === "false") markAuto = false;
      else if (raw === "1" || raw === "true") markAuto = true;
    } catch (_) {}
    syncMarkAutoBtn();
  }

  function syncMarkAutoBtn() {
    if (!elMarkAuto) return;
    elMarkAuto.classList.toggle("is-on", markAuto);
    elMarkAuto.setAttribute("aria-pressed", markAuto ? "true" : "false");
  }

  function saveMarkAutoPref() {
    try {
      localStorage.setItem(ARCHIVE_MARK_AUTO_KEY, markAuto ? "1" : "0");
    } catch (_) {}
  }

  function tradeHistoryBounds(tradeList) {
    let minOpen = null;
    let maxClose = null;
    (tradeList || []).forEach(function (tr) {
      const o = tradeOpenMs(tr);
      const c = tradeCloseMs(tr);
      if (o != null) minOpen = minOpen == null ? o : Math.min(minOpen, o);
      if (c != null) maxClose = maxClose == null ? c : Math.max(maxClose, c);
    });
    return { minOpen: minOpen, maxClose: maxClose };
  }

  function fmt(n, d) {
    if (n == null || n === "" || !Number.isFinite(Number(n))) return "—";
    return Number(n).toFixed(d == null ? 2 : d);
  }

  function fmtPnl(v) {
    const n = Number(v);
    if (!Number.isFinite(n)) return "—";
    return (n >= 0 ? "+" : "") + n.toFixed(2);
  }

  function pad2(n) {
    return n < 10 ? "0" + n : String(n);
  }

  function utcSecToBjDate(utcSec) {
    return new Date((Number(utcSec) + CHART_TZ_OFFSET_SEC) * 1000);
  }

  function formatChartTimeBj(utcSec, withDate) {
    const d = utcSecToBjDate(utcSec);
    const h = pad2(d.getUTCHours());
    const mi = pad2(d.getUTCMinutes());
    if (!withDate) return h + ":" + mi;
    return (
      d.getUTCFullYear() +
      "-" +
      pad2(d.getUTCMonth() + 1) +
      "-" +
      pad2(d.getUTCDate()) +
      " " +
      h +
      ":" +
      mi
    );
  }

  function chartLocalizationBj() {
    return {
      locale: "zh-CN",
      dateFormat: "yyyy-MM-dd",
      timeFormatter: function (time) {
        if (typeof time === "number") return formatChartTimeBj(time, true);
        if (time && typeof time === "object" && time.year) {
          return time.year + "-" + pad2(time.month) + "-" + pad2(time.day);
        }
        return "";
      },
      tickMarkFormatter: function (time, tickMarkType) {
        if (typeof time !== "number") {
          if (time && typeof time === "object" && time.year) {
            return time.year + "-" + pad2(time.month) + "-" + pad2(time.day);
          }
          return "";
        }
        const d = utcSecToBjDate(time);
        if (tickMarkType === 0) return String(d.getUTCFullYear());
        if (tickMarkType === 1) return pad2(d.getUTCMonth() + 1);
        if (tickMarkType === 2) return pad2(d.getUTCDate());
        return formatChartTimeBj(time, false);
      },
    };
  }

  function fmtDt(raw) {
    if (raw == null || raw === "") return "—";
    return String(raw).replace("T", " ").slice(0, 16);
  }

  function fmtHoldMinutes(tr) {
    if (!tr) return "—";
    const text = tr.hold_minutes_text;
    if (text) return text;
    const n = Number(tr.hold_minutes);
    if (!Number.isFinite(n) || n <= 0) return "0分钟";
    const hours = Math.floor(n / 60);
    const mins = Math.floor(n % 60);
    if (hours) return hours + "小时" + mins + "分钟";
    return mins + "分钟";
  }

  const ENTRY_TYPE_LABELS = {
    trend_pullback: "趋势回调",
    roll: "顺势加仓",
    trend: "趋势回调",
  };

  function fmtEntryType(tr) {
    if (!tr) return "—";
    const raw = String(
      tr.entry_type || tr.entry_reason || tr.reviewed_entry_reason || ""
    ).trim();
    if (raw) return ENTRY_TYPE_LABELS[raw] || raw;
    const mt = String(tr.monitor_type || "").trim();
    if (mt && mt !== "下单监控") return ENTRY_TYPE_LABELS[mt] || mt;
    return mt || "—";
  }

  function reviewMark(tr) {
    return tr && tr.reviewed ? "复" : "";
  }

  function pnlClass(v) {
    const n = Number(v);
    if (!Number.isFinite(n) || Math.abs(n) < 1e-6) return "";
    return n > 0 ? "pos" : "neg";
  }

  function setStatus(text) {
    if (elStatus) elStatus.textContent = text || "";
  }

  function tradeRowExchange(tr) {
    if (!tr) return "—";
    const exKey = String(tr.exchange_key || "").toLowerCase();
    return exKey ? exchangeLabel(exKey) : "—";
  }

  function tradeRowKey(tr) {
    if (!tr) return "";
    const exKey = String(tr.exchange_key || "").toLowerCase();
    const tid = tr.trade_id != null ? tr.trade_id : tr.id;
    if (!exKey || tid == null || tid === "") return "";
    return exKey + ":" + String(tid);
  }

  function findTradeByKey(key) {
    if (!key) return null;
    return (
      dailyTrades.find(function (t) {
        return tradeRowKey(t) === String(key);
      }) || null
    );
  }

  function applyTagSelectStyle(sel) {
    if (!sel) return;
    const v = sel.value || "";
    sel.classList.remove("is-tag-empty", "is-tag-sick", "is-tag-emotion");
    if (v === "sick") sel.classList.add("is-tag-sick");
    else if (v === "emotion") sel.classList.add("is-tag-emotion");
    else sel.classList.add("is-tag-empty");
  }

  function exchangeLabel(exKey) {
    const key = String(exKey || "").toLowerCase();
    if (!key) return "—";
    const hit = (meta && meta.exchanges || []).find(function (ex) {
      return String(ex.key || "").toLowerCase() === key;
    });
    return hit ? hit.name || hit.key : exKey;
  }

  function scheduleChartResize() {
    requestAnimationFrame(function () {
      if (chart && elChartHost) {
        const w = elChartHost.clientWidth;
        const h = elChartHost.clientHeight;
        if (w > 0 && h > 0) chart.applyOptions({ width: w, height: h });
      }
      requestAnimationFrame(function () {
        if (chart && elChartHost) {
          const w = elChartHost.clientWidth;
          const h = elChartHost.clientHeight;
          if (w > 0 && h > 0) chart.applyOptions({ width: w, height: h });
        }
      });
    });
  }

  async function ensureChartSelection() {
    if (selected && selected.exchange_key && selected.symbol) return;
    if (!dailyTrades.length) return;
    const tr = dailyTrades.find(function (t) {
      return t.exchange_key && t.symbol;
    });
    if (!tr) return;
    selected = { exchange_key: tr.exchange_key, symbol: tr.symbol };
    selectedTradeKey = tradeRowKey(tr);
    await loadSymbolTradesForChart(tr.exchange_key, tr.symbol);
  }

  function isChartOpen() {
    return !!(elChartSection && !elChartSection.hidden);
  }

  function syncTradesLayout() {
    const open = isChartOpen();
    if (page) page.classList.toggle("is-chart-open", open);
    if (elTradesSection) elTradesSection.classList.toggle("is-chart-open", open);
    if (!elTrades) return;
    if (open) {
      const head = elTrades.querySelector("thead tr");
      const row = elTrades.querySelector("tbody tr");
      if (head && row) {
        const h = head.offsetHeight + row.offsetHeight * TRADES_VISIBLE_ROWS_CHART_OPEN;
        elTrades.style.maxHeight = h + "px";
      }
    } else {
      elTrades.style.maxHeight = "";
    }
  }

  function setChartOpen(on) {
    if (!elChartSection) return;
    const want = !!on;
    elChartSection.hidden = !want;
    if (elBtnChartToggle) {
      elBtnChartToggle.classList.toggle("is-active", want);
    }
    if (want && archiveContentTab !== "trades") {
      setArchiveContentTab("trades");
    }
    syncTradesLayout();
    if (!want) {
      destroyChart();
      return;
    }
    scheduleChartResize();
  }

  function formatChartContractLabel(sym, exchangeSymbol, marketType) {
    const base = String(sym || "—");
    const mt = String(marketType || "").toLowerCase();
    if (mt === "swap" || (exchangeSymbol && String(exchangeSymbol).indexOf(":") >= 0)) {
      return base + " 永续";
    }
    return base;
  }

  function updateChartTitle() {
    if (!elChartTitle) return;
    if (!selected) {
      elChartTitle.textContent = "—";
      return;
    }
    const label = formatChartContractLabel(
      selected.symbol,
      chartExchangeSymbol,
      chartMarketType
    );
    elChartTitle.textContent = label + " · " + exchangeLabel(selected.exchange_key);
  }

  async function apiFetch(url, opts) {
    const r = await fetch(url, opts);
    if (r.status === 401) {
      location.href = "/login?next=" + encodeURIComponent(location.pathname);
      throw new Error("未登录");
    }
    return r;
  }

  function syncPeriodUI() {
    if (elPeriodTabs) {
      elPeriodTabs.querySelectorAll(".archive-period-btn").forEach(function (btn) {
        btn.classList.toggle("is-active", btn.getAttribute("data-period") === periodMode);
      });
    }
    if (elTradingDay) {
      elTradingDay.classList.toggle("hidden", periodMode !== "today");
    }
    if (elPeriodRangeWrap) {
      elPeriodRangeWrap.classList.toggle("hidden", periodMode !== "range");
    }
  }

  function setPeriodMode(mode) {
    periodMode = mode || "today";
    syncPeriodUI();
  }

  function queryDailyParams() {
    const q = new URLSearchParams();
    q.set("period", periodMode);
    if (periodMode === "today" && elTradingDay && elTradingDay.value) {
      q.set("trading_day", elTradingDay.value);
    }
    if (periodMode === "range") {
      if (elDateFrom && elDateFrom.value) q.set("date_from", elDateFrom.value);
      if (elDateTo && elDateTo.value) q.set("date_to", elDateTo.value);
    }
    const ex = (elExchange && elExchange.value) || "";
    if (ex) q.set("exchange_key", ex);
    if (elFilterProfit && elFilterProfit.checked) q.set("filter_profit", "1");
    if (elFilterLoss && elFilterLoss.checked) q.set("filter_loss", "1");
    if (elFilterSick && elFilterSick.checked) q.set("filter_sick", "1");
    if (elSearch && elSearch.value.trim()) q.set("search", elSearch.value.trim());
    return q.toString();
  }

  function fmtVolStat(v) {
    const n = Number(v);
    if (!Number.isFinite(n) || n <= 0) return "—";
    if (n >= 10000) return (n / 1000).toFixed(1) + "k";
    return n.toFixed(0) + "U";
  }

  function fmtFeeStat(v) {
    const n = Number(v);
    if (!Number.isFinite(n) || n <= 0) return "—";
    return n.toFixed(2) + "U";
  }

  function fmtPnlStat(v) {
    const n = Number(v);
    if (!Number.isFinite(n)) return "—";
    const cls = n >= 0 ? "pnl-pos" : "pnl-neg";
    const text = (n >= 0 ? "+" : "") + n.toFixed(2) + "U";
    return '<span class="' + cls + '">' + text + "</span>";
  }

  function renderExchangeOptions() {
    if (!elExchange || !meta) return;
    const cur = elExchange.value;
    elExchange.innerHTML = '<option value="">全部</option>';
    (meta.exchanges || []).forEach(function (ex) {
      const opt = document.createElement("option");
      opt.value = ex.key || "";
      opt.textContent = (ex.name || ex.key || "") + " (" + (ex.key || "") + ")";
      elExchange.appendChild(opt);
    });
    if (cur) elExchange.value = cur;
  }

  function fmtPnlStatOptional(v) {
    if (v == null || v === "") return "—";
    return fmtPnlStat(v);
  }

  function fmtWinRate(v, openN, winN) {
    if (v != null && v !== "") return Number(v).toFixed(1) + "%";
    if (openN) return (Math.round(((winN || 0) / openN) * 1000) / 10) + "%";
    return "—";
  }

  function fmtProfitLossRatio(v) {
    if (v == null || v === "") return "—";
    const n = Number(v);
    if (!Number.isFinite(n)) return "—";
    return n.toFixed(2) + ":1";
  }

  function renderStatsRow(label, e, isTotal) {
    const openN = e.open_count || 0;
    const sickN = e.sick_count || 0;
    const sickShare = e.sick_pct != null ? e.sick_pct : openN ? Math.round((sickN / openN) * 1000) / 10 : 0;
    const rowCls = isTotal ? ' class="archive-stats-total"' : "";
    return (
      "<tr" +
      rowCls +
      "><td>" +
      (isTotal ? "<strong>" + esc(label) + "</strong>" : esc(label)) +
      "</td><td>" +
      openN +
      "</td><td>" +
      (e.win_count || 0) +
      "</td><td>" +
      (e.loss_count || 0) +
      "</td><td>" +
      fmtWinRate(e.win_rate, openN, e.win_count) +
      "</td><td>" +
      fmtPnlStatOptional(e.avg_win) +
      "</td><td>" +
      fmtPnlStatOptional(e.avg_loss) +
      "</td><td>" +
      fmtProfitLossRatio(e.profit_loss_ratio) +
      "</td><td>" +
      fmtPnlStatOptional(e.max_win) +
      "</td><td>" +
      fmtPnlStatOptional(e.max_loss) +
      "</td><td>" +
      sickN +
      "</td><td>" +
      sickShare +
      "%</td><td>" +
      fmtPnlStat(e.pnl_total) +
      "</td><td>" +
      fmtPnlStat(e.pnl_ex_sick) +
      "</td><td>" +
      fmtVolStat(e.turnover_total) +
      "</td><td>" +
      fmtFeeStat(e.commission_total) +
      "</td></tr>"
    );
  }

  function calendarRefDate() {
    let ref = tradingDay || (elTradingDay && elTradingDay.value) || "";
    if (!ref && dateFrom) ref = dateFrom;
    return ref || new Date();
  }

  function ensureCalendarWidget() {
    if (calendarWidget || !window.TradeStatsCalendar || !elCalendar) return calendarWidget;
    calendarWidget = new TradeStatsCalendar({
      gridEl: elCalendar,
      titleEl: elCalTitle,
      prevBtn: elCalPrev,
      nextBtn: elCalNext,
      showSick: true,
      buildQuery: function (year, month) {
        const q = new URLSearchParams();
        q.set("year", String(year));
        q.set("month", String(month));
        const ex = (elExchange && elExchange.value) || "";
        if (ex) q.set("exchange_key", ex);
        return q;
      },
      fetchFn: async function (q) {
        const r = await apiFetch("/api/archive/calendar?" + q.toString());
        return r.json();
      },
      parseResponse: function (data) {
        if (!data || !data.ok) return {};
        return data.days || {};
      },
      onDayClick: function (day) {
        selectedCalendarDay = day;
        setPeriodMode("today");
        if (elTradingDay) elTradingDay.value = day;
        if (elQuoteDate) elQuoteDate.value = day;
        if (elFilterSick) elFilterSick.checked = false;
        syncPeriodUI();
        void loadDailyTrades();
      },
    });
    calendarWidget.ensureMonth(calendarRefDate());
    return calendarWidget;
  }

  async function loadCalendar() {
    const cal = ensureCalendarWidget();
    if (!cal) return;
    cal.selectedDay = selectedCalendarDay;
    await cal.load();
    if (elCalSummarySub && cal.monthPnlTotal != null) {
      const pnl = Number(cal.monthPnlTotal) || 0;
      const sign = pnl > 0 ? "+" : "";
      elCalSummarySub.textContent = cal.year + "年" + cal.month + "月 " + sign + pnl.toFixed(2) + "U";
    }
  }

  function renderStats() {
    if (!elStats) return;
    const st = dailyStats || { open_count: 0, by_exchange: {} };
    const label = periodLabel || "本日";
    const byEx = st.by_exchange || {};
    const exKeys = Object.keys(byEx).sort();
    let rows =
      renderStatsRow(
        label,
        {
          open_count: st.open_count,
          sick_count: st.sick_count,
          sick_pct: st.sick_pct,
          pnl_total: st.pnl_total,
          pnl_ex_sick: st.pnl_ex_sick,
          win_count: st.win_count,
          loss_count: st.loss_count,
          avg_win: st.avg_win,
          avg_loss: st.avg_loss,
          win_rate: st.win_rate,
          profit_loss_ratio: st.profit_loss_ratio,
          max_win: st.max_win,
          max_loss: st.max_loss,
          turnover_total: st.turnover_total,
          commission_total: st.commission_total,
        },
        true
      ) +
      exKeys
        .map(function (ex) {
          return renderStatsRow(exchangeLabel(ex), byEx[ex] || {}, false);
        })
        .join("");
    elStats.innerHTML =
      '<table class="archive-stats-table"><thead><tr>' +
      "<th>范围</th><th>开仓</th><th>盈利单</th><th>亏损单</th><th>胜率</th><th>平均盈利</th><th>平均亏损</th><th>盈亏比</th><th>最大盈利</th><th>最大亏损</th><th>犯病</th><th>犯病占比</th><th>盈亏</th><th>剔除犯病盈亏</th><th>成交额</th><th>手续费</th>" +
      "</tr></thead><tbody>" +
      rows +
      "</tbody></table>";
    renderStatsCharts();
  }

  function fmtDurationMinutes(minutes) {
    if (minutes == null || minutes === "" || Number.isNaN(Number(minutes))) return "—";
    let m = Math.max(0, Math.round(Number(minutes)));
    if (m < 60) return m + "分";
    const h = Math.floor(m / 60);
    const rm = m % 60;
    if (h < 24) return rm ? h + "时" + rm + "分" : h + "时";
    const d = Math.floor(h / 24);
    const rh = h % 24;
    return rh ? d + "天" + rh + "时" : d + "天";
  }

  function sumTradePnlSides(trades) {
    let profit = 0;
    let loss = 0;
    (trades || []).forEach(function (t) {
      const pnl = Number(t.pnl_amount);
      if (!Number.isFinite(pnl)) return;
      if (pnl > 0.0001) profit += pnl;
      else if (pnl < -0.0001) loss += Math.abs(pnl);
    });
    return { profit: profit, loss: loss };
  }

  function avgHoldMinutes(trades, side) {
    const vals = [];
    (trades || []).forEach(function (t) {
      const pnl = Number(t.pnl_amount);
      const hold = Number(t.hold_minutes);
      if (!Number.isFinite(hold) || hold < 0) return;
      if (side === "win" && pnl > 0.0001) vals.push(hold);
      if (side === "loss" && pnl < -0.0001) vals.push(hold);
    });
    if (!vals.length) return null;
    return Math.round(vals.reduce(function (a, b) { return a + b; }, 0) / vals.length);
  }

  function buildCumulativeSeries(trades) {
    const byDay = {};
    (trades || []).forEach(function (t) {
      const raw = t.closed_at || "";
      const day = String(raw).slice(0, 10);
      if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) return;
      byDay[day] = (byDay[day] || 0) + Number(t.pnl_amount || 0);
    });
    const days = Object.keys(byDay).sort();
    let cum = 0;
    return days.map(function (day) {
      cum += byDay[day];
      return { day: day, pnl: byDay[day], cum: cum };
    });
  }

  function renderCumulativeChart(series) {
    if (!series.length) {
      return '<p class="archive-viz-empty muted">当前区间暂无平仓数据</p>';
    }
    const w = 320;
    const h = 96;
    const padL = 6;
    const padR = 6;
    const padT = 10;
    const padB = 10;
    const vals = series.map(function (s) { return s.cum; });
    const rawMin = Math.min.apply(null, vals);
    const rawMax = Math.max.apply(null, vals);
    let minV = Math.min(0, rawMin);
    let maxV = Math.max(0, rawMax);
    const span = maxV - minV || Math.max(Math.abs(rawMax), Math.abs(rawMin), 1);
    const yPad = span * 0.14;
    minV -= yPad;
    maxV += yPad;
    const range = maxV - minV || 1;
    const innerW = w - padL - padR;
    const innerH = h - padT - padB;
    const yOf = function (v) {
      return padT + innerH - ((v - minV) / range) * innerH;
    };
    const zeroY = yOf(0);
    const showZero = rawMin < -0.0001 || rawMax > 0.0001;
    const pts = series.map(function (s, i) {
      const x = padL + (i / Math.max(series.length - 1, 1)) * innerW;
      const y = yOf(s.cum);
      return { x: x, y: y, day: s.day, pnl: s.pnl, cum: s.cum };
    });
    const linePts = pts.map(function (p) { return p.x.toFixed(1) + "," + p.y.toFixed(1); }).join(" ");
    const last = series[series.length - 1];
    const lastCls = last.cum >= 0 ? "pnl-pos" : "pnl-neg";
    const lineCls = last.cum >= 0 ? "archive-cum-line--up" : "archive-cum-line--down";
    const sign = last.cum > 0 ? "+" : "";
    const fmtAxis = function (v) {
      const a = Math.abs(v);
      if (a >= 100) return (v > 0 ? "+" : "") + v.toFixed(0) + "U";
      if (a >= 10) return (v > 0 ? "+" : "") + v.toFixed(1) + "U";
      return (v > 0 ? "+" : "") + v.toFixed(2) + "U";
    };
    const yTop = fmtAxis(maxV);
    const yMid = showZero ? "0" : fmtAxis((maxV + minV) / 2);
    const yBot = fmtAxis(minV);
    const gridLines = [maxV, showZero ? 0 : null, minV]
      .filter(function (v, i, arr) {
        return v != null && arr.indexOf(v) === i;
      })
      .map(function (v) {
        const y = yOf(v).toFixed(1);
        const cls = v === 0 ? "archive-cum-grid archive-cum-grid--zero" : "archive-cum-grid";
        return '<line x1="' + padL + '" y1="' + y + '" x2="' + (w - padR) + '" y2="' + y + '" class="' + cls + '" />';
      })
      .join("");
    const dots = pts
      .map(function (p, i) {
        const isLast = i === pts.length - 1;
        const dotCls = p.cum >= 0 ? "archive-cum-dot--up" : "archive-cum-dot--down";
        const r = isLast ? 4.2 : 2.6;
        const dayPnl = p.pnl > 0 ? "+" + p.pnl.toFixed(2) : p.pnl.toFixed(2);
        const cumPnl = p.cum > 0 ? "+" + p.cum.toFixed(2) : p.cum.toFixed(2);
        return (
          '<circle cx="' + p.x.toFixed(1) + '" cy="' + p.y.toFixed(1) + '" r="' + r + '" class="archive-cum-dot ' + dotCls + (isLast ? " archive-cum-dot--last" : "") + '">' +
          '<title>' + esc(p.day.slice(5)) + " 当日" + dayPnl + "U · 累计" + cumPnl + "U</title>" +
          "</circle>"
        );
      })
      .join("");
    const dayCount = series.length;
    const peak = fmtAxis(rawMax);
    const trough = fmtAxis(rawMin);
    const xStart = esc(series[0].day.slice(5));
    const xEnd = esc(series[series.length - 1].day.slice(5));
    return (
      '<div class="archive-cum-wrap">' +
      '<div class="archive-cum-head">' +
      '<span class="archive-viz-block-title">累计盈亏</span>' +
      '<span class="archive-cum-end ' + lastCls + '">' + sign + last.cum.toFixed(2) + "U</span>" +
      "</div>" +
      '<div class="archive-cum-body">' +
      '<div class="archive-cum-yaxis" aria-hidden="true">' +
      '<span>' + esc(yTop) + "</span>" +
      '<span>' + esc(yMid) + "</span>" +
      '<span>' + esc(yBot) + "</span>" +
      "</div>" +
      '<div class="archive-cum-plot">' +
      '<svg class="archive-cum-chart" viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="xMidYMid meet" aria-hidden="true">' +
      gridLines +
      '<polyline points="' + linePts + '" class="archive-cum-line ' + lineCls + '" vector-effect="non-scaling-stroke" />' +
      dots +
      "</svg>" +
      '<div class="archive-cum-xaxis" aria-hidden="true">' +
      "<span>" + xStart + "</span>" +
      (series.length > 1 ? "<span>" + xEnd + "</span>" : "") +
      "</div>" +
      "</div>" +
      "</div>" +
      '<div class="archive-cum-foot muted">' +
      dayCount + " 个交易日 · 区间高 " + esc(peak) + " · 低 " + esc(trough) +
      "</div>" +
      "</div>"
    );
  }

  function barRow(label, valueLabel, pct, fillCls) {
    const w = Math.max(0, Math.min(100, pct));
    return (
      '<div class="archive-viz-bar-row">' +
      '<span class="k" title="' + esc(label) + '">' + esc(label) + "</span>" +
      '<div class="archive-viz-bar-track"><div class="archive-viz-bar-fill ' + fillCls + '" style="width:' + w.toFixed(1) + '%"></div></div>' +
      '<span class="v">' + esc(valueLabel) + "</span>" +
      "</div>"
    );
  }

  function stackedPnlBar(profit, loss) {
    const total = profit + loss;
    if (total <= 0) {
      return '<p class="archive-viz-empty muted">暂无盈亏数据</p>';
    }
    const profitPct = (profit / total) * 100;
    const lossPct = 100 - profitPct;
    const net = profit - loss;
    const netCls = net >= 0 ? "pnl-pos" : "pnl-neg";
    const netSign = net > 0 ? "+" : "";
    return (
      '<div class="archive-viz-stacked">' +
      '<div class="archive-viz-stacked-seg archive-viz-stacked-seg--profit" style="width:' + profitPct.toFixed(1) + '%" title="总盈利 ' + profit.toFixed(2) + 'U">' +
      (profitPct >= 18 ? profit.toFixed(2) + "U" : "") +
      "</div>" +
      '<div class="archive-viz-stacked-seg archive-viz-stacked-seg--loss" style="width:' + lossPct.toFixed(1) + '%" title="总亏损 ' + loss.toFixed(2) + 'U">' +
      (lossPct >= 18 ? loss.toFixed(2) + "U" : "") +
      "</div>" +
      "</div>" +
      '<div class="archive-viz-stacked-meta">' +
      '<span class="archive-viz-legend archive-viz-legend--profit">盈利 ' + profit.toFixed(2) + "U</span>" +
      '<span class="archive-viz-legend archive-viz-legend--loss">亏损 ' + loss.toFixed(2) + "U</span>" +
      '<span class="archive-viz-legend archive-viz-legend--net ' + netCls + '">净 ' + netSign + net.toFixed(2) + "U</span>" +
      "</div>"
    );
  }

  function divergingBarRow(label, pnl, maxAbs) {
    const absPct = (Math.abs(pnl) / maxAbs) * 50;
    const cls = pnl >= 0 ? "archive-viz-div-fill--profit" : "archive-viz-div-fill--loss";
    const vCls = pnl >= 0 ? "pnl-pos" : "pnl-neg";
    const sign = pnl > 0 ? "+" : "";
    const style =
      pnl >= 0
        ? "left:50%;width:" + absPct.toFixed(1) + "%"
        : "right:50%;width:" + absPct.toFixed(1) + "%";
    return (
      '<div class="archive-viz-div-row">' +
      '<span class="k" title="' + esc(label) + '">' + esc(label) + "</span>" +
      '<div class="archive-viz-div-track"><div class="archive-viz-div-mid"></div><div class="archive-viz-div-fill ' + cls + '" style="' + style + '"></div></div>' +
      '<span class="v ' + vCls + '">' + sign + pnl.toFixed(2) + "U</span>" +
      "</div>"
    );
  }

  function renderStatsCharts() {
    if (!elStatsCharts) return;
    const st = dailyStats || { open_count: 0, by_exchange: {} };
    const openN = st.open_count || 0;
    const winN = st.win_count || 0;
    const lossN = st.loss_count || 0;
    const winRate = openN ? Number(st.win_rate) || 0 : 0;
    const sides = sumTradePnlSides(dailyTrades);
    const pnlTotal = sides.profit + sides.loss;
    const netPnl = Number(st.pnl_total) || 0;
    const sickN = st.sick_count || 0;
    const sickPct = openN ? (sickN / openN) * 100 : 0;
    const winHold = avgHoldMinutes(dailyTrades, "win");
    const lossHold = avgHoldMinutes(dailyTrades, "loss");
    const holdMax = Math.max(winHold || 0, lossHold || 0);
    const byEx = st.by_exchange || {};
    const exKeys = Object.keys(byEx).sort();

    if (elStatsVizSub) {
      const sign = netPnl > 0 ? "+" : "";
      elStatsVizSub.textContent = openN
        ? "胜率 " + winRate.toFixed(0) + "% · " + sign + netPnl.toFixed(2) + "U"
        : "暂无平仓";
    }

    if (!openN) {
      elStatsCharts.innerHTML = '<p class="archive-viz-empty muted">当前区间暂无平仓数据</p>';
      return;
    }

    const netCls = netPnl >= 0 ? "pnl-pos" : "pnl-neg";
    const netSign = netPnl > 0 ? "+" : "";

    let exBars = "";
    if (exKeys.length) {
      const maxAbs = Math.max.apply(
        null,
        exKeys.map(function (ex) {
          return Math.abs(Number(byEx[ex].pnl_total) || 0);
        }).concat([0.0001])
      );
      exBars = exKeys
        .map(function (ex) {
          const pnl = Number(byEx[ex].pnl_total) || 0;
          return divergingBarRow(exchangeLabel(ex), pnl, maxAbs);
        })
        .join("");
    } else {
      exBars = '<p class="archive-viz-empty muted">暂无分策略数据</p>';
    }

    const cumSeries = buildCumulativeSeries(dailyTrades);
    elStatsCharts.innerHTML =
      '<div class="archive-viz-kpis">' +
      '<div class="archive-viz-kpi archive-viz-kpi--pnl">' +
      '<span class="archive-viz-kpi-val ' + netCls + '">' + netSign + netPnl.toFixed(2) + "U</span>" +
      '<span class="archive-viz-kpi-lbl">净盈亏</span>' +
      "</div>" +
      '<div class="archive-viz-kpi archive-viz-kpi--win">' +
      '<div class="archive-viz-ring" style="--win-pct:' + winRate.toFixed(1) + '">' +
      '<span class="archive-viz-ring-label">' + winRate.toFixed(0) + "%</span>" +
      "</div>" +
      '<span class="archive-viz-kpi-lbl">' + winN + "胜 " + lossN + "负</span>" +
      "</div>" +
      '<div class="archive-viz-kpi archive-viz-kpi--sick">' +
      '<span class="archive-viz-kpi-val">' + sickN + " 笔</span>" +
      '<span class="archive-viz-kpi-lbl">犯病 ' + sickPct.toFixed(0) + '%</span>' +
      "</div>" +
      "</div>" +
      '<div class="archive-viz-block">' +
      '<div class="archive-viz-block-title">盈亏构成</div>' +
      (pnlTotal > 0 ? stackedPnlBar(sides.profit, sides.loss) : '<p class="archive-viz-empty muted">暂无盈亏数据</p>') +
      "</div>" +
      '<div class="archive-viz-block archive-viz-block--exchange">' +
      '<div class="archive-viz-block-title">分策略盈亏</div>' +
      exBars +
      "</div>" +
      (holdMax > 0
        ? '<div class="archive-viz-block archive-viz-block--hold">' +
          '<div class="archive-viz-block-title">持仓时长对比</div>' +
          '<div class="archive-viz-hold-grid">' +
          '<div class="archive-viz-hold-card archive-viz-hold-card--win">' +
          '<span class="archive-viz-hold-val">' + esc(fmtDurationMinutes(winHold)) + "</span>" +
          '<span class="archive-viz-hold-lbl">盈单均持仓</span>' +
          '<div class="archive-viz-bar-track"><div class="archive-viz-bar-fill archive-viz-bar-fill--profit" style="width:' + (((winHold || 0) / holdMax) * 100).toFixed(1) + '%"></div></div>' +
          "</div>" +
          '<div class="archive-viz-hold-card archive-viz-hold-card--loss">' +
          '<span class="archive-viz-hold-val">' + esc(fmtDurationMinutes(lossHold)) + "</span>" +
          '<span class="archive-viz-hold-lbl">亏单均持仓</span>' +
          '<div class="archive-viz-bar-track"><div class="archive-viz-bar-fill archive-viz-bar-fill--loss" style="width:' + (((lossHold || 0) / holdMax) * 100).toFixed(1) + '%"></div></div>' +
          "</div>" +
          "</div></div>"
        : "") +
      '<div class="archive-viz-block archive-viz-block--cum">' +
      renderCumulativeChart(cumSeries) +
      "</div>";
  }

  function quotePreview(text) {
    const s = String(text || "").replace(/\s+/g, " ").trim();
    if (!s) return "(空)";
    return s.length > 36 ? s.slice(0, 36) + "…" : s;
  }

  function findQuote(id) {
    if (id == null || id === "") return null;
    return (
      quotes.find(function (q) {
        return String(q.id) === String(id);
      }) || null
    );
  }

  function updateQuoteSubmitBtn() {
    if (!elQuoteSubmit) return;
    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 allowed = { viz: 1, calendar: 1, trades: 1, quotes: 1 };
    const next = allowed[tab] ? tab : "trades";
    archiveContentTab = next;
    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");
      });
    }
    const panels = [
      [elPanelViz, "viz"],
      [elPanelCalendar, "calendar"],
      [elPanelTrades, "trades"],
      [elPanelQuotes, "quotes"],
    ];
    panels.forEach(function (pair) {
      const el = pair[0];
      const key = pair[1];
      if (!el) return;
      const on = key === next;
      el.classList.toggle("is-active", on);
      el.hidden = !on;
    });
    if (next === "quotes") {
      syncQuoteDateFromTradingDay(false);
      void loadQuoteDayTrades();
    } else if (next === "calendar") {
      void loadCalendar();
      renderCalendarDayTrades();
    } else if (next === "trades") {
      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 =
        '<p class="archive-empty">选择日期后自动带上该日已平仓记录.</p>';
      return;
    }
    if (!quoteDayTrades.length) {
      elQuoteDayTradesBody.innerHTML =
        '<p class="archive-empty">该日暂无已平仓记录.</p>';
      return;
    }
    elQuoteDayTradesBody.innerHTML =
      '<table class="archive-quote-day-trades-table"><thead><tr>' +
      "<th>交易所</th><th>合约</th><th>方向</th><th>开仓</th><th>平仓</th><th>盈亏</th><th>标签</th>" +
      "</tr></thead><tbody>" +
      quoteDayTrades
        .map(function (t) {
          return (
            "<tr>" +
            "<td>" +
            esc(tradeRowExchange(t)) +
            "</td>" +
            "<td>" +
            esc(t.symbol || "—") +
            "</td>" +
            "<td>" +
            esc(t.direction || "—") +
            "</td>" +
            '<td class="archive-dt">' +
            fmtDt(t.opened_at) +
            "</td>" +
            '<td class="archive-dt">' +
            fmtDt(t.closed_at) +
            "</td>" +
            '<td class="' +
            pnlClass(t.pnl_amount) +
            '">' +
            fmtPnl(t.pnl_amount) +
            "</td>" +
            "<td>" +
            esc(quoteTagLabel(t)) +
            "</td></tr>"
          );
        })
        .join("") +
      "</tbody></table>";
  }

  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 = '<p class="archive-empty">加载当日已平仓…</p>';
    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 =
          '<p class="archive-empty">' + esc(j.detail || "加载失败") + "</p>";
        return;
      }
      quoteDayTrades = j.trades || [];
      quoteDayTradesDay = day;
      renderQuoteDayTrades();
    } catch (_) {
      if (req !== quoteDayTradesReq) return;
      quoteDayTrades = [];
      quoteDayTradesDay = day;
      elQuoteDayTradesBody.innerHTML =
        '<p class="archive-empty">加载失败</p>';
    }
  }

  function resetQuoteForm() {
    editingQuoteId = null;
    if (elQuoteContent) elQuoteContent.value = "";
    updateQuoteSubmitBtn();
  }

  function startEditQuote() {
    const q = findQuote(selectedQuoteId);
    if (!q) return;
    editingQuoteId = q.id;
    if (elQuoteDate) elQuoteDate.value = q.quote_date || "";
    if (elQuoteContent) {
      elQuoteContent.value = q.content || "";
      elQuoteContent.focus();
    }
    updateQuoteSubmitBtn();
    void loadQuoteDayTrades();
  }

  function selectQuote(id) {
    const nextId = String(id);
    const same = selectedQuoteId != null && String(selectedQuoteId) === nextId;
    if (editingQuoteId != null && String(editingQuoteId) !== nextId) {
      resetQuoteForm();
    }
    selectedQuoteId = same ? null : id;
    renderQuotes();
  }

  function renderQuotes() {
    if (!elQuotesList) return;
    if (elQuotesCount) {
      elQuotesCount.textContent = quotes.length ? quotes.length + " 条" : "";
    }
    if (!quotes.length) {
      elQuotesList.innerHTML = '<p class="archive-empty">暂无复盘语录,可在上方添加.</p>';
      return;
    }
    elQuotesList.innerHTML = quotes
      .map(function (q) {
        const selected = String(q.id) === String(selectedQuoteId);
        return (
          '<div class="archive-quote-block' +
          (selected ? " is-open" : "") +
          '">' +
          '<button type="button" class="archive-quote-item' +
          (selected ? " is-selected" : "") +
          '" data-id="' +
          q.id +
          '">' +
          '<span class="archive-quote-date">' +
          esc(q.quote_date) +
          "</span>" +
          '<span class="archive-quote-preview">' +
          esc(quotePreview(q.content)) +
          "</span>" +
          (selected ? "" : '<span class="archive-quote-open-hint">查看</span>') +
          "</button>" +
          (selected
            ? '<div class="archive-quote-detail">' +
              '<div class="archive-quote-full">' +
              esc(q.content || "(空)") +
              "</div>" +
              '<div class="archive-quote-actions">' +
              '<button type="button" class="ghost archive-quote-edit-btn" data-id="' +
              q.id +
              '">修改</button>' +
              '<button type="button" class="archive-del-btn archive-quote-del-btn" data-id="' +
              q.id +
              '">删除</button>' +
              '<button type="button" class="ghost archive-quote-ai-btn" data-id="' +
              q.id +
              '">AI对话</button>' +
              "</div></div>"
            : "") +
          "</div>"
        );
      })
      .join("");

    elQuotesList.querySelectorAll(".archive-quote-item").forEach(function (btn) {
      btn.addEventListener("click", function () {
        selectQuote(btn.getAttribute("data-id"));
      });
    });
    elQuotesList.querySelectorAll(".archive-quote-edit-btn").forEach(function (btn) {
      btn.addEventListener("click", function (ev) {
        ev.stopPropagation();
        selectedQuoteId = btn.getAttribute("data-id");
        startEditQuote();
      });
    });
    elQuotesList.querySelectorAll(".archive-quote-del-btn").forEach(function (btn) {
      btn.addEventListener("click", function (ev) {
        ev.stopPropagation();
        void deleteQuote(btn.getAttribute("data-id"));
      });
    });
    elQuotesList.querySelectorAll(".archive-quote-ai-btn").forEach(function (btn) {
      btn.addEventListener("click", function (ev) {
        ev.stopPropagation();
        startQuoteAiChat(btn.getAttribute("data-id"));
      });
    });
  }

  async function loadQuotes() {
    const r = await apiFetch("/api/archive/quotes");
    const j = await r.json();
    quotes = j.quotes || [];
    if (!findQuote(selectedQuoteId)) {
      selectedQuoteId = null;
    }
    renderQuotes();
  }

  async function submitQuoteForm(ev) {
    if (ev) ev.preventDefault();
    const date = elQuoteDate && elQuoteDate.value;
    const content = elQuoteContent && elQuoteContent.value.trim();
    if (!date || !content) return;
    if (editingQuoteId) {
      await saveQuote(editingQuoteId, date, content);
      return;
    }
    const r = await apiFetch("/api/archive/quotes", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ quote_date: date, content: content }),
    });
    const j = await r.json();
    if (!r.ok) {
      setStatus(j.detail || "添加失败");
      return;
    }
    resetQuoteForm();
    selectedQuoteId = null;
    await loadQuotes();
    setStatus("语录已添加");
  }

  async function saveQuote(id, quoteDate, content) {
    const r = await apiFetch("/api/archive/quotes/" + id, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ quote_date: String(quoteDate || "").trim(), content: content }),
    });
    const j = await r.json();
    if (!r.ok) {
      setStatus(j.detail || "保存失败");
      return;
    }
    resetQuoteForm();
    selectedQuoteId = null;
    await loadQuotes();
    setStatus("语录已保存");
  }

  const ARCHIVE_QUOTE_AI_KEY = "hub_archive_quote_ai";

  function startQuoteAiChat(quoteId) {
    const q = findQuote(quoteId);
    const content = q && String(q.content || "").trim();
    if (!q || !content) {
      setStatus("语录内容为空,无法发起 AI 对话");
      return;
    }
    try {
      sessionStorage.setItem(
        ARCHIVE_QUOTE_AI_KEY,
        JSON.stringify({
          quote_date: q.quote_date || "",
          content: content,
        })
      );
    } catch (_) {
      setStatus("无法保存跳转数据");
      return;
    }
    if (typeof window.hubNavigateTo === "function") {
      window.hubNavigateTo("/ai");
      return;
    }
    location.href = "/ai";
  }

  async function deleteQuote(id) {
    if (!id || !window.confirm("确定删除这条复盘语录?")) return;
    const r = await apiFetch("/api/archive/quotes/" + id, { method: "DELETE" });
    if (!r.ok) {
      const j = await r.json().catch(function () {
        return {};
      });
      setStatus(j.detail || "删除失败");
      return;
    }
    if (String(id) === String(editingQuoteId)) resetQuoteForm();
    if (String(id) === String(selectedQuoteId)) selectedQuoteId = null;
    await loadQuotes();
    setStatus("语录已删除");
  }

  function pickAnchorTrade() {
    if (!trades.length) return null;
    if (selectedTradeKey) {
      const hit = trades.find(function (t) {
        return tradeRowKey(t) === selectedTradeKey;
      });
      if (hit) return hit;
    }
    return trades[0];
  }

  function parseTimeMs(raw) {
    if (raw == null || raw === "") return null;
    if (typeof raw === "number" && Number.isFinite(raw)) {
      const v = Math.trunc(raw);
      return v > 1e12 ? v : v * 1000;
    }
    const s = String(raw).trim().replace("Z", "").replace("T", " ");
    if (!s) return null;
    const m = s.match(/^(\d{4})-(\d{2})-(\d{2})(?: (\d{2}):(\d{2})(?::(\d{2}))?)?/);
    if (!m) return null;
    const ms =
      Date.UTC(
        Number(m[1]),
        Number(m[2]) - 1,
        Number(m[3]),
        Number(m[4] || 0),
        Number(m[5] || 0),
        Number(m[6] || 0)
      ) -
      CHART_TZ_OFFSET_SEC * 1000;
    return Number.isFinite(ms) ? ms : null;
  }

  function tradeOpenMs(tr) {
    if (!tr) return null;
    return tr.opened_at_ms || parseTimeMs(tr.opened_at);
  }

  function tradeCloseMs(tr) {
    if (!tr) return null;
    return tr.closed_at_ms || parseTimeMs(tr.closed_at);
  }

  function anchorMsForTrade(tr) {
    if (!tr) return null;
    const mode = (elViewMode && elViewMode.value) || "hold";
    if (mode === "entry") return tradeOpenMs(tr);
    return tradeCloseMs(tr) || tradeOpenMs(tr);
  }

  function msToBarTime(ms, tf) {
    const period = TF_MS[tf] || TF_MS["15m"];
    const aligned = Math.floor(Number(ms) / period) * period;
    return Math.floor(aligned / 1000);
  }

  function snapToCandleTime(targetSec, candles) {
    if (!candles || !candles.length) return targetSec;
    let best = candles[0].time;
    let bestDiff = Math.abs(candles[0].time - targetSec);
    for (let i = 0; i < candles.length; i++) {
      const d = Math.abs(candles[i].time - targetSec);
      if (d < bestDiff) {
        bestDiff = d;
        best = candles[i].time;
      }
    }
    return best;
  }

  const OPEN_ARROW_LONG = "#22c55e";
  const OPEN_ARROW_SHORT = "#ef4444";
  const OPEN_ARROW_LONG_HI = "#4ade80";
  const OPEN_ARROW_SHORT_HI = "#f87171";

  function isLongDirection(dir) {
    const d = String(dir || "").trim().toLowerCase();
    if (d === "short" || d === "空" || d === "sell" || d === "做空" || d === "shorts") return false;
    if (d === "long" || d === "多" || d === "buy" || d === "做多" || d === "longs") return true;
    return true;
  }

  function openArrowColor(long, highlight) {
    if (long) return highlight ? OPEN_ARROW_LONG_HI : OPEN_ARROW_LONG;
    return highlight ? OPEN_ARROW_SHORT_HI : OPEN_ARROW_SHORT;
  }

  function buildTradeMarkers(tr, candles, tf, opts) {
    if (!tr || !candles.length) return [];
    const options = opts || {};
    const suffix = options.labelSuffix ? String(options.labelSuffix) : "";
    const highlight = !!options.highlight;
    const long = isLongDirection(tr.direction);
    const openMs = tradeOpenMs(tr);
    const closeMs = tradeCloseMs(tr);
    const openColor = openArrowColor(long, highlight);
    let closeColor = highlight ? "#fbbf24" : "#f59e0b";
    const pnl = Number(tr.pnl_amount);
    if (!highlight && Number.isFinite(pnl) && pnl < -0.0001) closeColor = "#a855f7";
    const markers = [];
    if (openMs) {
      markers.push({
        time: snapToCandleTime(msToBarTime(openMs, tf), candles),
        position: long ? "belowBar" : "aboveBar",
        color: openColor,
        shape: long ? "arrowUp" : "arrowDown",
        text: "开" + suffix,
      });
    }
    if (closeMs) {
      markers.push({
        time: snapToCandleTime(msToBarTime(closeMs, tf), candles),
        position: long ? "aboveBar" : "belowBar",
        color: closeColor,
        shape: long ? "arrowDown" : "arrowUp",
        text: "平" + suffix,
      });
    }
    return markers;
  }

  function buildChartMarkers(candles, tf) {
    if (!candles.length) return [];
    const tr = pickAnchorTrade();
    if (!markAuto || !trades.length) {
      return buildTradeMarkers(tr, candles, tf, { highlight: true });
    }
    const sorted = trades.slice().sort(function (a, b) {
      return (tradeOpenMs(a) || 0) - (tradeOpenMs(b) || 0);
    });
    const multi = sorted.length > 1;
    const out = [];
    sorted.forEach(function (row, idx) {
      const rowKey = tradeRowKey(row);
      const parts = buildTradeMarkers(row, candles, tf, {
        labelSuffix: multi ? String(idx + 1) : "",
        highlight: rowKey === selectedTradeKey,
      });
      out.push.apply(out, parts);
    });
    return out.sort(function (a, b) {
      return a.time > b.time ? 1 : a.time < b.time ? -1 : 0;
    });
  }

  function applyChartMarkers() {
    if (!candleSeries || !candleSeries.setMarkers || !lastCandles.length) return;
    candleSeries.setMarkers(buildChartMarkers(lastCandles, timeframe));
  }

  function focusInitialTradeView(candles, tr, tf) {
    if (!chart || !candles.length || !tr) return;
    const mode = (elViewMode && elViewMode.value) || "hold";
    const openSec = tradeOpenMs(tr) ? msToBarTime(tradeOpenMs(tr), tf) : null;
    const closeSec = tradeCloseMs(tr) ? msToBarTime(tradeCloseMs(tr), tf) : null;
    let openIdx = 0;
    let closeIdx = candles.length - 1;
    if (openSec != null) {
      for (let i = 0; i < candles.length; i++) {
        if (candles[i].time >= openSec) {
          openIdx = i;
          break;
        }
      }
    }
    if (closeSec != null) {
      for (let i = candles.length - 1; i >= 0; i--) {
        if (candles[i].time <= closeSec) {
          closeIdx = i;
          break;
        }
      }
    }
    const span = Math.max(24, closeIdx - openIdx + 20);
    let fromIdx;
    let toIdx;
    if (mode === "entry") {
      fromIdx = Math.max(0, openIdx - Math.floor(span * 0.35));
      toIdx = Math.min(candles.length - 1, openIdx + Math.floor(span * 0.65));
    } else {
      fromIdx = Math.max(0, openIdx - 10);
      toIdx = Math.min(candles.length - 1, closeIdx + 14);
    }
    if (toIdx <= fromIdx) toIdx = Math.min(candles.length - 1, fromIdx + 80);
    chart.timeScale().setVisibleLogicalRange({ from: fromIdx, to: toIdx + 4 });
  }

  function destroyChart() {
    if (chart) {
      chart.remove();
      chart = null;
      candleSeries = null;
      volumeSeries = null;
    }
    if (elChartHost) elChartHost.innerHTML = "";
  }

  function ensureChart() {
    if (!elChartHost || !window.LightweightCharts) return;
    if (chart) return;
    const isDark = document.documentElement.getAttribute("data-theme") !== "light";
    chart = LightweightCharts.createChart(elChartHost, {
      layout: {
        background: { color: isDark ? "#0b0e18" : "#f8f9fc" },
        textColor: isDark ? "#9aa4b8" : "#4a5568",
      },
      grid: {
        vertLines: { color: isDark ? "#1a2030" : "#e8ecf2" },
        horzLines: { color: isDark ? "#1a2030" : "#e8ecf2" },
      },
      rightPriceScale: { borderColor: isDark ? "#2a3348" : "#d0d7e2", autoScale: true },
      localization: chartLocalizationBj(),
      timeScale: {
        borderColor: isDark ? "#2a3348" : "#d0d7e2",
        timeVisible: true,
        secondsVisible: false,
      },
      crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
      handleScroll: {
        mouseWheel: true,
        pressedMouseMove: true,
        horzTouchDrag: true,
        vertTouchDrag: false,
      },
      handleScale: {
        axisPressedMouseMove: true,
        mouseWheel: true,
        pinch: true,
      },
    });
    candleSeries = chart.addCandlestickSeries({
      upColor: "#22c55e",
      downColor: "#ef4444",
      borderVisible: false,
      wickUpColor: "#22c55e",
      wickDownColor: "#ef4444",
    });
    volumeSeries = chart.addHistogramSeries({
      color: "#3b82f680",
      priceFormat: { type: "volume" },
      priceScaleId: "",
    });
    volumeSeries.priceScale().applyOptions({ scaleMargins: { top: 0.82, bottom: 0 } });
    new ResizeObserver(function () {
      if (chart && elChartHost) {
        chart.applyOptions({ width: elChartHost.clientWidth, height: elChartHost.clientHeight });
      }
    }).observe(elChartHost);
    chart.applyOptions({ width: elChartHost.clientWidth, height: elChartHost.clientHeight });
  }

  async function loadSymbolTradesForChart(exKey, sym) {
    const r = await apiFetch(
      "/api/archive/detail?exchange_key=" +
        encodeURIComponent(exKey) +
        "&symbol=" +
        encodeURIComponent(sym)
    );
    const j = await r.json();
    trades = j.trades || [];
  }

  async function loadChart() {
    if (!selected || !isChartOpen()) return;
    const tr = pickAnchorTrade();
    const jump = (elJumpAt && elJumpAt.value) || "";
    let openMs = null;
    let closeMs = null;
    if (markAuto && trades.length) {
      const bounds = tradeHistoryBounds(trades);
      openMs = bounds.minOpen;
      closeMs = bounds.maxClose;
    } else if (tr) {
      openMs = tradeOpenMs(tr);
      closeMs = tradeCloseMs(tr);
    }
    const params = new URLSearchParams({
      exchange_key: selected.exchange_key,
      symbol: selected.symbol,
      timeframe: timeframe,
      mode: (elViewMode && elViewMode.value) || "hold",
    });
    if (openMs && closeMs) {
      params.set("range", "history");
      params.set("opened_ms", String(openMs));
      params.set("closed_ms", String(closeMs));
    } else {
      params.set("bars", "200");
      const anchor = anchorMsForTrade(tr);
      if (jump.trim()) params.set("at", jump.trim());
      else if (anchor) params.set("anchor_ms", String(anchor));
    }
    setStatus("加载 K 线…");
    const r = await apiFetch("/api/archive/ohlcv?" + params.toString());
    const j = await r.json();
    if (!r.ok) {
      setStatus(j.detail || "K 线加载失败");
      return;
    }
    chartExchangeSymbol = j.exchange_symbol || "";
    chartMarketType = j.market_type || "swap";
    if (chart) {
      destroyChart();
    }
    ensureChart();
    scheduleChartResize();
    const candles = j.candles || [];
    lastCandles = candles;
    candleSeries.setData(
      candles.map(function (c) {
        return { time: c.time, open: c.open, high: c.high, low: c.low, close: c.close };
      })
    );
    volumeSeries.setData(
      candles.map(function (c) {
        return {
          time: c.time,
          value: c.volume || 0,
          color: c.close >= c.open ? "#22c55e55" : "#ef444455",
        };
      })
    );
    applyChartMarkers();
    if (tr && tradeOpenMs(tr) && tradeCloseMs(tr)) {
      focusInitialTradeView(candles, tr, timeframe);
    } else if (candles.length > 10) {
      chart.timeScale().setVisibleLogicalRange({ from: candles.length - 120, to: candles.length + 5 });
    }
    updateChartTitle();
    scheduleChartResize();
    setStatus(
      "K 线 " +
        candles.length +
        " 根 · " +
        timeframe +
        " · " +
        formatChartContractLabel(selected.symbol, chartExchangeSymbol, chartMarketType)
    );
  }

  function isTradeRowInteractiveTarget(el) {
    return !!(
      el &&
      el.closest &&
      el.closest("button, select, input, textarea, a, label, .archive-actions-cell")
    );
  }

  function ensureTradePageVisible(tr) {
    if (!tr || !dailyTrades.length) return;
    const key = tradeRowKey(tr);
    const idx = dailyTrades.findIndex(function (t) {
      return tradeRowKey(t) === key;
    });
    if (idx < 0) return;
    tradesPage = Math.floor(idx / TRADES_PAGE_SIZE);
  }

  async function switchToTrade(tr) {
    if (!tr) return;
    const exKey = String(tr.exchange_key || "").toLowerCase();
    const sym = tr.symbol || "";
    if (!exKey || !sym) {
      setStatus("该笔交易缺少交易所或合约,无法切换");
      return;
    }
    const key = tradeRowKey(tr);
    const prevEx = selected && selected.exchange_key;
    const prevSym = selected && selected.symbol;
    if (key === selectedTradeKey && prevEx === exKey && prevSym === sym) return;

    selected = { exchange_key: exKey, symbol: sym };
    selectedTradeKey = key;
    ensureTradePageVisible(tr);
    renderTrades();

    const needSymbolReload = prevEx !== exKey || prevSym !== sym;
    if (needSymbolReload) {
      await loadSymbolTradesForChart(exKey, sym);
    }
    if (!isChartOpen()) return;

    if (needSymbolReload) {
      await loadChart();
      return;
    }
    applyChartMarkers();
    const anchor = pickAnchorTrade();
    if (anchor && lastCandles.length) {
      focusInitialTradeView(lastCandles, anchor, timeframe);
    }
    updateChartTitle();
    setStatus("已切换至 " + sym + " · " + exchangeLabel(exKey));
  }

  async function openTradeChart(tr) {
    if (!tr) return;
    const exKey = String(tr.exchange_key || "").toLowerCase();
    const sym = tr.symbol || "";
    if (!exKey || !sym) {
      setStatus("该笔交易缺少交易所或合约,无法加载图表");
      return;
    }
    setChartOpen(true);
    await switchToTrade(tr);
  }

  function renderCalendarDayTrades() {
    if (!elCalDayTradesBody) return;
    const day =
      selectedCalendarDay ||
      (periodMode === "today" && tradingDay) ||
      (elTradingDay && elTradingDay.value) ||
      "";
    if (elCalDayTradesMeta) {
      elCalDayTradesMeta.textContent =
        periodMode === "today" && day ? day + " · " + dailyTrades.length + " 笔" : "";
    }
    if (periodMode !== "today" || !day) {
      elCalDayTradesBody.innerHTML =
        '<p class="archive-empty">点击日历中的日期,在下方查看当日交易记录.</p>';
      return;
    }
    if (!dailyTrades.length) {
      elCalDayTradesBody.innerHTML =
        '<p class="archive-empty">该日暂无交易记录.</p>';
      return;
    }
    elCalDayTradesBody.innerHTML =
      '<table class="archive-quote-day-trades-table"><thead><tr>' +
      "<th>交易所</th><th>合约</th><th>方向</th><th>开仓</th><th>平仓</th><th>盈亏</th><th>标签</th><th></th>" +
      "</tr></thead><tbody>" +
      dailyTrades
        .map(function (t) {
          const rowKey = tradeRowKey(t);
          const tag = quoteTagLabel(t);
          return (
            "<tr>" +
            "<td>" +
            esc(tradeRowExchange(t)) +
            "</td>" +
            "<td>" +
            esc(t.symbol || "—") +
            "</td>" +
            "<td>" +
            esc(t.direction || "—") +
            "</td>" +
            '<td class="archive-dt">' +
            fmtDt(t.opened_at) +
            "</td>" +
            '<td class="archive-dt">' +
            fmtDt(t.closed_at) +
            "</td>" +
            '<td class="' +
            pnlClass(t.pnl_amount) +
            '">' +
            fmtPnl(t.pnl_amount) +
            "</td>" +
            "<td>" +
            esc(tag) +
            "</td>" +
            '<td><button type="button" class="ghost archive-cal-chart-btn" data-key="' +
            esc(rowKey) +
            '">图表</button></td></tr>'
          );
        })
        .join("") +
      "</tbody></table>";
    elCalDayTradesBody.querySelectorAll(".archive-cal-chart-btn").forEach(function (btn) {
      btn.addEventListener("click", function () {
        const tr = findTradeByKey(btn.getAttribute("data-key"));
        if (tr) void openTradeChart(tr);
      });
    });
  }

  function tradesPageCount() {
    return Math.max(1, Math.ceil((dailyTrades.length || 0) / TRADES_PAGE_SIZE));
  }

  function clampTradesPage() {
    const pages = tradesPageCount();
    if (tradesPage >= pages) tradesPage = pages - 1;
    if (tradesPage < 0) tradesPage = 0;
  }

  function updateTradesPager() {
    clampTradesPage();
    const pages = tradesPageCount();
    const show = dailyTrades.length > TRADES_PAGE_SIZE;
    if (elTradesPager) elTradesPager.hidden = !show;
    if (elTradesPageLabel) {
      elTradesPageLabel.textContent = "第 " + (tradesPage + 1) + " / " + pages + " 页";
    }
    if (elTradesPrev) elTradesPrev.disabled = tradesPage <= 0;
    if (elTradesNext) elTradesNext.disabled = tradesPage + 1 >= pages;
  }

  function pagedDailyTrades() {
    clampTradesPage();
    const start = tradesPage * TRADES_PAGE_SIZE;
    return dailyTrades.slice(start, start + TRADES_PAGE_SIZE);
  }

  function renderTrades() {
    if (!elTrades) return;
    if (!dailyTrades.length) {
      elTrades.innerHTML =
        '<p class="archive-empty">该日暂无交易记录.可调整日期或点击「同步」拉取数据.</p>';
      updateTradesPager();
      return;
    }
    const pageRows = pagedDailyTrades();
    elTrades.innerHTML =
      '<table class="archive-trades-table"><thead><tr>' +
      "<th>交易所</th><th>合约</th><th>开仓类型</th><th>开仓时间</th><th>平仓时间</th><th>持仓时长</th>" +
      "<th>方向</th><th>结果</th><th>盈亏</th><th>成交额</th><th>手续费</th><th>标签</th><th>备注</th><th>操作</th>" +
      "</tr></thead><tbody>" +
      pageRows
        .map(function (t) {
          const tid = t.trade_id || t.id;
          const exKey = String(t.exchange_key || "").toLowerCase();
          const rowKey = tradeRowKey(t);
          const journalSick = !!t.behavior_tag_from_journal;
          const tag = journalSick ? "sick" : (t.behavior_tag || "");
          const sick = tag === "sick";
          const active = rowKey && rowKey === selectedTradeKey ? " is-active" : "";
          const rev = reviewMark(t);
          return (
            '<tr class="archive-trade-row' +
            active +
            (sick ? " archive-trade-sick" : "") +
            '" data-key="' +
            esc(rowKey) +
            '" data-id="' +
            tid +
            '" data-ex="' +
            esc(exKey) +
            '" data-sym="' +
            esc(t.symbol || "") +
            '">' +
            "<td>" +
            esc(tradeRowExchange(t)) +
            "</td>" +
            '<td class="archive-symbol">' +
            esc(t.symbol || "—") +
            "</td>" +
            "<td>" +
            (rev ? '<span class="archive-review-mark">' + rev + "</span>" : "") +
            esc(fmtEntryType(t)) +
            "</td>" +
            '<td class="archive-dt">' +
            fmtDt(t.opened_at) +
            "</td>" +
            '<td class="archive-dt">' +
            fmtDt(t.closed_at) +
            "</td>" +
            '<td class="archive-hold">' +
            fmtHoldMinutes(t) +
            "</td>" +
            "<td>" +
            esc(t.direction || "—") +
            "</td>" +
            "<td>" +
            esc(t.result || "—") +
            "</td>" +
            '<td class="' +
            pnlClass(t.pnl_amount) +
            '">' +
            fmtPnl(t.pnl_amount) +
            "</td>" +
            "<td>" +
            fmtVolStat(t.exchange_turnover_usdt) +
            "</td>" +
            "<td>" +
            fmtFeeStat(t.exchange_commission_usdt) +
            "</td>" +
            (journalSick
              ? '<td><span class="archive-tag-fixed is-tag-sick" title="实例复盘已勾选情绪标签">犯病</span></td>'
              : '<td><select class="archive-tag-select" data-id="' +
                tid +
                '" data-ex="' +
                esc(exKey) +
                '">' +
                '<option value=""' +
                (tag === "" ? " selected" : "") +
                ">—</option>" +
                '<option value="sick"' +
                (tag === "sick" ? " selected" : "") +
                ">犯病</option>" +
                '<option value="emotion"' +
                (tag === "emotion" ? " selected" : "") +
                ">情绪</option>" +
                "</select></td>") +
            '<td><input class="archive-note-input" data-id="' +
            tid +
            '" data-ex="' +
            esc(exKey) +
            '" value="' +
            esc(t.note || "") +
            '" placeholder="备注" /></td>' +
            '<td class="archive-actions-cell">' +
            '<button type="button" class="ghost archive-chart-btn" data-id="' +
            tid +
            '">图表</button>' +
            '<button type="button" class="archive-del-btn" data-id="' +
            tid +
            '">删除</button>' +
            "</td></tr>"
          );
        })
        .join("") +
      "</tbody></table>";

    updateTradesPager();

    elTrades.querySelectorAll(".archive-del-btn").forEach(function (btn) {
      btn.addEventListener("click", function (ev) {
        ev.stopPropagation();
        const row = btn.closest(".archive-trade-row");
        void deleteTrade(btn.getAttribute("data-id"), row && row.getAttribute("data-ex"));
      });
    });
    elTrades.querySelectorAll(".archive-chart-btn").forEach(function (btn) {
      btn.addEventListener("click", function (ev) {
        ev.stopPropagation();
        const row = btn.closest(".archive-trade-row");
        const rowKey = row && row.getAttribute("data-key");
        const tr = findTradeByKey(rowKey);
        if (tr) void openTradeChart(tr);
        else if (rowKey) {
          selectedTradeKey = rowKey;
          renderTrades();
        }
      });
    });
    elTrades.querySelectorAll(".archive-trade-row").forEach(function (row) {
      row.addEventListener("click", function (ev) {
        if (!isChartOpen()) return;
        if (isTradeRowInteractiveTarget(ev.target)) return;
        const tr = findTradeByKey(row.getAttribute("data-key"));
        if (tr) void switchToTrade(tr);
      });
    });
    elTrades.querySelectorAll(".archive-tag-select").forEach(function (sel) {
      applyTagSelectStyle(sel);
      sel.addEventListener("mousedown", function (ev) {
        ev.stopPropagation();
      });
      sel.addEventListener("change", function () {
        applyTagSelectStyle(sel);
        saveOverlay(sel.getAttribute("data-id"), sel.getAttribute("data-ex"), sel.value, null);
      });
    });
    elTrades.querySelectorAll(".archive-note-input").forEach(function (inp) {
      inp.addEventListener("mousedown", function (ev) {
        ev.stopPropagation();
      });
      inp.addEventListener("click", function (ev) {
        ev.stopPropagation();
      });
      inp.addEventListener("change", function () {
        const row = inp.closest(".archive-trade-row");
        const tagSel = row && row.querySelector(".archive-tag-select");
        const tr = findTradeByKey(row && row.getAttribute("data-key"));
        const tag =
          tr && tr.behavior_tag_from_journal
            ? "sick"
            : tagSel
              ? tagSel.value
              : "";
        saveOverlay(
          inp.getAttribute("data-id"),
          inp.getAttribute("data-ex"),
          tag,
          inp.value
        );
      });
    });
    requestAnimationFrame(syncTradesLayout);
  }

  async function deleteTrade(tradeId, exchangeKey) {
    const exKey = exchangeKey || (selected && selected.exchange_key);
    if (!exKey || tradeId == null) return;
    if (!window.confirm("从档案移除该笔交易?(不影响交易所实例里的复盘记录)")) return;
    const r = await apiFetch("/api/archive/trade/" + exKey + "/" + tradeId, { method: "DELETE" });
    if (!r.ok) {
      const j = await r.json().catch(function () {
        return {};
      });
      setStatus(j.detail || j.msg || "删除失败");
      return;
    }
    const deletedKey = String(exchangeKey || "").toLowerCase() + ":" + String(tradeId);
    if (selectedTradeKey === deletedKey) selectedTradeKey = null;
    await loadDailyTrades();
    setStatus("已移除 1 笔档案记录");
  }

  async function saveOverlay(tradeId, exchangeKey, tag, note) {
    const exKey = exchangeKey || (selected && selected.exchange_key);
    if (!exKey) return;
    const tr = dailyTrades.find(function (t) {
      return (
        String(t.trade_id || t.id) === String(tradeId) &&
        String(t.exchange_key || "").toLowerCase() === String(exKey).toLowerCase()
      );
    });
    if (tr && tr.behavior_tag_from_journal && tag != null && String(tag) !== "sick") {
      return;
    }
    const body = {
      behavior_tag: tr && tr.behavior_tag_from_journal ? "sick" : tag || "",
      note: note != null ? note : undefined,
    };
    if (note == null) {
      const row = elTrades.querySelector(
        '.archive-trade-row[data-id="' + tradeId + '"][data-ex="' + exKey + '"]'
      );
      const inp = row && row.querySelector(".archive-note-input");
      body.note = inp ? inp.value : "";
    }
    await apiFetch("/api/archive/trade/" + exKey + "/" + tradeId, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (tr) {
      tr.behavior_tag = body.behavior_tag;
      tr.note = body.note;
    }
    renderTrades();
  }

  async function loadDailyTrades() {
    setStatus("加载交易记录…");
    const r = await apiFetch("/api/archive/daily-trades?" + queryDailyParams());
    const j = await r.json();
    if (!r.ok) {
      setStatus(j.detail || "加载失败");
      return;
    }
    periodMode = j.period || periodMode || "today";
    periodLabel = j.period_label || periodLabel || "";
    dateFrom = j.date_from || dateFrom || "";
    dateTo = j.date_to || dateTo || "";
    tradingDay = j.trading_day || tradingDay;
    if (elTradingDay && tradingDay) elTradingDay.value = tradingDay;
    if (elDateFrom && dateFrom) elDateFrom.value = dateFrom;
    if (elDateTo && dateTo) elDateTo.value = dateTo;
    if (elQuoteDate && tradingDay && !elQuoteDate.value) elQuoteDate.value = tradingDay;
    syncPeriodUI();
    dailyTrades = j.trades || [];
    tradesPage = 0;
    dailyStats = j.stats || { open_count: 0, by_exchange: {} };
    if (periodMode === "today" && tradingDay) {
      selectedCalendarDay = tradingDay;
      if (calendarWidget) calendarWidget.selectedDay = tradingDay;
    }
    renderStats();
    renderTrades();
    renderCalendarDayTrades();
    void loadCalendar();
    if (archiveContentTab === "quotes") void loadQuoteDayTrades();
    setStatus(
      (periodLabel || tradingDay || "当日") +
        " · 列表 " +
        dailyTrades.length +
        " 笔 · " +
        new Date().toLocaleTimeString()
    );
  }

  async function loadMeta() {
    const r = await apiFetch("/api/archive/meta");
    meta = await r.json();
    timeframe = (meta && meta.default_timeframe) || "15m";
    if (meta && meta.last_sync && elStatus && !elStatus.textContent) {
      setStatus(formatSyncSummary(meta.last_sync));
    }
    renderExchangeOptions();
    if (elTfTabs) {
      elTfTabs.querySelectorAll(".archive-tf-btn").forEach(function (btn) {
        btn.classList.toggle("is-active", btn.getAttribute("data-tf") === timeframe);
      });
    }
  }

  function formatSyncSummary(j) {
    const results = j.results || [];
    const okN = results.filter(function (x) {
      return x.ok !== false;
    }).length;
    const parts = ["同步完成 · " + okN + "/" + (j.exchanges || 0) + " 所"];
    results.forEach(function (row) {
      const label = row.exchange_key || row.name || "?";
      if (row.ok === false) parts.push(label + " 失败: " + (row.msg || "未知错误"));
      else {
        let line = label + " " + (row.trade_count != null ? row.trade_count : row.trades || 0) + " 笔";
        if (row.trades_removed > 0) line += " 清" + row.trades_removed;
        parts.push(line);
      }
    });
    return parts.join(" · ");
  }

  async function syncAll() {
    setStatus("同步中(可能需数分钟)…");
    if (elBtnSync) elBtnSync.disabled = true;
    try {
      const r = await apiFetch("/api/archive/sync", { method: "POST" });
      const j = await r.json();
      setStatus(formatSyncSummary(j));
      await loadDailyTrades();
      await loadCalendar();
      await loadQuotes();
      if (isChartOpen() && selected) await loadChart();
    } catch (e) {
      setStatus(String(e));
    } finally {
      if (elBtnSync) elBtnSync.disabled = false;
    }
  }

  function bindEvents() {
    if (elBtnRefresh) elBtnRefresh.addEventListener("click", loadDailyTrades);
    if (elBtnSync) elBtnSync.addEventListener("click", syncAll);
    if (elTradesPrev) {
      elTradesPrev.addEventListener("click", function () {
        if (tradesPage <= 0) return;
        tradesPage -= 1;
        renderTrades();
      });
    }
    if (elTradesNext) {
      elTradesNext.addEventListener("click", function () {
        if (tradesPage + 1 >= tradesPageCount()) return;
        tradesPage += 1;
        renderTrades();
      });
    }
    if (elExchange) {
      elExchange.addEventListener("change", function () {
        void loadDailyTrades();
        void loadCalendar();
      });
    }
    if (elPeriodTabs) {
      elPeriodTabs.addEventListener("click", function (ev) {
        const btn = ev.target.closest(".archive-period-btn");
        if (!btn) return;
        const next = btn.getAttribute("data-period") || "today";
        if (next === periodMode) return;
        setPeriodMode(next);
        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) {
      if (el) el.addEventListener("change", loadDailyTrades);
    });
    if (elSearch) {
      elSearch.addEventListener("input", function () {
        clearTimeout(searchTimer);
        searchTimer = setTimeout(loadDailyTrades, 320);
      });
    }
    if (elBtnChartToggle) {
      elBtnChartToggle.addEventListener("click", async function () {
        const next = !isChartOpen();
        setChartOpen(next);
        if (next) {
          await ensureChartSelection();
          void loadChart();
        }
      });
    }
    if (elBtnChartClose) {
      elBtnChartClose.addEventListener("click", function () {
        setChartOpen(false);
      });
    }
    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");
        if (!btn) return;
        timeframe = btn.getAttribute("data-tf") || "15m";
        elTfTabs.querySelectorAll(".archive-tf-btn").forEach(function (b) {
          b.classList.toggle("is-active", b === btn);
        });
        loadChart();
      });
    }
    if (elViewMode) elViewMode.addEventListener("change", loadChart);
    if (elBtnReloadChart) elBtnReloadChart.addEventListener("click", loadChart);
    if (elMarkAuto) {
      elMarkAuto.addEventListener("click", function () {
        markAuto = !markAuto;
        syncMarkAutoBtn();
        saveMarkAutoPref();
        loadChart();
      });
    }
    if (elBtnJump) elBtnJump.addEventListener("click", loadChart);
  }

  async function init() {
    if (!page || page.classList.contains("hidden")) return;
    if (!inited) {
      loadMarkAutoPref();
      setChartOpen(false);
      syncPeriodUI();
      syncTradesLayout();
      bindEvents();
      setArchiveContentTab("trades");
      inited = true;
    }
    await loadMeta();
    await loadQuotes();
    await loadDailyTrades();
  }

  function destroy() {
    destroyChart();
  }

  window.hubArchivePage = { init: init, destroy: destroy };
})();
