javascript:void (function () {
  const SUPPORTED_ROUTE_RE = /^https:\/\/next\.lude\.site\/rotation\/(?:stock|convertible-bond|etf)(?:[/?#].*)?$/;
  if (!SUPPORTED_ROUTE_RE.test(location.href)) {
    return;
  }

  const BUILD_TAG = 'stock-bond-combo-20260421-v016-etf-clean';
  const PANEL_ID = 'lude-stock-bond-panel';
  const STYLE_ID = 'lude-stock-bond-style';
  const STORAGE_KEY = 'lude_portfolios';
  const INSTANCE_ID = `${BUILD_TAG}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
  window.__ludeStockBondComboBuildTag__ = BUILD_TAG;
  const THEME = {
    primary: '#2563eb',
    primaryDark: '#1d4ed8',
    primarySoft: '#eff6ff',
    border: '#bfdbfe',
    surface: '#f8fafc',
    success: '#1d4ed8',
    danger: '#dc2626',
    warning: '#475569',
    textMuted: '#64748b',
    neutralBorder: '#e2e8f0',
    neutralFill: '#f8fafc',
    neutralStrong: '#334155',
    series: ['#2563eb', '#7c3aed', '#d97706', '#0891b2', '#475569', '#0f766e'],
    portfolioLine: '#dc2626',
    rebalanceLine: '#16a34a',
    financialRed: '#dc2626',
    financialGreen: '#16a34a',
    correlationPositiveStrong: '#fecaca',
    correlationPositiveMid: '#fee2e2',
    correlationNegativeStrong: '#bbf7d0',
    correlationNegativeMid: '#dcfce7',
    correlationNeutral: '#f8fafc',
    portfolioSoft: '#eff6ff'
  };
  const REBALANCE_OPTIONS = [
    { key: 'daily', label: '日再平衡', days: 1 },
    { key: 'weekly', label: '周再平衡', days: 5 },
    { key: 'monthly', label: '月再平衡', days: 21 },
    { key: 'quarterly', label: '季度再平衡', days: 63 },
    { key: 'half_year', label: '半年再平衡', days: 126 },
    { key: 'yearly', label: '年度再平衡', days: 252 }
  ];

  function normalizeRebalancePeriod(periodKey) {
    return REBALANCE_OPTIONS.some((option) => option.key === periodKey) ? periodKey : 'half_year';
  }

  function getRebalanceOption(periodKey) {
    return REBALANCE_OPTIONS.find((option) => option.key === normalizeRebalancePeriod(periodKey)) || REBALANCE_OPTIONS[4];
  }

  function getRebalanceDays(periodKey) {
    return getRebalanceOption(periodKey).days;
  }

  function getRebalanceLabel(periodKey) {
    return getRebalanceOption(periodKey).label;
  }

  function getReturnColor(value) {
    const numericValue = Number(value);
    if (numericValue > 0) {
      return THEME.financialRed;
    }
    if (numericValue < 0) {
      return THEME.financialGreen;
    }
    return THEME.textMuted;
  }

  function getCorrelationTone(value, isDiagonal) {
    if (isDiagonal) {
      return { background: THEME.neutralFill, color: THEME.neutralStrong };
    }
    if (value >= 0.7) {
      return { background: THEME.correlationPositiveStrong, color: THEME.financialRed };
    }
    if (value > 0.3) {
      return { background: THEME.correlationPositiveMid, color: THEME.financialRed };
    }
    if (value <= -0.7) {
      return { background: THEME.correlationNegativeStrong, color: THEME.financialGreen };
    }
    if (value < -0.3) {
      return { background: THEME.correlationNegativeMid, color: THEME.financialGreen };
    }
    return { background: THEME.correlationNeutral, color: THEME.neutralStrong };
  }

  const existingCleanup = window.__ludeStockBondComboCleanup__;
  if (typeof existingCleanup === 'function') {
    existingCleanup('bootstrap-existing');
  }

  const existingPanel = document.getElementById(PANEL_ID);
  const existingStyle = document.getElementById(STYLE_ID);
  if (existingPanel) {
    existingPanel.remove();
  }
  if (existingStyle) {
    existingStyle.remove();
  }
  window.__ludeStockBondComboActiveInstance__ = INSTANCE_ID;

  const nativeFetch = window.__ludeStockBondComboOriginalFetch__ || window.fetch.bind(window);
  window.__ludeStockBondComboOriginalFetch__ = nativeFetch;

  const xhrProto = window.XMLHttpRequest && window.XMLHttpRequest.prototype;
  const originalXHROpen = xhrProto ? window.__ludeStockBondComboOriginalOpen__ || xhrProto.open : null;
  const originalXHRSend = xhrProto ? window.__ludeStockBondComboOriginalSend__ || xhrProto.send : null;
  if (xhrProto) {
    window.__ludeStockBondComboOriginalOpen__ = originalXHROpen;
    window.__ludeStockBondComboOriginalSend__ = originalXHRSend;
  }

  function query(selector, root = document) {
    return root.querySelector(selector);
  }

  function queryAll(selector, root = document) {
    return Array.from(root.querySelectorAll(selector));
  }

  function escapeHtml(value) {
    return String(value || '').replace(/[&<>"']/g, (char) => ({
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#39;'
    }[char]));
  }

  function roundNumber(value, digits = 2) {
    const factor = 10 ** digits;
    return Math.round(value * factor) / factor;
  }

  function stripStrategyNoise(value) {
    return String(value || '')
      .replace(/\s+/g, ' ')
      .replace(/(超参调优|覆盖策略|另存策略|新建空白策略|切换策略).*$/, '')
      .trim();
  }

  function normalizeStrategyName(value) {
    return stripStrategyNoise(String(value || '').replace(/[：]/g, ':').replace(/^当前策略\s*:\s*/, ''))
      .replace(/\s+/g, '')
      .toLowerCase();
  }

  function parseLegacyLines(text) {
    return String(text || '')
      .split('\n')
      .map((line) => line.trim())
      .filter(Boolean)
      .map((line) => {
        const parts = line.split(':');
        if (parts.length !== 2) {
          return null;
        }
        const name = parts[0].trim();
        const weight = Number(parts[1]);
        if (!name || Number.isNaN(weight)) {
          return null;
        }
        return { n: name, w: weight > 1 ? weight / 100 : weight };
      })
      .filter(Boolean);
  }

  class StrategyConfig {
    constructor(name = '', weight = 0) {
      this.name = String(name || '').trim();
      this.weight = Number.isFinite(weight) ? Number(weight) : 0;
    }

    rename(name) {
      this.name = String(name || '').trim();
      return this;
    }

    setWeight(value) {
      const next = Number(value);
      this.weight = Number.isFinite(next) ? Math.max(next, 0) : 0;
      return this;
    }

    setWeightFromPercent(percent) {
      const next = Number(percent);
      if (!Number.isFinite(next)) {
        this.weight = 0;
        return this;
      }
      this.weight = Math.max(next, 0) / 100;
      return this;
    }

    percent() {
      return roundNumber(this.weight * 100, 2);
    }

    normalizedName() {
      return normalizeStrategyName(this.name);
    }

    matches(candidate) {
      const normalized = normalizeStrategyName(candidate);
      const self = this.normalizedName();
      return !!self && !!normalized && (self === normalized || self.includes(normalized) || normalized.includes(self));
    }

    serialize() {
      return { n: this.name, w: this.weight };
    }
  }

  class StrategyCollection {
    constructor(items = []) {
      this.items = items.map((item) => item instanceof StrategyConfig ? item : new StrategyConfig(item.n || item.name || '', item.w || item.weight || 0));
      if (!this.items.length) {
        this.items = [new StrategyConfig('', 0), new StrategyConfig('', 0)];
      }
    }

    static fromLegacyText(text) {
      return new StrategyCollection(parseLegacyLines(text));
    }

    namedItems() {
      return this.items.filter((item) => item.name.trim());
    }

    totalWeight() {
      return this.namedItems().reduce((sum, item) => sum + item.weight, 0);
    }

    addBlank() {
      this.items.push(new StrategyConfig('', 0));
      return this;
    }

    addOrFill(name) {
      const normalized = normalizeStrategyName(name);
      if (!normalized) {
        return { added: false, reason: 'empty' };
      }
      if (this.namedItems().some((item) => item.matches(name))) {
        return { added: false, reason: 'exists' };
      }
      const blank = this.items.find((item) => !item.name.trim());
      if (blank) {
        blank.rename(name);
      } else {
        this.items.push(new StrategyConfig(name, 0));
      }
      return { added: true };
    }

    removeAt(index) {
      this.items.splice(index, 1);
      if (!this.items.length) {
        this.items.push(new StrategyConfig('', 0));
      }
      return this;
    }

    rebalanceEvenly() {
      const activeItems = this.namedItems();
      if (!activeItems.length) {
        return this;
      }
      const basePercent = roundNumber(100 / activeItems.length, 2);
      let assigned = 0;
      activeItems.forEach((item, index) => {
        const percent = index === activeItems.length - 1 ? roundNumber(100 - assigned, 2) : basePercent;
        item.setWeightFromPercent(percent);
        assigned += percent;
      });
      this.items.forEach((item) => {
        if (!item.name.trim()) {
          item.setWeight(0);
        }
      });
      return this;
    }

    validate() {
      const activeItems = this.namedItems();
      if (activeItems.length < 2) {
        return { ok: false, message: '至少需要 2 个策略' };
      }
      if (Math.abs(this.totalWeight() - 1) > 0.001) {
        return { ok: false, message: '权重和需为 100%' };
      }
      return { ok: true, message: '' };
    }

    toLegacyText() {
      return this.namedItems().map((item) => `${item.name}:${item.weight}`).join('\n');
    }

    serialize() {
      return this.namedItems().map((item) => item.serialize());
    }

    findMatch(candidateName) {
      if (!candidateName) {
        return null;
      }
      return this.namedItems().find((item) => item.matches(candidateName)) || null;
    }
  }

  const state = {
    collection: StrategyCollection.fromLegacyText(''),
    collected: {},
    monitoring: false,
    hooksInstalled: false,
    correlationPeriod: 'all',
    rebalancePeriod: 'half_year'
  };

  function toDateKey(point) {
    return String((point && (point.trade_time || point.trade_date)) || '').split(' ')[0];
  }

  function alignEntriesByDate(entries) {
    if (!Array.isArray(entries) || entries.length === 0) {
      return entries;
    }
    const dateMaps = entries.map((entry) => {
      const map = new Map();
      (entry.t || []).forEach((point) => {
        const key = toDateKey(point);
        if (key) {
          map.set(key, point);
        }
      });
      return map;
    });
    const commonDates = Array.from(dateMaps[0].keys())
      .filter((date) => dateMaps.every((map) => map.has(date)))
      .sort();
    return entries.map((entry, index) => ({
      ...entry,
      t: commonDates.map((date) => dateMaps[index].get(date))
    }));
  }

  function getCollectedEntries() {
    const entries = state.collection.namedItems()
      .filter((item) => state.collected[item.name])
      .map((item) => ({ n: item.name, w: item.weight, t: state.collected[item.name].t }));
    return alignEntriesByDate(entries);
  }

  function getMissingNames() {
    return state.collection.namedItems()
      .filter((item) => !state.collected[item.name])
      .map((item) => item.name);
  }

  function log(message, type = 'i') {
    const logNode = query('#lude_log');
    if (!logNode) {
      return;
    }
    const colors = {
      s: THEME.primaryDark,
      e: THEME.danger,
      i: THEME.neutralStrong,
      w: THEME.textMuted
    };
    const prefix = {
      s: '✓ ',
      e: '✗ ',
      i: 'ℹ ',
      w: '⚠ '
    };
    const line = document.createElement('div');
    line.style.color = colors[type] || THEME.neutralStrong;
    line.style.margin = '2px 0';
    line.textContent = `${prefix[type] || ''}${message}`;
    logNode.appendChild(line);
    logNode.scrollTop = logNode.scrollHeight;
  }

  function ensureStyle() {
    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.dataset.instanceId = INSTANCE_ID;
    style.textContent = `
      #${PANEL_ID} {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 520px;
        max-width: calc(100vw - 32px);
        background: #fff;
        border: 1px solid ${THEME.border};
        border-radius: 12px;
        box-shadow: 0 16px 40px rgba(37, 99, 235, 0.12);
        z-index: 999999;
        font: 13px sans-serif;
      }
      #${PANEL_ID} * {
        box-sizing: border-box;
      }
      #${PANEL_ID} .lude-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 10px 12px;
        background: ${THEME.primary};
        color: #fff;
        border-radius: 12px 12px 0 0;
        cursor: move;
        user-select: none;
      }
      #${PANEL_ID} .lude-header-actions {
        display: flex;
        gap: 8px;
      }
      #${PANEL_ID} .lude-icon-btn,
      #${PANEL_ID} .lude-btn {
        border: none;
        cursor: pointer;
        border-radius: 8px;
      }
      #${PANEL_ID} .lude-icon-btn {
        width: 26px;
        height: 26px;
        background: rgba(255, 255, 255, 0.14);
        color: #fff;
      }
      #${PANEL_ID} .lude-body {
        padding: 10px;
        max-height: 80vh;
        overflow-y: auto;
        background: #fff;
      }
      #${PANEL_ID} .lude-section {
        margin-top: 10px;
      }
      #${PANEL_ID} .lude-section-title {
        padding: 6px 8px;
        border-radius: 8px;
        background: ${THEME.primarySoft};
        color: ${THEME.primaryDark};
        font-weight: 700;
      }
      #${PANEL_ID} .lude-section-title.is-toggle {
        cursor: pointer;
      }
      #${PANEL_ID} .lude-toolbar,
      #${PANEL_ID} .lude-inline-actions {
        display: flex;
        gap: 6px;
        flex-wrap: wrap;
        margin-top: 8px;
      }
      #${PANEL_ID} .lude-btn {
        padding: 8px 10px;
        background: ${THEME.primary};
        color: #fff;
        white-space: nowrap;
      }
      #${PANEL_ID} .lude-btn.secondary {
        background: ${THEME.primary};
      }
      #${PANEL_ID} .lude-btn.light {
        background: ${THEME.primary};
      }
      #${PANEL_ID} .lude-btn.ghost {
        background: ${THEME.primarySoft};
        color: ${THEME.primaryDark};
        border: 1px solid ${THEME.border};
      }
      #${PANEL_ID} .lude-btn.danger {
        background: ${THEME.danger};
      }
      #${PANEL_ID} .lude-config-list {
        display: flex;
        flex-direction: column;
        gap: 6px;
        margin-top: 8px;
      }
      #${PANEL_ID} .lude-config-row {
        display: flex;
        gap: 6px;
        align-items: center;
        flex-wrap: nowrap;
      }
      #${PANEL_ID} .lude-input {
        width: 100%;
        padding: 6px 8px;
        border: 1px solid ${THEME.border};
        border-radius: 8px;
        background: #fff;
        min-width: 0;
      }
      #${PANEL_ID} .lude-input.weight {
        width: 88px;
        flex: 0 0 88px;
        text-align: right;
      }
      #${PANEL_ID} .lude-config-name {
        flex: 1 1 auto;
      }
      #${PANEL_ID} .lude-config-row > span,
      #${PANEL_ID} .lude-config-remove {
        flex: 0 0 auto;
      }
      #${PANEL_ID} .lude-helper {
        margin-top: 6px;
        font-size: 11px;
        color: ${THEME.textMuted};
      }
      #${PANEL_ID} .lude-log {
        margin-top: 10px;
        padding: 8px;
        background: ${THEME.surface};
        border: 1px solid ${THEME.neutralBorder};
        border-radius: 8px;
        max-height: 120px;
        overflow-y: auto;
      }
      #${PANEL_ID} table {
        width: 100%;
        border-collapse: collapse;
        margin-top: 6px;
        font-size: 11px;
      }
      #${PANEL_ID} th,
      #${PANEL_ID} td {
        border: 1px solid ${THEME.neutralBorder};
        padding: 4px;
      }
      #${PANEL_ID} th {
        background: ${THEME.neutralFill};
      }
      #${PANEL_ID} canvas {
        width: 100%;
        border: 1px solid ${THEME.neutralBorder};
        border-radius: 8px;
        margin-top: 6px;
      }
      #${PANEL_ID} .lude-hidden {
        display: none !important;
      }
      #${PANEL_ID} .lude-summary-weight {
        width: 64px;
        padding: 3px 4px;
        text-align: right;
        border: 1px solid ${THEME.border};
        border-radius: 6px;
      }
      #${PANEL_ID} .lude-status-success {
        color: ${THEME.financialRed};
      }
      #${PANEL_ID} .lude-status-danger {
        color: ${THEME.financialGreen};
      }
      #${PANEL_ID} .lude-resize-handle {
        position: absolute;
        right: 0;
        bottom: 0;
        width: 14px;
        height: 14px;
        border-radius: 0 0 12px 0;
        background: ${THEME.primary};
        cursor: se-resize;
      }
    `;
    document.head.appendChild(style);
  }

  function createPanel() {
    const panel = document.createElement('div');
    panel.id = PANEL_ID;
    panel.dataset.instanceId = INSTANCE_ID;
    panel.innerHTML = `
      <div id="lude_header" class="lude-header">
        <strong>禄得股票转债组合分析</strong>
        <div class="lude-header-actions">
          <button id="lude_expand" class="lude-icon-btn" title="展开/缩小">⬜</button>
          <button id="lude_close" class="lude-icon-btn" title="关闭">✕</button>
        </div>
      </div>
      <div class="lude-body">
        <div class="lude-section">
          <div id="lude_saved_toggle" class="lude-section-title is-toggle">💾 已保存组合 <span style="float:right">▼</span></div>
          <div id="lude_saved_content" class="lude-hidden">
            <div id="lude_saved_list" class="lude-config-list"></div>
          </div>
        </div>
        <div class="lude-section">
          <div class="lude-section-title">⚙️ 策略参数配置</div>
          <div class="lude-toolbar">
            <button id="lude_capture" class="lude-btn">抓取策略名称</button>
            <button id="lude_add_row" class="lude-btn secondary">新增策略</button>
            <button id="lude_equal_weight" class="lude-btn light">均分权重</button>
          </div>
          <div id="lude_config_list" class="lude-config-list"></div>
          <div id="lude_weight_status" class="lude-helper">当前权重和：0%</div>
          <textarea id="lude_input" class="lude-hidden"></textarea>
        </div>
        <div class="lude-inline-actions">
          <label>再平衡：
            <select id="lude_rebal_period" class="lude-input" style="width:auto;min-width:136px;display:inline-block;margin-left:4px">
              ${REBALANCE_OPTIONS.map((option) => `<option value="${option.key}">${option.label}</option>`).join('')}
            </select>
          </label>
        </div>
        <div class="lude-toolbar">
          <button id="lude_start" class="lude-btn">开始</button>
          <button id="lude_save" class="lude-btn secondary">保存</button>
          <button id="lude_export" class="lude-btn light">导出</button>
          <button id="lude_reset" class="lude-btn ghost">重置</button>
        </div>
        <div id="lude_log" class="lude-log">等待开始</div>

        <div id="lude_chart" class="lude-section lude-hidden">
          <div class="lude-section-title">📈 累计收益曲线</div>
          <canvas id="lude_canvas" width="760" height="260"></canvas>
          <div id="lude_legend" class="lude-helper"></div>
        </div>

        <div id="lude_corr" class="lude-section lude-hidden">
          <div class="lude-section-title">🔗 相关性矩阵</div>
          <div id="lude_corr_table"></div>
        </div>

        <div id="lude_summary" class="lude-section lude-hidden">
          <div class="lude-section-title">📊 汇总对比</div>
          <div id="lude_table"></div>
        </div>

        <div id="lude_allocation" class="lude-section lude-hidden">
          <div class="lude-section-title">⚖️ 资产配置优化</div>
          <div id="lude_allocation_content"></div>
        </div>

        <div id="lude_yearly" class="lude-section lude-hidden">
          <div class="lude-section-title">📅 年度收益统计</div>
          <div id="lude_yearly_content"></div>
        </div>

      </div>
      <div id="lude_resize_handle" class="lude-resize-handle"></div>
    `;
    document.body.appendChild(panel);
    return panel;
  }

  ensureStyle();
  const panel = createPanel();

  function setExpanded(expanded) {
    const expandButton = query('#lude_expand');
    if (expanded) {
      panel.style.width = '78vw';
      panel.style.maxWidth = '1120px';
      panel.style.height = '82vh';
      panel.style.left = '50%';
      panel.style.top = '50%';
      panel.style.transform = 'translate(-50%, -50%)';
      if (expandButton) {
        expandButton.textContent = '🔲';
      }
      return;
    }
    panel.style.width = '520px';
    panel.style.maxWidth = 'calc(100vw - 32px)';
    panel.style.height = '';
    panel.style.left = '50%';
    panel.style.top = '50%';
    panel.style.transform = 'translate(-50%, -50%)';
    if (expandButton) {
      expandButton.textContent = '⬜';
    }
  }

  setExpanded(false);

  function enableDrag() {
    const header = query('#lude_header');
    if (!header) {
      return;
    }
    let dragging = false;
    let originX = 0;
    let originY = 0;
    let panelX = 0;
    let panelY = 0;
    header.addEventListener('mousedown', (event) => {
      if (event.target instanceof HTMLElement && event.target.closest('button')) {
        return;
      }
      dragging = true;
      originX = event.clientX;
      originY = event.clientY;
      panelX = panel.offsetLeft;
      panelY = panel.offsetTop;
      panel.style.transform = 'none';
      event.preventDefault();
    });
    document.addEventListener('mousemove', (event) => {
      if (!dragging) {
        return;
      }
      panel.style.left = `${panelX + event.clientX - originX}px`;
      panel.style.top = `${panelY + event.clientY - originY}px`;
      panel.style.right = 'auto';
    });
    document.addEventListener('mouseup', () => {
      dragging = false;
    });
  }

  function enableResize() {
    const handle = query('#lude_resize_handle');
    if (!handle) {
      return;
    }
    handle.addEventListener('mousedown', (event) => {
      event.preventDefault();
      const startX = event.clientX;
      const startY = event.clientY;
      const startWidth = panel.offsetWidth;
      const startHeight = panel.offsetHeight;
      function onMove(moveEvent) {
        const nextWidth = startWidth + moveEvent.clientX - startX;
        const nextHeight = startHeight + moveEvent.clientY - startY;
        panel.style.width = `${Math.max(380, nextWidth)}px`;
        panel.style.height = `${Math.max(220, nextHeight)}px`;
        panel.style.transform = 'none';
      }
      function onUp() {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
      }
      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup', onUp);
    });
  }

  enableDrag();
  enableResize();

  function updateWeightStatus() {
    const total = state.collection.totalWeight();
    const node = query('#lude_weight_status');
    if (node) {
      node.textContent = '';
      node.style.display = 'none';
      node.className = `lude-helper ${Math.abs(total - 1) <= 0.001 ? 'lude-status-success' : 'lude-status-danger'}`;
    }
    const legacyInput = query('#lude_input');
    if (legacyInput) {
      legacyInput.value = state.collection.toLegacyText();
    }
  }

  function readSavedPortfolios() {
    try {
      const raw = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
      if (!Array.isArray(raw)) {
        return [];
      }
      let migratedCount = 0;
      const normalized = raw
        .filter((item) => item && typeof item === 'object')
        .slice(0, 10)
        .map((item) => {
          const normalizedPeriod = normalizeRebalancePeriod(item.rebalancePeriod);
          if (!Object.prototype.hasOwnProperty.call(item, 'rebalancePeriod') || item.rebalancePeriod !== normalizedPeriod) {
            migratedCount += 1;
          }
          return { ...item, rebalancePeriod: normalizedPeriod };
        });
      if (migratedCount > 0 || normalized.length !== raw.length) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(normalized));
      }
      return normalized;
    } catch (error) {
      return [];
    }
  }

  function renderConfigRows() {
    const container = query('#lude_config_list');
    if (!container) {
      return;
    }
    if (!state.collection.items.length) {
      state.collection.addBlank();
    }
    container.innerHTML = state.collection.items.map((item, index) => `
      <div class="lude-config-row" data-index="${index}">
        <input class="lude-input lude-config-name" data-field="name" value="${escapeHtml(item.name)}" placeholder="策略名称">
        <input class="lude-input weight lude-config-weight" data-field="weight" type="number" min="0" max="100" step="0.01" value="${item.name ? item.percent() : ''}" placeholder="权重">
        <span style="color:${THEME.primaryDark};font-size:11px">%</span>
        <button class="lude-btn danger lude-config-remove" data-action="remove">删除</button>
      </div>
    `).join('');
    updateWeightStatus();
  }

  function renderSavedPortfolios() {
    const saved = readSavedPortfolios();
    const container = query('#lude_saved_list');
    if (!container) {
      return;
    }
    if (!saved.length) {
      container.innerHTML = '<div class="lude-helper">暂无保存的组合</div>';
      return;
    }
    container.innerHTML = saved.map((item, index) => `
      <div class="lude-config-row" style="align-items:flex-start;flex-direction:column;background:${THEME.surface};padding:8px;border:1px solid ${THEME.neutralBorder};border-radius:8px">
        <div style="font-weight:700;color:${THEME.neutralStrong}">${escapeHtml(item.name)}</div>
        <div class="lude-helper">${escapeHtml((item.strategies || []).map((strategy) => strategy.n).join('、'))}</div>
        <div class="lude-inline-actions" style="margin-top:4px">
          <button class="lude-btn secondary" data-action="load" data-index="${index}">加载</button>
          <button class="lude-btn danger" data-action="delete" data-index="${index}">删除</button>
        </div>
      </div>
    `).join('');
  }

  function toggleSection(sectionId) {
    const section = query(sectionId);
    if (!section) {
      return;
    }
    section.classList.toggle('lude-hidden');
  }

  function extractCurrentStrategyName() {
    const candidateTexts = [];
    const menu = query('#lude-menu-content');
    if (menu) {
      candidateTexts.push(menu.innerText || menu.textContent || '');
      queryAll('[title],[aria-label],span,div,strong,b', menu).forEach((node) => {
        if (node.closest(`#${PANEL_ID}`)) {
          return;
        }
        const text = (node.innerText || node.textContent || node.getAttribute('title') || node.getAttribute('aria-label') || '').trim();
        if (!text) {
          return;
        }
        if (text.includes('当前策略') || text.includes('策略名称') || text.includes('已选策略') || text.includes('选中策略')) {
          candidateTexts.push(text);
        }
      });
    }
    queryAll('span,div').forEach((node) => {
      if (node.closest(`#${PANEL_ID}`)) {
        return;
      }
      const text = (node.innerText || node.textContent || '').trim();
      if (text.includes('当前策略') || text.includes('策略名称') || text.includes('已选策略') || text.includes('选中策略')) {
        candidateTexts.push(text);
      }
    });
    for (const candidate of candidateTexts) {
      const match = candidate.match(/(?:当前策略|策略名称|已选策略|选中策略)[:：]?\s*([^\n\r]+)/);
      if (match && match[1]) {
        const strategyName = stripStrategyNoise(match[1]);
        if (strategyName) {
          return strategyName;
        }
      }
    }
    return null;
  }

  function getFallbackStrategyName() {
    const preferred = extractCurrentStrategyName();
    if (preferred) {
      return preferred;
    }
    const path = location.pathname || '';
    const preferredKeys = [];
    if (path.includes('/convertible-bond')) {
      preferredKeys.push('cbStrategy');
    }
    if (path.includes('/stock')) {
      preferredKeys.push('stkStrategy');
    }
    if (path.includes('/etf')) {
      preferredKeys.push('etfStrategy');
    }
    ['cbStrategy', 'stkStrategy', 'etfStrategy'].forEach((key) => {
      if (!preferredKeys.includes(key)) {
        preferredKeys.push(key);
      }
    });
    for (const key of preferredKeys) {
      try {
        const raw = localStorage.getItem(key);
        if (!raw) {
          continue;
        }
        const parsed = JSON.parse(raw);
        const strategyName = stripStrategyNoise(parsed && parsed.strategy_name);
        if (strategyName) {
          return strategyName;
        }
      } catch (error) {
      }
    }
    return null;
  }

  function captureCurrentStrategy() {
    const strategyName = getFallbackStrategyName();
    if (!strategyName) {
      log('当前页面未检测到可抓取的策略名称，请手动填写', 'e');
      return;
    }
    const result = state.collection.addOrFill(strategyName);
    if (!result.added) {
      log(result.reason === 'exists' ? `策略已存在：${strategyName}` : '未抓到有效策略名', result.reason === 'exists' ? 'w' : 'e');
      return;
    }
    if (state.collection.namedItems().every((item) => item.weight === 0)) {
      state.collection.rebalanceEvenly();
    }
    renderConfigRows();
    log(`已抓取策略：${strategyName}`, 's');
  }

  function hideAnalysis() {
    ['#lude_chart', '#lude_summary', '#lude_corr', '#lude_allocation', '#lude_yearly'].forEach((selector) => {
      const section = query(selector);
      if (section) {
        section.classList.add('lude-hidden');
      }
    });
  }

  function startMonitoring() {
    const validation = state.collection.validate();
    if (!validation.ok) {
      log(validation.message, 'e');
      return;
    }
    installRuntimeHooks();
    state.collected = {};
    state.monitoring = true;
    hideAnalysis();
    log('开始监听', 's');
    state.collection.namedItems().forEach((item, index) => {
      log(`${index + 1}. ${item.name}: ${item.percent()}%`, 'i');
    });
    log('请依次回测股票和转债策略', 'i');
  }

  function resetState() {
    state.monitoring = false;
    state.collected = {};
    uninstallRuntimeHooks('reset');
    const logNode = query('#lude_log');
    if (logNode) {
      logNode.innerHTML = '等待开始';
    }
    hideAnalysis();
    updateWeightStatus();
    log('已重置分析结果，策略配置已保留', 'i');
  }

  function savePortfolio() {
    const namedItems = state.collection.namedItems();
    if (!namedItems.length) {
      log('没有可保存的组合', 'e');
      return;
    }
    const weightName = namedItems.map((item) => `${item.name}${roundNumber(item.weight * 100, 0)}%`).join('_');
    const portfolioName = `组合_${weightName}_${new Date().toLocaleDateString('zh-CN').replace(/\//g, '-')}`;
    const saved = readSavedPortfolios();
    const payload = {
      name: portfolioName,
      strategies: state.collection.serialize(),
      data: state.collected,
      rebalancePeriod: state.rebalancePeriod,
      time: Date.now()
    };
    saved.unshift(payload);
    localStorage.setItem(STORAGE_KEY, JSON.stringify(saved.slice(0, 10)));
    const savedAfter = readSavedPortfolios();
    renderSavedPortfolios();
    log(`组合已保存：${portfolioName}`, 's');
  }

  function loadPortfolio(index) {
    const saved = readSavedPortfolios();
    const target = saved[index];
    if (!target) {
      log('组合不存在', 'e');
      return;
    }
    state.collection = new StrategyCollection(target.strategies || []);
    state.collected = target.data || {};
    state.rebalancePeriod = normalizeRebalancePeriod(target.rebalancePeriod);
    renderConfigRows();
    const rebalanceSelect = query('#lude_rebal_period');
    if (rebalanceSelect) {
      rebalanceSelect.value = state.rebalancePeriod;
    }
    if (Object.keys(state.collected).length === state.collection.namedItems().length) {
      refreshAnalysis();
      log(`已加载：${target.name}`, 's');
      log('历史数据已恢复', 's');
      return;
    }
    hideAnalysis();
    log(`已加载：${target.name}`, 's');
    log('需要重新回测以获取完整数据', 'w');
  }

  function deletePortfolio(index) {
    const saved = readSavedPortfolios();
    const target = saved[index];
    if (!target) {
      return;
    }
    saved.splice(index, 1);
    localStorage.setItem(STORAGE_KEY, JSON.stringify(saved));
    renderSavedPortfolios();
    log(`已删除：${target.name}`, 's');
  }

  function getRequestUrl(input) {
    if (!input) {
      return '';
    }
    if (typeof input === 'string') {
      return input;
    }
    if (input.url) {
      return input.url;
    }
    try {
      return String(input);
    } catch (_error) {
      return '';
    }
  }

  function isBacktestRequest(url) {
    return /\/(advanced-backtest|(?:stock|convertible-bond|etf)\/backtest|(?:stock|convertible-bond|etf)\/factors\/backtest)/.test(url || '');
  }

  function normalizeTimelines(rows) {
    return Array.isArray(rows)
      ? rows.filter((row) => row && typeof row === 'object' && (row.trade_time || row.trade_date))
      : [];
  }

  function extractTimelinesFromPayload(payload) {
    const candidates = [
      payload,
      payload && payload.timelines,
      payload && payload.data && payload.data.timelines,
      payload && payload.backtest && payload.backtest.timelines,
      payload && payload.result && payload.result.timelines
    ];
    const matched = candidates.find((candidate) => Array.isArray(candidate));
    return normalizeTimelines(matched);
  }

  function parseTimelinePayload(rawText, context) {
    try {
      return extractTimelinesFromPayload(JSON.parse(rawText));
    } catch (error) {
      return [];
    }
  }

  function parseTimelineLine(line) {
    const trimmed = String(line || '').trim();
    if (!trimmed.startsWith('data:')) {
      return [];
    }
    const payloadText = trimmed.slice(5).trim();
    if (!payloadText) {
      return [];
    }
    if (payloadText.startsWith('timelines=')) {
      return parseTimelinePayload(payloadText.slice(10), 'script.js:parseTimelineLine');
    }
    const delimiterIndex = payloadText.indexOf('=');
    if (delimiterIndex > 0) {
      const rawText = payloadText.slice(delimiterIndex + 1).trim();
      if (rawText.startsWith('{') || rawText.startsWith('[')) {
        return parseTimelinePayload(rawText, 'script.js:parseTimelineLine');
      }
      return [];
    }
    if (payloadText.startsWith('{') || payloadText.startsWith('[')) {
      return parseTimelinePayload(payloadText, 'script.js:parseTimelineLine');
    }
    return [];
  }

  function parseSSEText(text) {
    const source = String(text || '');
    const timelines = [];
    source.split(/\r?\n/).forEach((line) => {
      const rows = parseTimelineLine(line);
      if (rows.length) {
        timelines.push(...rows);
      }
    });
    if (timelines.length) {
      return timelines;
    }
    const trimmed = source.trim();
    if ((trimmed.startsWith('{') || trimmed.startsWith('[')) && trimmed.length) {
      return parseTimelinePayload(trimmed, 'script.js:parseSSEText');
    }
    return [];
  }

  function canBuildCombinedAnalysis(strategies) {
    if (!Array.isArray(strategies) || !strategies.length) {
      return false;
    }
    const baseLength = strategies[0].t.length;
    return baseLength >= 2 && strategies.every((strategy) => Array.isArray(strategy.t) && strategy.t.length === baseLength);
  }

  function renderCollectedPreview() {
    const strategies = getCollectedEntries();
    if (strategies.length < 2) {
      return;
    }
    if (!canBuildCombinedAnalysis(strategies)) {
      log('已收集多个策略，但没有足够的共同交易日，无法拼出组合。请确保各策略回测时间段有重叠', 'w');
      return;
    }
    const alignedDates = strategies[0].t;
    const firstDate = toDateKey(alignedDates[0]);
    const lastDate = toDateKey(alignedDates[alignedDates.length - 1]);
    log(`按共同交易日对齐：${firstDate} ~ ${lastDate}（${alignedDates.length}个交易日）`, 'i');
    renderChart();
    renderCorrelation();
  }

  function processTimelines(timelines, meta = {}) {
    const normalizedTimelines = normalizeTimelines(timelines);
    const detectedName = getFallbackStrategyName();
    let matched = state.collection.findMatch(detectedName);

    if (!normalizedTimelines.length) {
      log('回测响应已命中，但未解析到有效时间序列', 'w');
      return;
    }

    if (!matched) {
      const missing = getMissingNames();
      if (!detectedName && missing.length === 1) {
        matched = state.collection.namedItems().find((item) => item.name === missing[0]) || null;
        if (matched) {
          log(`自动匹配：${matched.name}`, 'w');
        }
      }
    }

    if (!matched) {
      log(detectedName ? `策略未匹配：${detectedName}` : '无法识别策略', 'e');
      return;
    }

    if (state.collected[matched.name]) {
      log(`已收集：${matched.name}`, 'w');
      return;
    }

    state.collected[matched.name] = { t: normalizedTimelines };
    log(`收集：${matched.name}（${normalizedTimelines.length}条）`, 's');

    const collectedCount = Object.keys(state.collected).length;
    if (collectedCount >= 2) {
      renderCollectedPreview();
    }

    const missing = getMissingNames();
    if (missing.length) {
      log(`待收集：${missing.join('、')}`, 'i');
      return;
    }

    log('全部完成！', 's');
    state.monitoring = false;
    uninstallRuntimeHooks('completed');
    refreshAnalysis();
  }

  function installFetchHook() {
    window.fetch = function patchedFetch(input, init) {
      const url = getRequestUrl(input);
      const matchedBacktest = isBacktestRequest(url);
      if (!matchedBacktest) {
        return nativeFetch(input, init);
      }

      return nativeFetch(input, init).then((response) => {
        let clone;
        try {
          clone = response.clone();
        } catch (error) {
          return response;
        }

        if (!clone.body || !clone.body.getReader) {
          return response;
        }

        const reader = clone.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';
        let parsed = false;

        function readStream() {
          return reader.read().then(({ done, value }) => {
            if (done) {
              if (!parsed) {
                const rows = parseSSEText(buffer);
                if (rows.length) {
                  parsed = true;
                  if (state.monitoring) {
                    processTimelines(rows, { source: 'fetch', url });
                  }
                }
              }
              return;
            }

            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop() || '';
            lines.forEach((line) => {
              const rows = parseTimelineLine(line);
              if (rows.length) {
                parsed = true;
                if (state.monitoring) {
                  processTimelines(rows, { source: 'fetch', url });
                }
              }
            });
            return readStream();
          }).catch((error) => {
          });
        }

        readStream();
        return response;
      });
    };
  }

  function installXHRHook() {
    if (!xhrProto || !originalXHROpen || !originalXHRSend) {
      return;
    }
    xhrProto.open = function patchedOpen(method, url) {
      this.__ludeMethod = method;
      this.__ludeUrl = url;
      return originalXHROpen.apply(this, arguments);
    };
    xhrProto.send = function patchedSend() {
      if (isBacktestRequest(this.__ludeUrl)) {
        this.addEventListener('loadend', () => {
          try {
            const rows = parseSSEText(this.responseText || '');
            if (!rows.length) {
              return;
            }
            if (state.monitoring) {
              processTimelines(rows, { source: 'xhr', url: this.__ludeUrl });
            }
          } catch (error) {
          }
        }, { once: true });
      }
      return originalXHRSend.apply(this, arguments);
    };
  }

  function installRuntimeHooks() {
    if (state.hooksInstalled) {
      return;
    }
    installFetchHook();
    installXHRHook();
    state.hooksInstalled = true;
  }

  function uninstallRuntimeHooks(reason) {
    if (!state.hooksInstalled) {
      return;
    }
    window.fetch = nativeFetch;
    if (xhrProto && originalXHROpen && originalXHRSend) {
      xhrProto.open = originalXHROpen;
      xhrProto.send = originalXHRSend;
    }
    state.hooksInstalled = false;
  }

  window.__ludeStockBondComboCleanup__ = function cleanup(reason = 'manual') {
    const activeInstanceId = window.__ludeStockBondComboActiveInstance__ || null;
    const mountedPanel = document.getElementById(PANEL_ID);
    const mountedStyle = document.getElementById(STYLE_ID);
    uninstallRuntimeHooks(`cleanup:${reason}`);
    if (mountedPanel) {
      mountedPanel.remove();
    }
    if (mountedStyle) {
      mountedStyle.remove();
    }
    if (activeInstanceId === INSTANCE_ID) {
      delete window.__ludeStockBondComboActiveInstance__;
    }
  };

  function metrics(timelines) {
    const first = timelines[0];
    const last = timelines[timelines.length - 1];
    const totalReturn = Number(last.total_return || 0);
    const years = Math.max((new Date(last.trade_time || last.trade_date) - new Date(first.trade_time || first.trade_date)) / 31557600000, 1 / 252);
    const annualReturn = Math.pow(1 + totalReturn, 1 / years) - 1;
    const maxDrawdown = Math.min(...timelines.map((point) => Number(point.drawdown || 0)));
    const returns = timelines.map((point) => Number(point.time_return || 0));
    const average = returns.reduce((sum, value) => sum + value, 0) / returns.length;
    const std = Math.sqrt(returns.reduce((sum, value) => sum + (value - average) ** 2, 0) / returns.length);
    return {
      总收益率: `${roundNumber(totalReturn * 100, 2)}%`,
      年化收益率: `${roundNumber(annualReturn * 100, 2)}%`,
      年化波动率: `${roundNumber(std * Math.sqrt(252) * 100, 2)}%`,
      最大回撤: `${roundNumber(maxDrawdown * 100, 2)}%`,
      夏普比: std ? roundNumber((average * 252) / (std * Math.sqrt(252)), 3).toFixed(3) : '0.000'
    };
  }

  function calculateYearlyReturns(timelines) {
    const yearly = {};
    timelines.forEach((point) => {
      const year = String(point.trade_time || point.trade_date).slice(0, 4);
      if (!yearly[year]) {
        yearly[year] = { endReturn: 0, returns: [], drawdowns: [] };
      }
      yearly[year].endReturn = Number(point.total_return || 0);
      yearly[year].returns.push(Number(point.time_return || 0));
      yearly[year].drawdowns.push(Number(point.drawdown || 0));
    });
    const years = Object.keys(yearly).sort();
    let previousEnd = 0;
    years.forEach((year, index) => {
      const current = yearly[year];
      current.yearlyReturn = index === 0 ? current.endReturn : (1 + current.endReturn) / (1 + previousEnd) - 1;
      const average = current.returns.reduce((sum, value) => sum + value, 0) / current.returns.length;
      const variance = current.returns.reduce((sum, value) => sum + (value - average) ** 2, 0) / current.returns.length;
      current.volatility = Math.sqrt(variance) * Math.sqrt(252);
      current.maxDrawdown = Math.min(...current.drawdowns);
      previousEnd = current.endReturn;
    });
    return yearly;
  }

  function calculateMonthlyReturns(timelines) {
    const monthly = {};
    timelines.forEach((point) => {
      const month = String(point.trade_time || point.trade_date).slice(0, 7);
      if (!monthly[month]) {
        monthly[month] = { endReturn: 0, returns: [], drawdowns: [] };
      }
      monthly[month].endReturn = Number(point.total_return || 0);
      monthly[month].returns.push(Number(point.time_return || 0));
      monthly[month].drawdowns.push(Number(point.drawdown || 0));
    });
    const months = Object.keys(monthly).sort();
    let previousEnd = 0;
    months.forEach((month, index) => {
      const current = monthly[month];
      current.monthlyReturn = index === 0 ? current.endReturn : (1 + current.endReturn) / (1 + previousEnd) - 1;
      const average = current.returns.reduce((sum, value) => sum + value, 0) / current.returns.length;
      const variance = current.returns.reduce((sum, value) => sum + (value - average) ** 2, 0) / current.returns.length;
      current.volatility = Math.sqrt(variance) * Math.sqrt(21);
      current.maxDrawdown = Math.min(...current.drawdowns);
      previousEnd = current.endReturn;
    });
    return monthly;
  }

  function correlation(seriesA, seriesB) {
    const length = Math.min(seriesA.length, seriesB.length);
    if (!length) {
      return 0;
    }
    const left = seriesA.slice(-length);
    const right = seriesB.slice(-length);
    const averageA = left.reduce((sum, value) => sum + value, 0) / length;
    const averageB = right.reduce((sum, value) => sum + value, 0) / length;
    let numerator = 0;
    let denominatorA = 0;
    let denominatorB = 0;
    for (let index = 0; index < length; index += 1) {
      numerator += (left[index] - averageA) * (right[index] - averageB);
      denominatorA += (left[index] - averageA) ** 2;
      denominatorB += (right[index] - averageB) ** 2;
    }
    return numerator / Math.sqrt(denominatorA * denominatorB || 1);
  }

  function calculateCorrelation(strategies, period) {
    const returns = strategies.map((strategy) => {
      const series = strategy.t.map((point) => Number(point.time_return || 0));
      if (period === 'all') {
        return series;
      }
      return series.slice(-Number(period));
    });
    const matrix = [];
    for (let row = 0; row < strategies.length; row += 1) {
      matrix[row] = [];
      for (let column = 0; column < strategies.length; column += 1) {
        if (row === column) {
          matrix[row][column] = 1;
        } else if (column > row) {
          matrix[row][column] = correlation(returns[row], returns[column]);
        } else {
          matrix[row][column] = matrix[column][row];
        }
      }
    }
    return matrix;
  }

  function calcPortfolio(strategies, rebalance, rebalancePeriod = state.rebalancePeriod) {
    const dates = strategies[0].t.map((point) => point.trade_time || point.trade_date);
    if (!rebalance) {
      const result = dates.map((date, index) => {
        let equity = 0;
        let timeReturn = 0;
        strategies.forEach((strategy) => {
          const point = strategy.t[index];
          if (!point) {
            throw new Error(`timeline point missing: ${strategy.n} @ ${index}`);
          }
          equity += Number(point.equity || 0) * strategy.w;
          timeReturn += Number(point.time_return || 0) * strategy.w;
        });
        return { trade_time: date, equity, time_return: timeReturn };
      });
      let maxEquity = result[0].equity;
      result.forEach((point, index) => {
        point.total_return = index === 0 ? point.time_return : (1 + result[index - 1].total_return) * (1 + point.time_return) - 1;
        maxEquity = Math.max(maxEquity, point.equity);
        point.drawdown = maxEquity ? (point.equity - maxEquity) / maxEquity : 0;
      });
      return result;
    }
    const totalWeight = strategies.reduce((sum, strategy) => sum + strategy.w, 0);
    const equities = {};
    let baseEquity = 1000000;
    strategies.forEach((strategy) => {
      equities[strategy.n] = baseEquity * (strategy.w / totalWeight);
    });
    const result = [];
    let lastRebalance = 0;
    const rebalanceDays = getRebalanceDays(rebalancePeriod);
    dates.forEach((date, index) => {
      strategies.forEach((strategy) => {
        const point = strategy.t[index];
        if (!point) {
          throw new Error(`timeline point missing: ${strategy.n} @ ${index}`);
        }
        equities[strategy.n] *= 1 + Number(point.time_return || 0);
      });
      const total = Object.values(equities).reduce((sum, value) => sum + value, 0);
      if (index - lastRebalance >= rebalanceDays && index > 0) {
        strategies.forEach((strategy) => {
          equities[strategy.n] = total * strategy.w;
        });
        lastRebalance = index;
      }
      const timeReturn = strategies.reduce((sum, strategy) => {
        const point = strategy.t[index];
        return sum + Number((point && point.time_return) || 0) * (equities[strategy.n] / total);
      }, 0);
      result.push({ trade_time: date, equity: total, time_return: timeReturn });
    });
    let maxEquity = result[0].equity;
    result.forEach((point, index) => {
      point.total_return = index === 0 ? point.time_return : (1 + result[index - 1].total_return) * (1 + point.time_return) - 1;
      maxEquity = Math.max(maxEquity, point.equity);
      point.drawdown = maxEquity ? (point.equity - maxEquity) / maxEquity : 0;
    });
    return result;
  }

  function calculateVolatilityMatching(strategies) {
    const volatilities = strategies.map((strategy) => {
      const returns = strategy.t.map((point) => Number(point.time_return || 0));
      const average = returns.reduce((sum, value) => sum + value, 0) / returns.length;
      const variance = returns.reduce((sum, value) => sum + (value - average) ** 2, 0) / returns.length;
      return Math.sqrt(variance) * Math.sqrt(252);
    });
    const inverseSum = volatilities.reduce((sum, value) => sum + 1 / value, 0);
    return volatilities.map((value) => (1 / value) / inverseSum);
  }

  function calculateRiskParity(strategies) {
    const detail = strategies.map((strategy) => {
      const returns = strategy.t.map((point) => Number(point.time_return || 0));
      const average = returns.reduce((sum, value) => sum + value, 0) / returns.length;
      const variance = returns.reduce((sum, value) => sum + (value - average) ** 2, 0) / returns.length;
      const volatility = Math.sqrt(variance) * Math.sqrt(252);
      const sharpe = volatility ? (average * 252) / volatility : 0;
      return { volatility, sharpeSquared: sharpe ** 2 };
    });
    const sharpeSquaredSum = detail.reduce((sum, item) => sum + item.sharpeSquared, 0) || 1;
    const budgetOverVol = detail.map((item) => (item.sharpeSquared / sharpeSquaredSum) / (item.volatility || 1));
    const total = budgetOverVol.reduce((sum, value) => sum + value, 0) || 1;
    return budgetOverVol.map((value) => value / total);
  }

  function drawLineChart(canvas, series, labels) {
    if (!canvas) {
      return;
    }
    const ctx = canvas.getContext('2d');
    const width = canvas.width;
    const height = canvas.height;
    const padding = 40;
    ctx.clearRect(0, 0, width, height);
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, width, height);

    const values = [0];
    series.forEach((item) => item.values.forEach((value) => values.push(value)));
    let min = Math.min(...values);
    let max = Math.max(...values);
    if (min === max) {
      min -= 10;
      max += 10;
    }
    const range = max - min;
    const scaleY = (height - padding * 2) / range;
    const scaleX = (width - padding * 2) / Math.max(labels.length - 1, 1);
    const toX = (index) => padding + index * scaleX;
    const toY = (value) => height - padding - (value - min) * scaleY;

    ctx.strokeStyle = THEME.neutralBorder;
    ctx.lineWidth = 1;
    for (let step = 0; step <= 4; step += 1) {
      const y = padding + ((height - padding * 2) / 4) * step;
      ctx.beginPath();
      ctx.moveTo(padding, y);
      ctx.lineTo(width - padding, y);
      ctx.stroke();
      const labelValue = max - (range / 4) * step;
      ctx.fillStyle = THEME.textMuted;
      ctx.font = '10px sans-serif';
      ctx.textAlign = 'right';
      ctx.fillText(`${roundNumber(labelValue, 0)}%`, padding - 4, y + 3);
    }

    ctx.strokeStyle = THEME.neutralStrong;
    ctx.beginPath();
    ctx.moveTo(padding, toY(0));
    ctx.lineTo(width - padding, toY(0));
    ctx.stroke();

    series.forEach((item) => {
      ctx.strokeStyle = item.color;
      ctx.lineWidth = 2;
      ctx.beginPath();
      item.values.forEach((value, index) => {
        const x = toX(index);
        const y = toY(value);
        if (index === 0) {
          ctx.moveTo(x, y);
        } else {
          ctx.lineTo(x, y);
        }
      });
      ctx.stroke();
    });
  }

  function renderChart() {
    const strategies = getCollectedEntries();
    if (!strategies.length) {
      return;
    }
    const rebalanceLabel = getRebalanceLabel(state.rebalancePeriod);
    if (!canBuildCombinedAnalysis(strategies)) {
      const chartSeries = strategies.map((strategy, index) => ({
        name: strategy.n,
        color: THEME.series[index % THEME.series.length],
        values: strategy.t.map((point) => Number(point.total_return || 0) * 100)
      }));
      drawLineChart(query('#lude_canvas'), chartSeries, strategies[0].t.map((point) => point.trade_time || point.trade_date));
      query('#lude_legend').innerHTML = `${chartSeries.map((item) => `<span style="color:${item.color}">■</span>${escapeHtml(item.name)}`).join(' ')} <span class="lude-helper">时间序列未完全对齐，暂不展示组合线</span>`;
      query('#lude_chart').classList.remove('lude-hidden');
      return;
    }
    const portfolio = calcPortfolio(strategies, false);
    const rebalanced = calcPortfolio(strategies, true, state.rebalancePeriod);
    const chartSeries = strategies.map((strategy, index) => ({
      name: strategy.n,
      color: THEME.series[index % THEME.series.length],
      values: strategy.t.map((point) => Number(point.total_return || 0) * 100)
    }));
    chartSeries.push({ name: '组合', color: THEME.portfolioLine, values: portfolio.map((point) => point.total_return * 100) });
    chartSeries.push({ name: rebalanceLabel, color: THEME.rebalanceLine, values: rebalanced.map((point) => point.total_return * 100) });
    drawLineChart(query('#lude_canvas'), chartSeries, portfolio.map((point) => point.trade_time));
    query('#lude_legend').innerHTML = chartSeries.map((item) => `<span style="color:${item.color}">■</span>${escapeHtml(item.name)}`).join(' ');
    query('#lude_chart').classList.remove('lude-hidden');
  }

  function renderSummary() {
    const strategies = getCollectedEntries();
    if (!strategies.length || !canBuildCombinedAnalysis(strategies)) {
      return;
    }
    const rebalanceLabel = getRebalanceLabel(state.rebalancePeriod);
    const portfolio = calcPortfolio(strategies, false);
    const rebalanced = calcPortfolio(strategies, true, state.rebalancePeriod);
    const portfolioMetrics = metrics(portfolio);
    const rebalancedMetrics = metrics(rebalanced);
    const strategyMetrics = strategies.map((strategy) => metrics(strategy.t));

    const html = [`<table><tr><th>指标</th>${strategies.map((strategy) => `<th>${escapeHtml(strategy.n)}</th>`).join('')}<th style="background:${THEME.portfolioSoft}">组合</th><th style="background:${THEME.neutralFill}">${rebalanceLabel}</th></tr>`];
    html.push(`<tr><td>权重</td>${strategies.map((strategy, index) => `<td style="text-align:center"><input class="lude-summary-weight" data-summary-index="${index}" type="number" min="0" max="100" step="0.01" value="${roundNumber(strategy.w * 100, 2)}">%</td>`).join('')}<td style="text-align:center;background:${THEME.portfolioSoft}">${roundNumber(strategies.reduce((sum, strategy) => sum + strategy.w, 0) * 100, 2)}%</td><td style="text-align:center;background:${THEME.neutralFill}">100%</td></tr>`);
    ['总收益率', '年化收益率', '年化波动率', '最大回撤', '夏普比'].forEach((label) => {
      html.push(`<tr><td>${label}</td>${strategyMetrics.map((item) => `<td style="text-align:right">${item[label]}</td>`).join('')}<td style="text-align:right;background:${THEME.portfolioSoft}">${portfolioMetrics[label]}</td><td style="text-align:right;background:${THEME.neutralFill}">${rebalancedMetrics[label]}</td></tr>`);
    });
    html.push('</table>');
    query('#lude_table').innerHTML = html.join('');
    query('#lude_summary').classList.remove('lude-hidden');
  }

  function renderCorrelation() {
    const strategies = getCollectedEntries();
    if (strategies.length < 2) {
      return;
    }
    const matrix = calculateCorrelation(strategies, state.correlationPeriod);
    const rows = ['<div class="lude-inline-actions"><label>时间周期：<select id="lude_corr_period"><option value="all">全部时间</option><option value="30">最近30天</option><option value="60">最近60天</option><option value="120">最近120天</option><option value="250">最近250天</option></select></label></div>'];
    rows.push(`<table><tr><th></th>${strategies.map((strategy) => `<th>${escapeHtml(strategy.n)}</th>`).join('')}</tr>`);
    strategies.forEach((strategy, rowIndex) => {
      rows.push(`<tr><td style="font-weight:700;background:${THEME.neutralFill}">${escapeHtml(strategy.n)}</td>${strategies.map((_, columnIndex) => {
        const value = matrix[rowIndex][columnIndex];
        const tone = getCorrelationTone(value, rowIndex === columnIndex);
        return `<td style="text-align:center;background:${tone.background};color:${tone.color}">${roundNumber(value * 100, 1)}%</td>`;
      }).join('')}</tr>`);
    });
    rows.push('</table><div class="lude-helper">颜色说明：红色=正相关，绿色=负相关，白色=弱相关/对角线</div>');
    query('#lude_corr_table').innerHTML = rows.join('');
    const periodSelect = query('#lude_corr_period');
    if (periodSelect) {
      periodSelect.value = state.correlationPeriod;
    }
    query('#lude_corr').classList.remove('lude-hidden');
  }

  function renderAllocation() {
    const strategies = getCollectedEntries();
    if (strategies.length < 2 || !canBuildCombinedAnalysis(strategies)) {
      return;
    }
    const volatilityWeights = calculateVolatilityMatching(strategies);
    const riskParityWeights = calculateRiskParity(strategies);
    const volatilityStrategies = strategies.map((strategy, index) => ({ ...strategy, w: volatilityWeights[index] }));
    const riskParityStrategies = strategies.map((strategy, index) => ({ ...strategy, w: riskParityWeights[index] }));
    const volatilityMetrics = metrics(calcPortfolio(volatilityStrategies, false));
    const riskParityMetrics = metrics(calcPortfolio(riskParityStrategies, false));

    const html = [`<table><tr><th>方法</th>${strategies.map((strategy) => `<th>${escapeHtml(strategy.n)}</th>`).join('')}<th>年化收益</th><th>年化波动</th><th>最大回撤</th><th>夏普比</th></tr>`];
    html.push(`<tr><td>波动率配平</td>${volatilityWeights.map((weight) => `<td style="text-align:center">${roundNumber(weight * 100, 1)}%</td>`).join('')}<td style="text-align:right">${volatilityMetrics.年化收益率}</td><td style="text-align:right">${volatilityMetrics.年化波动率}</td><td style="text-align:right">${volatilityMetrics.最大回撤}</td><td style="text-align:right">${volatilityMetrics.夏普比}</td></tr>`);
    html.push(`<tr><td>风险平价</td>${riskParityWeights.map((weight) => `<td style="text-align:center">${roundNumber(weight * 100, 1)}%</td>`).join('')}<td style="text-align:right">${riskParityMetrics.年化收益率}</td><td style="text-align:right">${riskParityMetrics.年化波动率}</td><td style="text-align:right">${riskParityMetrics.最大回撤}</td><td style="text-align:right">${riskParityMetrics.夏普比}</td></tr>`);
    html.push('</table>');
    html.push('<div class="lude-helper"><strong>说明：</strong>波动率配平采用逆波动率分配；风险平价采用夏普平方与波动率联合分配。</div>');
    query('#lude_allocation_content').innerHTML = html.join('');
    query('#lude_allocation').classList.remove('lude-hidden');
  }

  function renderYearlyStats() {
    const strategies = getCollectedEntries();
    if (!strategies.length || !canBuildCombinedAnalysis(strategies)) {
      return;
    }
    const portfolio = calcPortfolio(strategies, false);
    const allYearData = {};
    const allMonthData = {};
    strategies.forEach((strategy) => {
      allYearData[strategy.n] = calculateYearlyReturns(strategy.t);
      allMonthData[strategy.n] = calculateMonthlyReturns(strategy.t);
    });
    allYearData['组合'] = calculateYearlyReturns(portfolio);
    allMonthData['组合'] = calculateMonthlyReturns(portfolio);

    const years = Object.keys(allYearData['组合']).sort();
    const months = Object.keys(allMonthData['组合']).sort();
    const html = [];

    if (years.length) {
      html.push('<table><tr><th rowspan="2">年份</th>');
      strategies.forEach((strategy) => {
        html.push(`<th colspan="2">${escapeHtml(strategy.n)}</th>`);
      });
      html.push('<th colspan="2" style="background:${THEME.portfolioSoft}">组合</th></tr><tr>');
      strategies.forEach(() => {
        html.push('<th>收益</th><th>回撤</th>');
      });
      html.push('<th style="background:${THEME.portfolioSoft}">收益</th><th style="background:${THEME.portfolioSoft}">回撤</th></tr>');
      years.forEach((year) => {
        html.push(`<tr><td style="font-weight:700">${year}</td>`);
        strategies.forEach((strategy) => {
          const row = allYearData[strategy.n][year];
          html.push(`<td style="text-align:right;color:${row ? getReturnColor(row.yearlyReturn) : THEME.textMuted}">${row ? `${roundNumber(row.yearlyReturn * 100, 2)}%` : '-'}</td>`);
          html.push(`<td style="text-align:right;color:${THEME.financialGreen}">${row ? `${roundNumber(row.maxDrawdown * 100, 2)}%` : '-'}</td>`);
        });
        const combined = allYearData['组合'][year];
        html.push(`<td style="text-align:right;background:${THEME.portfolioSoft};color:${getReturnColor(combined.yearlyReturn)}">${roundNumber(combined.yearlyReturn * 100, 2)}%</td>`);
        html.push(`<td style="text-align:right;background:${THEME.portfolioSoft};color:${THEME.financialGreen}">${roundNumber(combined.maxDrawdown * 100, 2)}%</td></tr>`);
      });
      html.push('</table>');
    }

    if (months.length) {
      html.push(`<div class="lude-section-title" style="margin-top:10px">📅 月度收益统计</div>`);
      html.push('<div style="max-height:300px;overflow:auto"><table><tr><th rowspan="2">月份</th>');
      strategies.forEach((strategy) => {
        html.push(`<th colspan="2">${escapeHtml(strategy.n)}</th>`);
      });
      html.push('<th colspan="2" style="background:${THEME.portfolioSoft}">组合</th></tr><tr>');
      strategies.forEach(() => {
        html.push('<th>收益</th><th>回撤</th>');
      });
      html.push('<th style="background:${THEME.portfolioSoft}">收益</th><th style="background:${THEME.portfolioSoft}">回撤</th></tr>');
      months.forEach((month) => {
        html.push(`<tr><td style="font-weight:700">${month}</td>`);
        strategies.forEach((strategy) => {
          const row = allMonthData[strategy.n][month];
          html.push(`<td style="text-align:right;color:${row ? getReturnColor(row.monthlyReturn) : THEME.textMuted}">${row ? `${roundNumber(row.monthlyReturn * 100, 2)}%` : '-'}</td>`);
          html.push(`<td style="text-align:right;color:${THEME.financialGreen}">${row ? `${roundNumber(row.maxDrawdown * 100, 2)}%` : '-'}</td>`);
        });
        const combined = allMonthData['组合'][month];
        html.push(`<td style="text-align:right;background:${THEME.portfolioSoft};color:${getReturnColor(combined.monthlyReturn)}">${roundNumber(combined.monthlyReturn * 100, 2)}%</td>`);
        html.push(`<td style="text-align:right;background:${THEME.portfolioSoft};color:${THEME.financialGreen}">${roundNumber(combined.maxDrawdown * 100, 2)}%</td></tr>`);
      });
      html.push('</table></div>');
    }

    query('#lude_yearly_content').innerHTML = html.join('');
    query('#lude_yearly').classList.remove('lude-hidden');
  }

  function refreshAnalysis() {
    try {
      renderChart();
      renderSummary();
      renderCorrelation();
      renderAllocation();
      renderYearlyStats();
    } catch (error) {
      log(`分析渲染失败：${error.message}`, 'e');
      throw error;
    }
  }

  function exportCSV() {
    const strategies = getCollectedEntries();
    if (!strategies.length) {
      log('无可导出数据', 'e');
      return;
    }
    const rebalanceLabel = getRebalanceLabel(state.rebalancePeriod);
    const portfolio = calcPortfolio(strategies, false);
    const rebalanced = calcPortfolio(strategies, true, state.rebalancePeriod);
    const dates = strategies[0].t.map((point) => point.trade_time || point.trade_date);
    const header = ['日期'];
    strategies.forEach((strategy) => {
      header.push(`${strategy.n}_净值`, `${strategy.n}_收益率`);
    });
    header.push('组合净值', '组合收益率', '组合累计', '组合回撤', `${rebalanceLabel}净值`, `${rebalanceLabel}收益率`, `${rebalanceLabel}累计`, `${rebalanceLabel}回撤`);
    const rows = dates.map((date, index) => {
      const row = [String(date).split(' ')[0]];
      strategies.forEach((strategy) => {
        const point = strategy.t[index];
        row.push(Number(point.equity || 0).toFixed(2), `${roundNumber(Number(point.time_return || 0) * 100, 4)}%`);
      });
      const point = portfolio[index];
      const rebalancedPoint = rebalanced[index];
      row.push(point.equity.toFixed(2), `${roundNumber(point.time_return * 100, 4)}%`, `${roundNumber(point.total_return * 100, 4)}%`, `${roundNumber(point.drawdown * 100, 4)}%`);
      row.push(rebalancedPoint.equity.toFixed(2), `${roundNumber(rebalancedPoint.time_return * 100, 4)}%`, `${roundNumber(rebalancedPoint.total_return * 100, 4)}%`, `${roundNumber(rebalancedPoint.drawdown * 100, 4)}%`);
      return row;
    });
    const summary = [['指标', ...strategies.map((strategy) => strategy.n), '组合', rebalanceLabel]];
    summary.push(['权重', ...strategies.map((strategy) => `${roundNumber(strategy.w * 100, 2)}%`), '100%', '100%']);
    ['总收益率', '年化收益率', '年化波动率', '最大回撤', '夏普比'].forEach((label) => {
      summary.push([label, ...strategies.map((strategy) => metrics(strategy.t)[label]), metrics(portfolio)[label], metrics(rebalanced)[label]]);
    });
    const csv = `\ufeff【每日明细】\n${header.join(',')}\n${rows.map((row) => row.join(',')).join('\n')}\n\n【汇总对比】\n${summary.map((row) => row.join(',')).join('\n')}`;
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
    const anchor = document.createElement('a');
    anchor.href = URL.createObjectURL(blob);
    anchor.download = `禄得股票转债组合_${Date.now()}.csv`;
    anchor.click();
    log('CSV 已导出', 's');
  }

  query('#lude_config_list').addEventListener('input', (event) => {
    const target = event.target;
    if (!(target instanceof HTMLElement)) {
      return;
    }
    const row = target.closest('.lude-config-row');
    if (!row) {
      return;
    }
    const index = Number(row.getAttribute('data-index'));
    const config = state.collection.items[index];
    if (!config) {
      return;
    }
    if (target.classList.contains('lude-config-name')) {
      config.rename(target.value);
    }
    if (target.classList.contains('lude-config-weight')) {
      config.setWeightFromPercent(target.value);
    }
    updateWeightStatus();
  });

  query('#lude_config_list').addEventListener('click', (event) => {
    const target = event.target;
    if (!(target instanceof HTMLElement) || target.getAttribute('data-action') !== 'remove') {
      return;
    }
    const row = target.closest('.lude-config-row');
    if (!row) {
      return;
    }
    state.collection.removeAt(Number(row.getAttribute('data-index')));
    renderConfigRows();
    log('已删除策略行', 'i');
  });

  query('#lude_saved_list').addEventListener('click', (event) => {
    const target = event.target;
    if (!(target instanceof HTMLElement)) {
      return;
    }
    const action = target.getAttribute('data-action');
    const index = Number(target.getAttribute('data-index'));
    if (action === 'load') {
      loadPortfolio(index);
    }
    if (action === 'delete') {
      deletePortfolio(index);
    }
  });

  query('#lude_summary').addEventListener('change', (event) => {
    const target = event.target;
    if (!(target instanceof HTMLElement) || !target.classList.contains('lude-summary-weight')) {
      return;
    }
    const index = Number(target.getAttribute('data-summary-index'));
    const strategy = state.collection.namedItems()[index];
    if (!strategy) {
      return;
    }
    strategy.setWeightFromPercent(target.value);
    updateWeightStatus();
    if (Math.abs(state.collection.totalWeight() - 1) > 0.001) {
      log('权重和需为 100%，已暂停刷新分析', 'w');
      return;
    }
    renderConfigRows();
    refreshAnalysis();
  });

  query('#lude_corr').addEventListener('change', (event) => {
    const target = event.target;
    if (!(target instanceof HTMLSelectElement) || target.id !== 'lude_corr_period') {
      return;
    }
    state.correlationPeriod = target.value;
    renderCorrelation();
  });

  query('#lude_saved_toggle').addEventListener('click', () => {
    toggleSection('#lude_saved_content');
  });

  query('#lude_expand').addEventListener('click', () => {
    const expanded = query('#lude_expand').textContent === '⬜';
    setExpanded(expanded);
  });

  query('#lude_close').addEventListener('click', () => {
    if (typeof window.__ludeStockBondComboCleanup__ === 'function') {
      window.__ludeStockBondComboCleanup__();
    }
  });

  query('#lude_capture').addEventListener('click', captureCurrentStrategy);
  query('#lude_add_row').addEventListener('click', () => {
    state.collection.addBlank();
    renderConfigRows();
    log('已新增策略行', 'i');
  });
  query('#lude_equal_weight').addEventListener('click', () => {
    state.collection.rebalanceEvenly();
    renderConfigRows();
    log('已均分权重', 's');
  });
  query('#lude_start').addEventListener('click', startMonitoring);
  query('#lude_save').addEventListener('click', savePortfolio);
  query('#lude_export').addEventListener('click', exportCSV);
  query('#lude_reset').addEventListener('click', resetState);
  query('#lude_rebal_period').addEventListener('change', (event) => {
    const target = event.target;
    if (!(target instanceof HTMLSelectElement)) {
      return;
    }
    state.rebalancePeriod = normalizeRebalancePeriod(target.value);
    if (Object.keys(state.collected).length === state.collection.namedItems().length && state.collection.namedItems().length >= 2) {
      refreshAnalysis();
    }
  });

  renderConfigRows();
  renderSavedPortfolios();
  const rebalanceSelect = query('#lude_rebal_period');
  if (rebalanceSelect) {
    rebalanceSelect.value = state.rebalancePeriod;
  }
  log(`插件已加载：${BUILD_TAG}`, 's');
  log('工具已加载，可抓取策略名称并配置组合权重', 's');
  log('点击“开始”后才会安装监听，完成或重置后自动卸载', 'i');
  log('请注意回测起始时间一定相同', 'w');
})();