Paper

Virtual trading simulator sidebar for Polymarket with unified settlement, watchlist, alerts, and backups

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Paper
// @namespace    http://tampermonkey.net/
// @version      3.2.0
// @description  Virtual trading simulator sidebar for Polymarket with unified settlement, watchlist, alerts, and backups
// @match        https://polymarket.com/*
// @match        https://www.polymarket.com/*
// @match        https://gamma.polymarket.com/*
// @match        https://clob.polymarket.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_notification
// @connect      gamma-api.polymarket.com
// @connect      clob.polymarket.com
// @connect      gamma.polymarket.com
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function (root) {
  'use strict';

  const STORAGE_KEY = 'polymarket_sim_v2';
  const UI_KEY = 'polymarket_sim_ui_v2';
  const MAX_HISTORY = 300;
  const MARKET_UI_THROTTLE_MS = 450;
  const PORTFOLIO_UI_DELAY_MS = 140;
  const MARKET_CACHE_MS = 300000;
  const MARKET_PRICE_CACHE_MS = 1200;
  const GAMMA_BASE = 'https://gamma-api.polymarket.com';
  const CLOB_BASE = 'https://clob.polymarket.com';
  const PRICE_TYPES = ['BUY', 'SELL', 'MID'];
  const HOLDINGS_SORTS = ['pnl_desc', 'value_desc', 'qty_desc', 'newest'];
  const PANEL_WIDTH = { DEFAULT: 760, MIN: 320, MAX: 760 };
  const PANEL_EDGE_OFFSET = 12;
  const PANEL_SNAP_GAP = 18;
  const MAIN_TABS = ['market', 'holdings', 'watchlist', 'history', 'alerts', 'settings'];

  function noop() {}

  function deepClone(value) {
    return value == null ? value : JSON.parse(JSON.stringify(value));
  }

  function roundNumber(value, digits) {
    if (!Number.isFinite(Number(value))) return 0;
    const factor = 10 ** (digits == null ? 6 : digits);
    return Math.round(Number(value) * factor) / factor;
  }

  function roundMoney(value) {
    return roundNumber(value, 6);
  }

  function clampPanelWidth(value) {
    return Math.max(PANEL_WIDTH.MIN, Math.min(PANEL_WIDTH.MAX, Number(value) || PANEL_WIDTH.DEFAULT));
  }

  function getDefaultPanelHeight() {
    const viewportHeight = Math.max(root.innerHeight || 0, 640);
    return Math.max(500, Math.min(980, Math.round(viewportHeight * 0.84)));
  }

  function clampPanelHeight(value) {
    const viewportHeight = Math.max(root.innerHeight || 0, 640);
    const maxHeight = Math.max(420, Math.min(960, viewportHeight - 20));
    return Math.max(360, Math.min(maxHeight, Number(value) || getDefaultPanelHeight()));
  }

  function clampPanelPosition(left, top, width, height) {
    const viewportWidth = Math.max(root.innerWidth || 0, 320);
    const viewportHeight = Math.max(root.innerHeight || 0, 240);
    const maxLeft = Math.max(0, viewportWidth - width - 8);
    const maxTop = Math.max(0, viewportHeight - Math.min(height, viewportHeight) - 8);
    return {
      left: Math.max(0, Math.min(maxLeft, left)),
      top: Math.max(0, Math.min(maxTop, top))
    };
  }

  function snapPanelPosition(left, top, width, height) {
    const clamped = clampPanelPosition(left, top, width, height);
    const viewportWidth = Math.max(root.innerWidth || 0, 320);
    const rightDistance = Math.abs((viewportWidth - width - PANEL_EDGE_OFFSET) - clamped.left);
    const leftDistance = Math.abs(PANEL_EDGE_OFFSET - clamped.left);
    let snappedLeft = clamped.left;
    if (leftDistance <= PANEL_SNAP_GAP) snappedLeft = PANEL_EDGE_OFFSET;
    else if (rightDistance <= PANEL_SNAP_GAP) snappedLeft = Math.max(0, viewportWidth - width - PANEL_EDGE_OFFSET);
    return clampPanelPosition(snappedLeft, clamped.top, width, height);
  }

  function pulseLiveValue(node, direction) {
    if (!node) return;
    node.classList.remove('psim-tick-up', 'psim-tick-down');
    void node.offsetWidth;
    node.classList.add(direction === 'down' ? 'psim-tick-down' : 'psim-tick-up');
    root.setTimeout(() => {
      node.classList.remove('psim-tick-up', 'psim-tick-down');
    }, 320);
  }

  function updateLiveText(node, nextText, numericValue, options) {
    if (!node) return;
    const config = options || {};
    const prevText = node.textContent;
    const prevValue = Number(node.dataset.value);
    if (prevText === nextText) return;
    node.textContent = nextText;
    if (Number.isFinite(numericValue)) {
      node.dataset.value = String(numericValue);
      if (config.pulse !== false && Number.isFinite(prevValue) && prevValue !== numericValue) {
        pulseLiveValue(node, numericValue < prevValue ? 'down' : 'up');
      }
    }
  }

  function bindOrderSizeControls(scope) {
    const rootNode = scope || root.document;
    const orderQtyInput = rootNode.querySelector('#psim-order-qty');
    if (!orderQtyInput) return;
    const chips = Array.from(rootNode.querySelectorAll('[data-order-size]'));
    const setOrderQuantity = (value) => {
      const nextQty = roundNumber(normalizePositiveNumber(value, getState().settings.defaultQuantity), 6);
      engine.patchUI({ orderQuantity: nextQty });
      orderQtyInput.value = String(nextQty);
      chips.forEach((chip) => {
        const chipValue = roundNumber(normalizePositiveNumber(chip.dataset.orderSize, 0), 6);
        chip.classList.toggle('active', Math.abs(chipValue - nextQty) < 0.000001);
      });
      return nextQty;
    };

    const persistOrderQty = () => setOrderQuantity(orderQtyInput.value);
    orderQtyInput.onchange = persistOrderQty;
    orderQtyInput.onblur = persistOrderQty;
    chips.forEach((btn) => {
      btn.onclick = (event) => {
        event.preventDefault();
        event.stopPropagation();
        setOrderQuantity(btn.dataset.orderSize);
      };
    });
  }

  function normalizePositiveNumber(value, fallback) {
    const parsed = typeof value === 'number' ? value : parseFloat(value);
    if (!Number.isFinite(parsed) || parsed <= 0) return fallback == null ? 0 : fallback;
    return parsed;
  }

  function normalizeBoolean(value, fallback) {
    if (typeof value === 'boolean') return value;
    if (value === 'true') return true;
    if (value === 'false') return false;
    return !!fallback;
  }

  function generateId(prefix) {
    return `${prefix || 'id'}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
  }

  function truncate(str, len) {
    const text = str == null ? '' : String(str);
    return text.length > len ? `${text.slice(0, len)}...` : text;
  }

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

  function fmt$(value) {
    return `$${roundNumber(value || 0, 4).toFixed(2)}`;
  }

  function fmtPct(value) {
    return `${roundNumber(value || 0, 2).toFixed(2)}%`;
  }

  function formatTime(value) {
    try {
      return new Date(value).toLocaleTimeString();
    } catch (_err) {
      return '';
    }
  }

  function formatHistoryAction(action) {
    if (action === 'BUY') return '买入';
    if (action === 'SELL') return '减仓';
    if (action === 'CLOSE') return '平仓';
    if (action === 'CLOSE_ALL') return '全平';
    return action || '—';
  }

  function getOutcomeSide(outcome) {
    return String(outcome || '').startsWith('NO: ') ? 'NO' : 'YES';
  }

  function getOutcomeDisplayName(outcome) {
    const text = String(outcome || 'Unknown');
    return text.startsWith('NO: ') ? text.slice(4) : text;
  }

  function getOrderQuantity(uiState, stateSnapshot) {
    return roundNumber(normalizePositiveNumber(uiState && uiState.orderQuantity, stateSnapshot && stateSnapshot.settings && stateSnapshot.settings.defaultQuantity), 6);
  }

  function getOrderSizePresets(stateSnapshot) {
    const sizes = [1, 5, 10, 25, 50, stateSnapshot && stateSnapshot.settings ? stateSnapshot.settings.defaultQuantity : 10];
    return [...new Set(sizes.map((value) => roundNumber(normalizePositiveNumber(value, 0), 6)).filter((value) => value > 0))].sort((left, right) => left - right);
  }

  function renderSideBadge(side, compact) {
    const normalized = side === 'NO' ? 'NO' : 'YES';
    return `<span class="psim-side-badge psim-side-${normalized.toLowerCase()}${compact ? ' psim-side-compact' : ''}">${normalized}</span>`;
  }

  function normalizeMainTab(value) {
    return MAIN_TABS.includes(value) ? value : 'market';
  }

  function parseArrayish(value) {
    if (Array.isArray(value)) return value;
    if (typeof value !== 'string' || !value.trim()) return [];
    try {
      const parsed = JSON.parse(value);
      if (Array.isArray(parsed)) return parsed;
    } catch (_err) {
      // noop
    }
    return value.split(',').map((item) => item.trim()).filter(Boolean);
  }

  function normalizeOutcomeList(value, fallbackConditionId) {
    const parsed = parseArrayish(value);
    if (parsed.length > 0) return parsed;
    return fallbackConditionId ? ['Yes', 'No'] : [];
  }

  function normalizeTokenIdList(raw) {
    if (Array.isArray(raw)) return raw.filter(Boolean);
    if (typeof raw === 'string' && raw.trim()) {
      try {
        const parsed = JSON.parse(raw);
        if (Array.isArray(parsed)) return parsed.filter(Boolean);
      } catch (_err) {
        return raw.split(',').map((item) => item.trim()).filter(Boolean);
      }
    }
    return [];
  }

  function defaultState() {
    return {
      initialBalance: 100,
      cash: 100,
      realizedPnl: 0,
      positions: [],
      history: [],
      alerts: [],
      watchlist: [],
      settings: {
        refreshSeconds: 5,
        defaultQuantity: 10,
        feeRate: 0.002,
        slippageRate: 0.01,
        soundEnabled: true,
        darkMode: true
      }
    };
  }

  function defaultUI() {
    return {
      collapsed: false,
      panelX: 0,
      panelY: 0,
      historyOpen: false,
      settingsOpen: false,
      alertsOpen: false,
      watchlistOpen: true,
      searchQuery: '',
      searchResults: [],
      searchOffset: 0,
      searchHasMore: false,
      pendingResetConfirm: false,
      selectedSearchMarket: null,
      alertDraft: null,
      orderQuantity: defaultState().settings.defaultQuantity,
      activeTab: 'market',
      holdingsSort: 'pnl_desc',
      panelWidth: PANEL_WIDTH.DEFAULT,
      panelHeight: getDefaultPanelHeight(),
      binarySide: 'YES'
    };
  }

  function hydratePosition(raw) {
    if (!raw || !raw.tokenId) return null;
    const parsedQuantity = Number(raw.quantity);
    const quantity = Number.isFinite(parsedQuantity) ? roundNumber(Math.max(0, parsedQuantity), 6) : 0;
    return {
      id: raw.id || generateId('pos'),
      marketSlug: raw.marketSlug || '',
      marketTitle: raw.marketTitle || '',
      tokenId: raw.tokenId,
      quantity,
      avgPrice: roundNumber(Math.max(0, Number(raw.avgPrice) || 0), 6),
      outcome: raw.outcome || 'Unknown',
      openedAt: raw.openedAt || new Date().toISOString(),
      lastPrice: raw.lastPrice == null || raw.lastPrice === '' ? null : roundNumber(Number(raw.lastPrice), 6),
      lastPriceType: PRICE_TYPES.includes(raw.lastPriceType) ? raw.lastPriceType : 'SELL'
    };
  }

  function hydrateAlert(raw) {
    if (!raw || !raw.tokenId) return null;
    return {
      id: raw.id || generateId('alert'),
      marketSlug: raw.marketSlug || '',
      tokenId: raw.tokenId,
      priceTarget: roundNumber(normalizePositiveNumber(raw.priceTarget, 0), 6),
      side: raw.side === 'BELOW' ? 'BELOW' : 'ABOVE',
      active: raw.active !== false,
      outcomeName: raw.outcomeName || 'Unknown',
      priceType: PRICE_TYPES.includes(raw.priceType) ? raw.priceType : 'BUY'
    };
  }

  function hydrateWatchlistItem(raw) {
    if (!raw || !raw.marketSlug) return null;
    return {
      id: raw.id || generateId('watch'),
      marketSlug: raw.marketSlug,
      marketTitle: raw.marketTitle || raw.marketSlug,
      addedAt: raw.addedAt || new Date().toISOString(),
      collapsed: raw.collapsed || false
    };
  }

  function hydrateHistoryEntry(raw) {
    if (!raw) return null;
    return {
      time: raw.time || new Date().toISOString(),
      action: raw.action || 'UNKNOWN',
      marketTitle: raw.marketTitle || '',
      outcome: raw.outcome || '',
      tokenId: raw.tokenId || '',
      quantity: roundNumber(Number(raw.quantity) || 0, 6),
      price: roundNumber(Number(raw.price) || 0, 6),
      grossValue: roundNumber(Number(raw.grossValue) || 0, 6),
      fee: roundNumber(Number(raw.fee) || 0, 6),
      netValue: roundNumber(Number(raw.netValue) || 0, 6),
      pnl: roundNumber(Number(raw.pnl) || 0, 6),
      cashAfter: roundNumber(Number(raw.cashAfter) || 0, 6),
      realizedAfter: roundNumber(Number(raw.realizedAfter) || 0, 6),
      positionAfter: roundNumber(Number(raw.positionAfter) || 0, 6)
    };
  }

  function hydrateState(raw) {
    const def = defaultState();
    const source = raw && typeof raw === 'object' ? raw : {};
    const settings = {
      ...def.settings,
      ...(source.settings || {})
    };

    settings.refreshSeconds = Math.min(60, Math.max(2, Math.round(normalizePositiveNumber(settings.refreshSeconds, def.settings.refreshSeconds))));
    settings.defaultQuantity = Math.max(1, roundNumber(normalizePositiveNumber(settings.defaultQuantity, def.settings.defaultQuantity), 6));
    settings.feeRate = Math.max(0, roundNumber(Number(settings.feeRate) || 0, 6));
    settings.slippageRate = Math.max(0, roundNumber(Number(settings.slippageRate) || 0, 6));
    settings.soundEnabled = normalizeBoolean(settings.soundEnabled, def.settings.soundEnabled);
    settings.darkMode = normalizeBoolean(settings.darkMode, def.settings.darkMode);

    return {
      initialBalance: roundMoney(normalizePositiveNumber(source.initialBalance, def.initialBalance)),
      cash: roundMoney(Number(source.cash != null ? source.cash : def.cash) || 0),
      realizedPnl: roundMoney(Number(source.realizedPnl != null ? source.realizedPnl : def.realizedPnl) || 0),
      positions: Array.isArray(source.positions) ? source.positions.map(hydratePosition).filter(Boolean) : [],
      history: Array.isArray(source.history) ? source.history.map(hydrateHistoryEntry).filter(Boolean).slice(0, MAX_HISTORY) : [],
      alerts: Array.isArray(source.alerts) ? source.alerts.map(hydrateAlert).filter(Boolean) : [],
      watchlist: Array.isArray(source.watchlist) ? source.watchlist.map(hydrateWatchlistItem).filter(Boolean) : [],
      settings
    };
  }

  function normalizeAlertDraft(raw) {
    if (!raw || !raw.tokenId) return null;
    const priceTarget = Number(raw.priceTarget);
    return {
      marketSlug: raw.marketSlug || '',
      marketTitle: raw.marketTitle || raw.marketSlug || '',
      tokenId: raw.tokenId,
      outcomeName: raw.outcomeName || 'Unknown',
      priceTarget: Number.isFinite(priceTarget) && priceTarget > 0 ? roundNumber(priceTarget, 6) : '',
      side: raw.side === 'BELOW' ? 'BELOW' : 'ABOVE',
      priceType: PRICE_TYPES.includes(raw.priceType) ? raw.priceType : 'MID'
    };
  }

  function hydrateUI(raw) {
    const def = defaultUI();
    const source = raw && typeof raw === 'object' ? raw : {};
    return {
      collapsed: normalizeBoolean(source.collapsed, def.collapsed),
      panelX: Math.max(0, Number(source.panelX) || 0),
      panelY: Math.max(0, Number(source.panelY) || 0),
      historyOpen: normalizeBoolean(source.historyOpen, def.historyOpen),
      settingsOpen: normalizeBoolean(source.settingsOpen, def.settingsOpen),
      alertsOpen: normalizeBoolean(source.alertsOpen, def.alertsOpen),
      watchlistOpen: normalizeBoolean(source.watchlistOpen, def.watchlistOpen),
      searchQuery: source.searchQuery ? String(source.searchQuery) : '',
      searchResults: Array.isArray(source.searchResults) ? source.searchResults.filter(Boolean) : [],
      searchOffset: Math.max(0, parseInt(source.searchOffset, 10) || 0),
      searchHasMore: normalizeBoolean(source.searchHasMore, def.searchHasMore),
      pendingResetConfirm: normalizeBoolean(source.pendingResetConfirm, def.pendingResetConfirm),
      selectedSearchMarket: source.selectedSearchMarket ? String(source.selectedSearchMarket) : null,
      alertDraft: normalizeAlertDraft(source.alertDraft),
      orderQuantity: roundNumber(normalizePositiveNumber(source.orderQuantity, def.orderQuantity), 6),
      activeTab: normalizeMainTab(source.activeTab),
      holdingsSort: HOLDINGS_SORTS.includes(source.holdingsSort) ? source.holdingsSort : def.holdingsSort,
      panelWidth: clampPanelWidth(source.panelWidth || def.panelWidth),
      panelHeight: clampPanelHeight(source.panelHeight || def.panelHeight),
      binarySide: source.binarySide === 'NO' ? 'NO' : 'YES'
    };
  }


  function normalizeMarket(raw, fallbackSlug) {
    if (!raw) return null;
    return {
      slug: raw.slug || fallbackSlug || '',
      question: raw.question || raw.title || '',
      title: raw.title || raw.question || '',
      outcomes: normalizeOutcomeList(raw.outcomes, raw.condition_id),
      token_ids: normalizeTokenIdList(raw.token_ids || raw.clobTokenIds || (raw.tokens || []).map((token) => token && token.token_id)),
      prices: raw.prices || null,
      closed: raw.closed || false,
      active: raw.active !== false,
      event_slug: raw.event_slug || '',
      condition_id: raw.condition_id || ''
    };
  }

  function mergeEventMarkets(markets, evt) {
    const list = (markets || []).filter(Boolean);
    if (list.length === 0) return null;
    return {
      slug: (evt && evt.slug) || list[0].slug,
      question: (evt && evt.title) || list[0].question,
      title: (evt && evt.title) || list[0].title,
      outcomes: list.map((market, index) => `${index + 1}. ${market.question || market.title || `Outcome ${index + 1}`}`),
      token_ids: list.map((market) => (market.token_ids || market.clobTokenIds || [])[0]).filter(Boolean),
      prices: null,
      closed: list.every((market) => market.closed),
      active: list.some((market) => market.active),
      event_slug: (evt && evt.slug) || '',
      condition_id: '',
      _subMarkets: list
    };
  }
  function mergeSearchMarkets(existing, incoming) {
    const merged = [];
    const seen = new Set();
    [...(existing || []), ...(incoming || [])].forEach((item) => {
      if (!item || !item.slug || seen.has(item.slug)) return;
      seen.add(item.slug);
      merged.push({
        slug: item.slug,
        title: item.title || item.question || item.slug,
        marketTitle: item.marketTitle || item.title || item.question || item.slug,
        eventTitle: item.eventTitle || '',
        question: item.question || item.title || item.slug
      });
    });
    return merged;
  }

  function getWatchlistSnapshots(priceCacheMap, watchlist, marketLookup) {
    return (watchlist || []).map((item) => {
      const market = marketLookup && marketLookup[item.marketSlug];
      const title = (market && (market.question || market.title)) || item.marketTitle || item.marketSlug;
      const tokenId = market ? getOutcomeTokenId(market, 0) : null;
      const priceRow = tokenId && priceCacheMap ? priceCacheMap[tokenId] : null;
      const snapshot = buildPriceSnapshot(priceRow || {});
      return {
        ...item,
        marketTitle: title,
        tokenId,
        buyPrice: snapshot.BUY,
        sellPrice: snapshot.SELL,
        midPrice: snapshot.MID
      };
    });
  }


  function getOutcomeCount(market) {
    if (!market) return 0;
    if (Array.isArray(market._subMarkets) && market._subMarkets.length > 0) return market._subMarkets.length;
    return Array.isArray(market.outcomes) ? market.outcomes.length : 0;
  }

  function getOutcomeName(market, index, side) {
    if (!market) return `Outcome ${index + 1}`;
    if (Array.isArray(market._subMarkets) && market._subMarkets[index]) {
      const name = market._subMarkets[index].question || market._subMarkets[index].title || `Outcome ${index + 1}`;
      return side === 'NO' ? `NO: ${name}` : name;
    }
    const name = (market.outcomes || [])[index] || `Outcome ${index + 1}`;
    return side === 'NO' ? `NO: ${name}` : name;
  }

  function getOutcomeTokenId(market, index, side) {
    if (!market) return null;
    if (Array.isArray(market._subMarkets) && market._subMarkets[index]) {
      const sub = market._subMarkets[index];
      const tokenIdx = side === 'NO' ? 1 : 0;
      return (sub.token_ids || sub.clobTokenIds || [])[tokenIdx] || null;
    }
    return (market.token_ids || market.clobTokenIds || [])[index] || null;
  }

  function getMidPrice(priceRow) {
    if (!priceRow) return null;
    const buy = Number(priceRow.buyPrice);
    const sell = Number(priceRow.sellPrice);
    if (Number.isFinite(buy) && buy > 0 && Number.isFinite(sell) && sell > 0) {
      return roundNumber((buy + sell) / 2, 6);
    }
    if (Number.isFinite(sell) && sell > 0) return roundNumber(sell, 6);
    if (Number.isFinite(buy) && buy > 0) return roundNumber(buy, 6);
    return null;
  }

  function buildPriceSnapshot(input) {
    if (!input) return { BUY: null, SELL: null, MID: null };
    const buy = Number.isFinite(Number(input.BUY)) ? roundNumber(Number(input.BUY), 6) : (Number.isFinite(Number(input.buyPrice)) ? roundNumber(Number(input.buyPrice), 6) : null);
    const sell = Number.isFinite(Number(input.SELL)) ? roundNumber(Number(input.SELL), 6) : (Number.isFinite(Number(input.sellPrice)) ? roundNumber(Number(input.sellPrice), 6) : null);
    const mid = Number.isFinite(Number(input.MID)) ? roundNumber(Number(input.MID), 6) : getMidPrice({ buyPrice: buy, sellPrice: sell });
    return { BUY: buy, SELL: sell, MID: mid };
  }

  function getQuoteSnapshot(priceRow, side) {
    if (side === 'NO') {
      return buildPriceSnapshot({
        BUY: priceRow && priceRow.noBuyPrice,
        SELL: priceRow && priceRow.noSellPrice,
        MID: priceRow && priceRow.noMidPrice
      });
    }
    return buildPriceSnapshot({
      BUY: priceRow && priceRow.buyPrice,
      SELL: priceRow && priceRow.sellPrice,
      MID: priceRow && priceRow.midPrice
    });
  }

  function renderQuoteStack(label, snapshot, tone, emptyText, actionHtml, attributeHtml) {
    const quote = snapshot || { BUY: null, SELL: null, MID: null };
    const hasPrice = quote.MID != null || quote.BUY != null || quote.SELL != null;
    if (!hasPrice && emptyText) {
      return `
        <div class="psim-quote-stack psim-quote-${tone} psim-quote-empty" ${attributeHtml || ''}>
          <span class="psim-quote-label">${escapeHtml(label)}</span>
          <span class="psim-quote-best"><span class="psim-quote-best-value">${escapeHtml(emptyText)}</span></span>
          <span class="psim-quote-meta">M — · 出 —</span>
          ${actionHtml || ''}
        </div>
      `;
    }
    return `
      <div class="psim-quote-stack psim-quote-${tone}" ${attributeHtml || ''}>
        <span class="psim-quote-label">${escapeHtml(label)}</span>
        <span class="psim-quote-best"><span class="psim-quote-best-tag">B</span><span class="psim-quote-best-value">${fmt$(quote.BUY || 0)}</span></span>
        <span class="psim-quote-meta">M ${fmt$(quote.MID || 0)} · 出 ${fmt$(quote.SELL || 0)}</span>
        ${actionHtml || ''}
      </div>
    `;
  }

  function applySlippage(price, side, settings) {
    const numericPrice = Number(price);
    if (!Number.isFinite(numericPrice) || numericPrice <= 0) return null;
    const slippage = Math.max(0, Number(settings && settings.slippageRate) || 0);
    const adjusted = side === 'BUY' ? numericPrice * (1 + slippage) : numericPrice * Math.max(0, 1 - slippage);
    return roundNumber(adjusted, 6);
  }

  function computeBuyExecution(quantity, rawPrice, settings) {
    const qty = roundNumber(normalizePositiveNumber(quantity, 0), 6);
    const execPrice = applySlippage(rawPrice, 'BUY', settings);
    if (!qty || !execPrice) return null;
    const gross = roundMoney(qty * execPrice);
    const fee = roundMoney(gross * ((settings && settings.feeRate) || 0));
    const total = roundMoney(gross + fee);
    return { qty, rawPrice: roundNumber(rawPrice, 6), execPrice, gross, fee, total, basisPerUnit: qty > 0 ? roundNumber(total / qty, 6) : 0 };
  }

  function computeSellExecution(quantity, rawPrice, settings) {
    const qty = roundNumber(normalizePositiveNumber(quantity, 0), 6);
    const execPrice = applySlippage(rawPrice, 'SELL', settings);
    if (!qty || !execPrice) return null;
    const gross = roundMoney(qty * execPrice);
    const fee = roundMoney(gross * ((settings && settings.feeRate) || 0));
    const net = roundMoney(gross - fee);
    return { qty, rawPrice: roundNumber(rawPrice, 6), execPrice, gross, fee, net };
  }

  function getObservedAlertPrice(alert, prices) {
    const snapshot = buildPriceSnapshot(prices);
    const priceType = PRICE_TYPES.includes(alert && alert.priceType) ? alert.priceType : 'BUY';
    return snapshot[priceType];
  }

  function shouldTriggerAlert(alert, prices) {
    if (!alert || alert.active === false) return false;
    const observed = getObservedAlertPrice(alert, prices);
    if (!Number.isFinite(observed)) return false;
    return alert.side === 'BELOW' ? observed <= alert.priceTarget : observed >= alert.priceTarget;
  }

  function createHistoryEntry(input) {
    return hydrateHistoryEntry({
      time: input.time || new Date().toISOString(),
      action: input.action,
      marketTitle: truncate(input.marketTitle || input.market?.question || input.market?.title || 'Unknown', 80),
      outcome: input.outcome || '',
      tokenId: input.tokenId || '',
      quantity: input.quantity,
      price: input.price,
      grossValue: input.grossValue,
      fee: input.fee,
      netValue: input.netValue,
      pnl: input.pnl,
      cashAfter: input.cashAfter,
      realizedAfter: input.realizedAfter,
      positionAfter: input.positionAfter
    });
  }

  function escapeCSVField(value) {
    if (value == null) return '';
    const text = String(value);
    return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
  }

  function parseCSVLine(line) {
    const text = String(line == null ? '' : line);
    const result = [];
    let current = '';
    let inQuotes = false;

    for (let index = 0; index < text.length; index += 1) {
      const char = text[index];
      if (char === '"') {
        if (inQuotes && text[index + 1] === '"') {
          current += '"';
          index += 1;
        } else {
          inQuotes = !inQuotes;
        }
      } else if (char === ',' && !inQuotes) {
        result.push(current);
        current = '';
      } else {
        current += char;
      }
    }

    result.push(current);
    return result;
  }

  function parseBackupSections(text) {
    const sections = {};
    let current = null;
    String(text || '').replace(/\r\n/g, '\n').split('\n').forEach((line) => {
      if (!line.trim() || line.startsWith('#')) return;
      const row = parseCSVLine(line);
      const firstCell = String(row[0] || '').trim();
      if (/^\[[^\]]+\]$/.test(firstCell)) {
        current = firstCell.slice(1, -1).toLowerCase();
        sections[current] = { headers: null, rows: [] };
        return;
      }
      if (!current) return;
      if (!sections[current].headers) {
        sections[current].headers = row;
      } else {
        sections[current].rows.push(row);
      }
    });
    return sections;
  }
  function rowToObject(headers, row) {
    const result = {};
    (headers || []).forEach((header, index) => {
      const key = String(header || '').trim();
      if (!key) return;
      result[key] = row[index];
    });
    return result;
  }


  function readStoredJson(key, fallbackValue) {
    if (typeof root.GM_getValue !== 'function') return fallbackValue;
    try {
      const raw = root.GM_getValue(key, null);
      if (raw == null) return fallbackValue;
      return typeof raw === 'string' ? JSON.parse(raw) : raw;
    } catch (_err) {
      return fallbackValue;
    }
  }

  function saveStoredJson(key, value) {
    if (typeof root.GM_setValue !== 'function') return;
    try {
      root.GM_setValue(key, JSON.stringify(value));
    } catch (_err) {
      // noop
    }
  }

  function downloadFile(content, filename, type) {
    const blob = new root.Blob([content], { type });
    const url = root.URL.createObjectURL(blob);
    const anchor = root.document.createElement('a');
    anchor.href = url;
    anchor.download = filename;
    root.document.body.appendChild(anchor);
    anchor.click();
    root.document.body.removeChild(anchor);
    root.URL.revokeObjectURL(url);
  }

  function notify(message, level) {
    const text = truncate(message, 140);
    try {
      if (typeof root.GM_notification === 'function') {
        root.GM_notification({ text, title: level ? `Paper · ${String(level).toUpperCase()}` : 'Paper', timeout: 3500 });
      } else if (root.console && typeof root.console.log === 'function') {
        root.console.log('[Paper]', text);
      }
    } catch (_err) {
      if (root.console && typeof root.console.log === 'function') root.console.log('[Paper]', text);
    }
  }

  function createEngine(options) {
    const settings = options || {};
    let state = hydrateState(settings.state);
    let ui = hydrateUI(settings.ui);
    const hooks = {
      onStateChange: typeof settings.onStateChange === 'function' ? settings.onStateChange : noop,
      onUIChange: typeof settings.onUIChange === 'function' ? settings.onUIChange : noop
    };

    function emitState() {
      hooks.onStateChange(deepClone(state));
    }

    function emitUI() {
      hooks.onUIChange(deepClone(ui));
    }

    function getState() {
      return { ...deepClone(state), ui: deepClone(ui) };
    }

    function getUI() {
      return deepClone(ui);
    }

    function replaceState(nextState) {
      state = hydrateState(nextState);
      emitState();
      return getState();
    }

    function replaceUI(nextUI) {
      ui = hydrateUI(nextUI);
      emitUI();
      return getUI();
    }

    function patchUI(partial) {
      ui = hydrateUI({ ...ui, ...(partial || {}) });
      emitUI();
      return getUI();
    }
    function prefillAlertDraft(draft) {
      ui = hydrateUI({ ...ui, alertDraft: normalizeAlertDraft(draft) });
      emitUI();
      return getUI();
    }

    function clearAlertDraft() {
      ui = hydrateUI({ ...ui, alertDraft: null });
      emitUI();
      return getUI();
    }

    function submitAlertDraft() {
      const draft = normalizeAlertDraft(ui.alertDraft);
      if (!draft || !draft.tokenId) return { ok: false, error: '警报草稿不完整' };
      const priceTarget = Number(draft.priceTarget);
      if (!Number.isFinite(priceTarget) || priceTarget <= 0 || priceTarget > 1) return { ok: false, error: '无效的目标价格' };
      const alert = addAlert(draft);
      clearAlertDraft();
      return { ok: true, alert };
    }


    function pushHistory(entry) {
      state.history.unshift(entry);
      if (state.history.length > MAX_HISTORY) state.history = state.history.slice(0, MAX_HISTORY);
    }

    function getPortfolioSummary() {
      let totalCost = 0;
      let marketValue = 0;
      state.positions.forEach((position) => {
        const cost = roundMoney(position.avgPrice * position.quantity);
        const price = Number.isFinite(Number(position.lastPrice)) && Number(position.lastPrice) > 0 ? Number(position.lastPrice) : position.avgPrice;
        totalCost += cost;
        marketValue += roundMoney(price * position.quantity);
      });
      totalCost = roundMoney(totalCost);
      marketValue = roundMoney(marketValue);
      return {
        cash: roundMoney(state.cash),
        totalCost,
        marketValue,
        realizedPnl: roundMoney(state.realizedPnl),
        unrealizedPnl: roundMoney(marketValue - totalCost),
        equity: roundMoney(state.cash + marketValue)
      };
    }

    function getHoldingsView(sortKey) {
      const summary = getPortfolioSummary();
      const rows = state.positions.map((position) => {
        const currentPrice = Number.isFinite(Number(position.lastPrice)) && Number(position.lastPrice) > 0 ? Number(position.lastPrice) : position.avgPrice;
        const totalCost = roundMoney(position.avgPrice * position.quantity);
        const marketValue = roundMoney(currentPrice * position.quantity);
        const pnl = roundMoney(marketValue - totalCost);
        const exposurePct = summary.equity > 0 ? roundNumber((marketValue / summary.equity) * 100, 4) : 0;
        const costRecoveryPct = totalCost > 0 ? roundNumber((marketValue / totalCost) * 100, 4) : 0;
        const breakEvenPct = position.avgPrice > 0 ? roundNumber(((currentPrice - position.avgPrice) / position.avgPrice) * 100, 4) : 0;
        return {
          ...deepClone(position),
          currentPrice: roundNumber(currentPrice, 6),
          totalCost,
          marketValue,
          pnl,
          pnlPct: totalCost > 0 ? roundNumber((pnl / totalCost) * 100, 4) : 0,
          exposurePct,
          costRecoveryPct,
          breakEvenPct,
          remainingCost: totalCost
        };
      });

      const sort = HOLDINGS_SORTS.includes(sortKey) ? sortKey : ui.holdingsSort;
      rows.sort((left, right) => {
        if (sort === 'value_desc') return right.marketValue - left.marketValue;
        if (sort === 'qty_desc') return right.quantity - left.quantity;
        if (sort === 'newest') return new Date(right.openedAt) - new Date(left.openedAt);
        return right.pnl - left.pnl;
      });
      return rows;
    }

    function buy(market, outcomeIdx, quantity, prices, side) {
      const buySide = side === 'NO' ? 'NO' : 'YES';
      const qty = roundNumber(normalizePositiveNumber(quantity, 0), 6);
      const tokenId = getOutcomeTokenId(market, outcomeIdx, buySide);
      if (!qty) return { ok: false, error: '无效数量' };
      if (!tokenId) return { ok: false, error: '无法交易: 无 token ID' };

      const priceRow = Array.isArray(prices) ? prices[outcomeIdx] : null;
      const rawPrice = buySide === 'NO'
        ? (priceRow && Number.isFinite(Number(priceRow.noBuyPrice)) ? Number(priceRow.noBuyPrice) : null)
        : (priceRow && Number.isFinite(Number(priceRow.buyPrice)) ? Number(priceRow.buyPrice) : null);
      const execution = computeBuyExecution(qty, rawPrice, state.settings);
      if (!execution) return { ok: false, error: '价格不可用' };
      if (execution.total > state.cash + 1e-9) {
        return { ok: false, error: `资金不足: 需要 ${fmt$(execution.total)}, 当前 ${fmt$(state.cash)}` };
      }

      const marketTitle = truncate(market.question || market.title || 'Unknown', 80);
      const outcome = getOutcomeName(market, outcomeIdx, buySide);
      let position = state.positions.find((item) => item.marketSlug === (market.slug || '') && item.tokenId === tokenId);
      if (!position) {
        position = hydratePosition({
          id: generateId('pos'),
          marketSlug: market.slug || '',
          marketTitle,
          tokenId,
          quantity: 0,
          avgPrice: 0,
          outcome,
          openedAt: new Date().toISOString(),
          lastPrice: execution.rawPrice,
          lastPriceType: 'BUY'
        });
        state.positions.push(position);
      }

      const existingCost = roundMoney(position.avgPrice * position.quantity);
      const nextQuantity = roundNumber(position.quantity + execution.qty, 6);
      position.quantity = nextQuantity;
      position.avgPrice = nextQuantity > 0 ? roundNumber((existingCost + execution.total) / nextQuantity, 6) : 0;
      position.lastPrice = execution.rawPrice;
      position.lastPriceType = 'BUY';

      state.cash = roundMoney(state.cash - execution.total);
      pushHistory(createHistoryEntry({
        action: 'BUY',
        marketTitle,
        outcome,
        tokenId,
        quantity: execution.qty,
        price: execution.execPrice,
        grossValue: execution.gross,
        fee: execution.fee,
        netValue: execution.total,
        pnl: 0,
        cashAfter: state.cash,
        realizedAfter: state.realizedPnl,
        positionAfter: position.quantity
      }));
      emitState();
      return { ok: true, execution, position: deepClone(position), side: buySide };
    }

    function sellByPositionId(positionId, quantity, rawSellPrice, action, marketTitleOverride) {
      const index = state.positions.findIndex((item) => item.id === positionId);
      if (index === -1) return { ok: false, error: '无持仓可卖出' };

      const position = state.positions[index];
      const qty = Math.min(roundNumber(normalizePositiveNumber(quantity, 0), 6), position.quantity);
      if (!qty) return { ok: false, error: '无效数量' };

      const execution = computeSellExecution(qty, rawSellPrice, state.settings);
      if (!execution) return { ok: false, error: '卖出价格不可用' };

      const costBasis = roundMoney(position.avgPrice * qty);
      const pnl = roundMoney(execution.net - costBasis);

      position.quantity = roundNumber(position.quantity - qty, 6);
      position.lastPrice = execution.rawPrice;
      position.lastPriceType = 'SELL';
      if (position.quantity <= 0.000001) {
        state.positions.splice(index, 1);
      }

      state.cash = roundMoney(state.cash + execution.net);
      state.realizedPnl = roundMoney(state.realizedPnl + pnl);
      pushHistory(createHistoryEntry({
        action: action || 'SELL',
        marketTitle: marketTitleOverride || position.marketTitle,
        outcome: position.outcome,
        tokenId: position.tokenId,
        quantity: execution.qty,
        price: execution.execPrice,
        grossValue: execution.gross,
        fee: execution.fee,
        netValue: execution.net,
        pnl,
        cashAfter: state.cash,
        realizedAfter: state.realizedPnl,
        positionAfter: position.quantity
      }));
      emitState();
      return { ok: true, execution, pnl };
    }

    function sell(market, outcomeIdx, quantity, prices) {
      const tokenId = getOutcomeTokenId(market, outcomeIdx);
      if (!tokenId) return { ok: false, error: '无法交易: 无 token ID' };
      const priceRow = Array.isArray(prices) ? prices[outcomeIdx] : null;
      const rawPrice = priceRow && Number.isFinite(Number(priceRow.sellPrice)) ? Number(priceRow.sellPrice) : null;
      if (!rawPrice || rawPrice <= 0) return { ok: false, error: '卖出价格不可用' };
      const position = state.positions.find((item) => item.marketSlug === (market.slug || '') && item.tokenId === tokenId);
      if (!position) return { ok: false, error: '无持仓可卖出' };
      return sellByPositionId(position.id, quantity, rawPrice, 'SELL', truncate(market.question || market.title || position.marketTitle, 80));
    }

    function closePosition(positionId, rawSellPrice) {
      const position = state.positions.find((item) => item.id === positionId);
      if (!position) return { ok: false, error: '未找到持仓' };
      const rawPrice = Number.isFinite(Number(rawSellPrice)) && Number(rawSellPrice) > 0 ? Number(rawSellPrice) : position.lastPrice;
      return sellByPositionId(position.id, position.quantity, rawPrice, 'CLOSE', position.marketTitle);
    }

    function closeAllPositions(priceMap) {
      const prices = priceMap || {};
      let closedCount = 0;
      let totalPnl = 0;
      const targets = [...state.positions];
      targets.forEach((position) => {
        const raw = prices[position.tokenId];
        const rawPrice = Number.isFinite(Number(raw)) && Number(raw) > 0 ? Number(raw) : position.lastPrice;
        if (!rawPrice || rawPrice <= 0) return;
        const result = sellByPositionId(position.id, position.quantity, rawPrice, 'CLOSE_ALL', position.marketTitle);
        if (result.ok) {
          closedCount += 1;
          totalPnl = roundMoney(totalPnl + result.pnl);
        }
      });
      return { ok: closedCount > 0, closedCount, totalPnl, error: closedCount > 0 ? null : '无实时价格可平仓' };
    }

    function updatePositionPrice(tokenId, rawPrice, priceType) {
      const position = state.positions.find((item) => item.tokenId === tokenId);
      if (!position) return false;
      if (!Number.isFinite(Number(rawPrice)) || Number(rawPrice) <= 0) return false;
      position.lastPrice = roundNumber(Number(rawPrice), 6);
      position.lastPriceType = PRICE_TYPES.includes(priceType) ? priceType : 'SELL';
      emitState();
      return true;
    }

    function addAlert(alert) {
      const hydrated = hydrateAlert(alert);
      if (!hydrated) return null;
      state.alerts.push(hydrated);
      emitState();
      return deepClone(hydrated);
    }

    function removeAlert(alertId) {
      const before = state.alerts.length;
      state.alerts = state.alerts.filter((alert) => alert.id !== alertId);
      if (state.alerts.length !== before) emitState();
      return before !== state.alerts.length;
    }

    function evaluateAlerts(priceResolver) {
      const triggered = [];
      state.alerts.forEach((alert) => {
        if (!alert.active) return;
        const prices = typeof priceResolver === 'function' ? priceResolver(alert) : null;
        if (!shouldTriggerAlert(alert, prices)) return;
        const observed = getObservedAlertPrice(alert, prices);
        alert.active = false;
        triggered.push({ alert: deepClone(alert), observedPrice: observed });
      });
      if (triggered.length > 0) emitState();
      return triggered;
    }

    function addWatchlistItem(item) {
      const hydrated = hydrateWatchlistItem(item);
      if (!hydrated) return null;
      if (state.watchlist.some((entry) => entry.marketSlug === hydrated.marketSlug)) return deepClone(state.watchlist.find((entry) => entry.marketSlug === hydrated.marketSlug));
      state.watchlist.unshift(hydrated);
      emitState();
      return deepClone(hydrated);
    }

    function removeWatchlistItem(marketSlug) {
      const before = state.watchlist.length;
      state.watchlist = state.watchlist.filter((item) => item.marketSlug !== marketSlug);
      if (state.watchlist.length !== before) emitState();
      return before !== state.watchlist.length;
    }

    function clearHistory() {
      state.history = [];
      emitState();
    }

    function clearAlerts() {
      state.alerts = [];
      emitState();
    }

    function clearWatchlist() {
      state.watchlist = [];
      emitState();
    }

    function resetAll() {
      state = hydrateState(defaultState());
      emitState();
      return getState();
    }

    function exportBackupJSON() {
      return JSON.stringify({ version: 3, exportedAt: new Date().toISOString(), state: getState(), ui: getUI() }, null, 2);
    }

    function importBackupJSON(text) {
      const parsed = JSON.parse(text);
      if (!parsed || typeof parsed !== 'object') throw new Error('Invalid JSON backup');
      state = hydrateState(parsed.state || parsed);
      if (parsed.ui) ui = hydrateUI(parsed.ui);
      emitState();
      emitUI();
      return getState();
    }

    function exportBackupCSV() {
      const rows = [];
      rows.push(['# Polymarket Simulator Backup']);
      rows.push(['# Exported', new Date().toISOString()]);
      rows.push([]);
      rows.push(['[SETTINGS]']);
      rows.push(['key', 'value']);
      rows.push(['initialBalance', state.initialBalance]);
      rows.push(['cash', state.cash]);
      rows.push(['realizedPnl', state.realizedPnl]);
      rows.push(['refreshSeconds', state.settings.refreshSeconds]);
      rows.push(['defaultQuantity', state.settings.defaultQuantity]);
      rows.push(['feeRate', state.settings.feeRate]);
      rows.push(['slippageRate', state.settings.slippageRate]);
      rows.push(['soundEnabled', state.settings.soundEnabled]);
      rows.push(['darkMode', state.settings.darkMode]);
      rows.push([]);
      rows.push(['[ALERTS]']);
      rows.push(['id', 'marketSlug', 'tokenId', 'priceTarget', 'side', 'active', 'outcomeName', 'priceType']);
      state.alerts.forEach((alert) => {
        rows.push([alert.id, alert.marketSlug, alert.tokenId, alert.priceTarget, alert.side, alert.active, alert.outcomeName, alert.priceType]);
      });
      rows.push([]);
      rows.push(['[WATCHLIST]']);
      rows.push(['id', 'marketSlug', 'marketTitle', 'addedAt']);
      state.watchlist.forEach((item) => {
        rows.push([item.id, item.marketSlug, item.marketTitle, item.addedAt]);
      });
      rows.push([]);
      rows.push(['[POSITIONS]']);
      rows.push(['id', 'marketSlug', 'marketTitle', 'tokenId', 'quantity', 'avgPrice', 'outcome', 'openedAt', 'lastPrice', 'lastPriceType']);
      state.positions.forEach((position) => {
        rows.push([position.id, position.marketSlug, position.marketTitle, position.tokenId, position.quantity, position.avgPrice, position.outcome, position.openedAt, position.lastPrice, position.lastPriceType]);
      });
      rows.push([]);
      rows.push(['[HISTORY]']);
      rows.push(['time', 'action', 'marketTitle', 'outcome', 'tokenId', 'quantity', 'price', 'grossValue', 'fee', 'netValue', 'pnl']);
      state.history.forEach((entry) => {
        rows.push([entry.time, entry.action, entry.marketTitle, entry.outcome, entry.tokenId, entry.quantity, entry.price, entry.grossValue, entry.fee, entry.netValue, entry.pnl]);
      });
      return rows.map((row) => row.map(escapeCSVField).join(',')).join('\n');
    }

    function importBackupCSV(text) {
      const sections = parseBackupSections(text);
      const nextState = hydrateState(defaultState());

      (sections.settings && sections.settings.rows || []).forEach((row) => {
        const key = row[0];
        const value = row[1];
        if (key === 'initialBalance') nextState.initialBalance = roundMoney(normalizePositiveNumber(value, nextState.initialBalance));
        else if (key === 'cash') nextState.cash = roundMoney(Number(value) || 0);
        else if (key === 'realizedPnl') nextState.realizedPnl = roundMoney(Number(value) || 0);
        else if (key === 'refreshSeconds') nextState.settings.refreshSeconds = Math.min(60, Math.max(2, parseInt(value, 10) || nextState.settings.refreshSeconds));
        else if (key === 'defaultQuantity') nextState.settings.defaultQuantity = Math.max(1, parseFloat(value) || nextState.settings.defaultQuantity);
        else if (key === 'feeRate') nextState.settings.feeRate = Math.max(0, parseFloat(value) || 0);
        else if (key === 'slippageRate') nextState.settings.slippageRate = Math.max(0, parseFloat(value) || 0);
        else if (key === 'soundEnabled') nextState.settings.soundEnabled = value === 'true';
        else if (key === 'darkMode') nextState.settings.darkMode = value === 'true';
      });

      const alertHeaders = sections.alerts && sections.alerts.headers || [];
      nextState.alerts = (sections.alerts && sections.alerts.rows || []).map((row) => {
        const data = rowToObject(alertHeaders, row);
        return hydrateAlert({
          id: data.id,
          marketSlug: data.marketSlug,
          tokenId: data.tokenId,
          priceTarget: data.priceTarget,
          side: data.side,
          active: data.active !== 'false',
          outcomeName: data.outcomeName,
          priceType: data.priceType
        });
      }).filter(Boolean);

      const watchlistHeaders = sections.watchlist && sections.watchlist.headers || [];
      nextState.watchlist = (sections.watchlist && sections.watchlist.rows || []).map((row) => {
        const data = rowToObject(watchlistHeaders, row);
        return hydrateWatchlistItem({
          id: data.id,
          marketSlug: data.marketSlug,
          marketTitle: data.marketTitle,
          addedAt: data.addedAt
        });
      }).filter(Boolean);

      const positionHeaders = sections.positions && sections.positions.headers || [];
      nextState.positions = (sections.positions && sections.positions.rows || []).map((row) => {
        const data = rowToObject(positionHeaders, row);
        return hydratePosition({
          id: data.id,
          marketSlug: data.marketSlug,
          marketTitle: data.marketTitle,
          tokenId: data.tokenId,
          quantity: data.quantity,
          avgPrice: data.avgPrice,
          outcome: data.outcome,
          openedAt: data.openedAt,
          lastPrice: data.lastPrice,
          lastPriceType: data.lastPriceType
        });
      }).filter(Boolean);

      const historyHeaders = sections.history && sections.history.headers || [];
      nextState.history = (sections.history && sections.history.rows || []).map((row) => {
        const data = rowToObject(historyHeaders, row);
        return hydrateHistoryEntry({
          time: data.time,
          action: data.action,
          marketTitle: data.marketTitle,
          outcome: data.outcome,
          tokenId: data.tokenId,
          quantity: data.quantity,
          price: data.price,
          grossValue: data.grossValue,
          fee: data.fee,
          netValue: data.netValue,
          pnl: data.pnl
        });
      }).filter(Boolean).slice(0, MAX_HISTORY);

      state = hydrateState(nextState);
      emitState();
      return getState();
    }

    return {
      getState,
      getUI,
      replaceState,
      replaceUI,
      patchUI,
      buy,
      sell,
      sellByPositionId,
      closePosition,
      closeAllPositions,
      updatePositionPrice,
      getPortfolioSummary,
      getHoldingsView,
      prefillAlertDraft,
      clearAlertDraft,
      submitAlertDraft,
      addAlert,
      removeAlert,
      evaluateAlerts,
      addWatchlistItem,
      removeWatchlistItem,
      clearHistory,
      clearAlerts,
      clearWatchlist,
      resetAll,
      exportBackupCSV,
      importBackupCSV,
      exportBackupJSON,
      importBackupJSON
    };
  }

  const exportedApi = {
    createEngine,
    defaultState,
    defaultUI,
    hydrateState,
    hydrateUI,
    normalizeMarket,
    mergeEventMarkets,
    parseCSVLine,
    shouldTriggerAlert,
    buildPriceSnapshot,
    getObservedAlertPrice,
    mergeSearchMarkets,
    normalizeAlertDraft,
    getWatchlistSnapshots,
  };

  if (typeof module !== 'undefined' && module.exports) {
    module.exports = exportedApi;
    return;
  }

  let engine = createEngine({
    state: hydrateState(readStoredJson(STORAGE_KEY, defaultState())),
    ui: hydrateUI(readStoredJson(UI_KEY, defaultUI())),
    onStateChange(nextState) {
      saveStoredJson(STORAGE_KEY, nextState);
    },
    onUIChange(nextUI) {
      saveStoredJson(UI_KEY, nextUI);
    }
  });

  let panelEl = null;
  let contentEl = null;
  let refreshTimer = null;
  let marketCache = {};
  let marketInflight = {};
  let marketPriceCache = {};
  let marketPriceInflight = {};
  let priceCache = {};
  let marketUiUpdateTimer = null;
  let marketUiLastUpdateAt = 0;
  let portfolioUiUpdateTimer = null;
  let snapGuideEl = null;
  let currentPageSlug = null;
  let marketWorkspaceCacheEl = null;
  let marketWorkspaceCacheKey = null;

  root.console.info('[PolySim] Script loaded. Version 3.1.0');

  function getState() {
    return engine.getState();
  }

  function getUI() {
    return engine.getUI();
  }

  function getMarketWorkspaceCacheKey() {
    const ui = getUI();
    const slug = getPageSlug() || '';
    const draft = ui.alertDraft;
    return `${slug}|${draft && draft.tokenId ? draft.tokenId : ''}`;
  }

  function stashMarketWorkspace() {
    const marketWorkspace = root.document.getElementById('psim-market-workspace');
    if (!marketWorkspace) return;
    marketWorkspaceCacheEl = marketWorkspace;
    marketWorkspaceCacheKey = getMarketWorkspaceCacheKey();
    marketWorkspace.remove();
  }

  let apiErrorCount = 0;
  let lastApiError = null;

  function gmFetch(url, callback, retries) {
    if (typeof root.GM_xmlhttpRequest !== 'function') {
      callback(new Error('GM_xmlhttpRequest unavailable'), null);
      return;
    }
    const attempt = retries == null ? 0 : retries;
    root.GM_xmlhttpRequest({
      method: 'GET',
      url,
      timeout: 15000,
      onload(resp) {
        if (resp.status >= 200 && resp.status < 300) {
          try {
            callback(null, JSON.parse(resp.responseText));
          } catch (err) {
            apiErrorCount += 1;
            lastApiError = new Error('Invalid JSON response from API');
            callback(new Error('Invalid JSON response'), null);
          }
        } else if (resp.status === 403 || resp.status === 429) {
          apiErrorCount += 1;
          lastApiError = new Error(`API forbidden (${resp.status}): ${url}`);
          if (attempt < 2) {
            root.setTimeout(() => gmFetch(url, callback, attempt + 1), 2000 * (attempt + 1));
          } else {
            callback(lastApiError, null);
          }
        } else {
          apiErrorCount += 1;
          lastApiError = new Error(`HTTP ${resp.status}`);
          callback(new Error(`HTTP ${resp.status}`), null);
        }
      },
      onerror() {
        apiErrorCount += 1;
        lastApiError = new Error('Network error');
        if (attempt < 2) {
          root.setTimeout(() => gmFetch(url, callback, attempt + 1), 1500 * (attempt + 1));
        } else {
          callback(new Error('Network error'), null);
        }
      },
      ontimeout() {
        apiErrorCount += 1;
        lastApiError = new Error('Timeout');
        if (attempt < 2) {
          root.setTimeout(() => gmFetch(url, callback, attempt + 1), 1500);
        } else {
          callback(new Error('Timeout'), null);
        }
      }
    });
  }

  function getPageSlug() {
    const path = root.window.location.pathname || '';
    const patterns = [
      /\/event\/([^/?#]+)/,
      /\/markets\/([^/?#]+)/,
      /\/e\/([^/?#]+)/,
      /\/m\/([^/?#]+)/,
      /\/c\/([^/?#]+)/,
      /\/question\/([^/?#]+)/,
      /\/group\/([^/?#]+)/
    ];
    for (const pattern of patterns) {
      const match = path.match(pattern);
      if (match) return match[1];
    }
    const scripts = root.document.querySelectorAll('script[type="application/json"]');
    for (const script of scripts) {
      try {
        const parsed = JSON.parse(script.textContent);
        const slug = parsed?.props?.pageProps?.market?.slug
          || parsed?.props?.pageProps?.event?.slug
          || parsed?.query?.slug;
        if (slug) return slug;
      } catch (_) {}
    }
    return null;
  }

  function resolveInflightMarket(slug, err, market) {
    const callbacks = marketInflight[slug] || [];
    delete marketInflight[slug];
    callbacks.forEach((cb) => {
      cb(err, market);
    });
  }

  function getMarketPriceCacheKey(market) {
    return market && (market.slug || market.event_slug || market.title || market.question || '');
  }

  function resolveInflightMarketPrices(key, err, prices) {
    const callbacks = marketPriceInflight[key] || [];
    delete marketPriceInflight[key];
    callbacks.forEach((cb) => {
      cb(err, prices ? deepClone(prices) : prices);
    });
  }

  function getMarketBySlug(slug, callback) {
    if (!slug) { callback(new Error('Missing slug'), null); return; }
    if (marketCache[slug] && Date.now() - marketCache[slug].ts < MARKET_CACHE_MS) {
      callback(null, marketCache[slug].data);
      return;
    }
    if (marketInflight[slug]) {
      marketInflight[slug].push(callback);
      return;
    }
    marketInflight[slug] = [callback];

    let settled = false;
    let pending = 3;
    let lastError = new Error('Market not found');
    const finish = (err, market) => {
      if (settled) return;
      if (market) {
        settled = true;
        marketCache[slug] = { data: market, ts: Date.now() };
        resolveInflightMarket(slug, null, market);
        return;
      }
      pending -= 1;
      if (err) lastError = err;
      if (pending === 0) {
        settled = true;
        resolveInflightMarket(slug, lastError, null);
      }
    };

    gmFetch(`${GAMMA_BASE}/markets/slug/${encodeURIComponent(slug)}`, (err, data) => {
      if (!err && data) {
        const market = normalizeMarket(Array.isArray(data) ? data[0] : data, slug);
        if (market) { finish(null, market); return; }
      }
      finish(err, null);
    });

    gmFetch(`${GAMMA_BASE}/markets?slug=${encodeURIComponent(slug)}&limit=1`, (err, data) => {
      if (!err && Array.isArray(data) && data.length > 0) {
        const market = normalizeMarket(data[0], slug);
        if (market) { finish(null, market); return; }
      }
      finish(err, null);
    });

    gmFetch(`${GAMMA_BASE}/events/slug/${encodeURIComponent(slug)}`, (err, evt) => {
      if (!err && evt) {
        const eventData = Array.isArray(evt) ? evt[0] : evt;
        const markets = (eventData.markets || []).map((market) => normalizeMarket(market, market.slug || slug));
        const merged = markets.length === 1 ? markets[0] : mergeEventMarkets(markets, eventData);
        if (merged) { finish(null, merged); return; }
      }
      finish(err, null);
    });
  }

  function getClobPrice(tokenId, side, callback) {
    const cacheKey = `${tokenId}:${side}`;
    if (priceCache[cacheKey] && Date.now() - priceCache[cacheKey].ts < 15000) {
      callback(null, priceCache[cacheKey].price);
      return;
    }
    gmFetch(`${CLOB_BASE}/price?token_id=${tokenId}&side=${side}`, (err, data) => {
      if (err) { callback(err, null); return; }
      const price = data ? parseFloat(data.price || 0) : null;
      if (Number.isFinite(price) && price > 0) priceCache[cacheKey] = { price, ts: Date.now() };
      callback(null, Number.isFinite(price) && price > 0 ? price : null);
    });
  }

  function getClobPricePair(tokenId, callback) {
    let pending = 2;
    let buyPrice = null;
    let sellPrice = null;
    let lastError = null;
    const done = () => {
      pending -= 1;
      if (pending === 0) callback(lastError, { buyPrice, sellPrice, midPrice: getMidPrice({ buyPrice, sellPrice }) });
    };
    getClobPrice(tokenId, 'BUY', (err, price) => {
      if (err) lastError = err;
      buyPrice = price;
      done();
    });
    getClobPrice(tokenId, 'SELL', (err, price) => {
      if (err) lastError = err;
      sellPrice = price;
      done();
    });
  }

  function fetchAllClobPrices(market, callback) {
    const count = getOutcomeCount(market);
    if (!count) { callback(null, []); return; }
    const hasSubMarkets = Array.isArray(market._subMarkets) && market._subMarkets.length > 0;
    const results = new Array(count);
    let pending = count;

    for (let index = 0; index < count; index += 1) {
      const tokenId = getOutcomeTokenId(market, index);
      const noTokenId = hasSubMarkets ? getOutcomeTokenId(market, index, 'NO') : null;
      if (!tokenId) {
        results[index] = { tokenId: null, buyPrice: null, sellPrice: null, midPrice: null, noTokenId: null, noBuyPrice: null, noSellPrice: null, noMidPrice: null };
        pending -= 1;
        if (pending === 0) callback(null, results);
        continue;
      }
      getClobPricePair(tokenId, (_yesErr, yesPair) => {
        const yesResult = { tokenId, buyPrice: yesPair.buyPrice, sellPrice: yesPair.sellPrice, midPrice: yesPair.midPrice };
        if (Number.isFinite(yesPair.sellPrice) && yesPair.sellPrice > 0) engine.updatePositionPrice(tokenId, yesPair.sellPrice, 'SELL');
        if (!noTokenId) {
          results[index] = { ...yesResult, noTokenId: null, noBuyPrice: null, noSellPrice: null, noMidPrice: null };
          pending -= 1;
          if (pending === 0) callback(null, results);
          return;
        }
        getClobPricePair(noTokenId, (_noErr, noPair) => {
          results[index] = { ...yesResult, noTokenId, noBuyPrice: noPair.buyPrice, noSellPrice: noPair.sellPrice, noMidPrice: noPair.midPrice };
          if (Number.isFinite(noPair.sellPrice) && noPair.sellPrice > 0) engine.updatePositionPrice(noTokenId, noPair.sellPrice, 'SELL');
          pending -= 1;
          if (pending === 0) callback(null, results);
        });
      });
    }
  }

  function fetchMarketPriceSnapshot(market, callback, forceFresh) {
    const key = getMarketPriceCacheKey(market);
    if (!market || !key) {
      fetchAllClobPrices(market, callback);
      return;
    }
    const cached = marketPriceCache[key];
    if (!forceFresh && cached && Date.now() - cached.ts < MARKET_PRICE_CACHE_MS) {
      callback(null, deepClone(cached.data));
      return;
    }
    if (marketPriceInflight[key]) {
      marketPriceInflight[key].push(callback);
      return;
    }
    marketPriceInflight[key] = [callback];
    fetchAllClobPrices(market, (err, prices) => {
      if (!err && Array.isArray(prices)) {
        marketPriceCache[key] = { data: deepClone(prices), ts: Date.now() };
      }
      resolveInflightMarketPrices(key, err, prices);
    });
  }

  function triggerNotifications(items) {
    items.forEach((item) => {
      notify(`Alert: "${item.alert.outcomeName}" ${item.alert.side === 'ABOVE' ? 'above' : 'below'} ${fmt$(item.alert.priceTarget)} (${item.alert.priceType} now ${fmt$(item.observedPrice)})`, 'info');
    });
  }

  function el(tag, attrs) {
    const node = root.document.createElement(tag);
    if (attrs) {
      Object.entries(attrs).forEach(([key, value]) => {
        node.setAttribute(key, value);
      });
    }
    return node;
  }

  function createPanel() {
    panelEl = root.document.createElement('div');
    panelEl.id = 'polymarket-sim-panel';
    panelEl.innerHTML = `
      <div id="psim-header">
        <span id="psim-title">Paper</span>
        <div id="psim-header-btns">
          <button id="psim-collapse-btn" title="折叠">─</button>
          <button id="psim-close-btn" title="关闭">✕</button>
        </div>
      </div>
      <div id="psim-resize-top-handle" title="拖动调整高度"></div>
      <div id="psim-content"></div>
      <div id="psim-resize-handle" title="拖动右边缘调宽,双击恢复默认宽度"></div>
      <div id="psim-resize-bottom-handle" title="拖动调整高度"></div>
    `;
    contentEl = panelEl.querySelector('#psim-content');
    root.document.body.appendChild(panelEl);

    const ui = getUI();
    if (ui.panelX > 0 || ui.panelY > 0) {
      panelEl.style.right = 'auto';
      panelEl.style.left = `${ui.panelX}px`;
      panelEl.style.top = `${ui.panelY}px`;
    }
    panelEl.style.width = `${ui.panelWidth}px`;
    panelEl.style.height = `${ui.panelHeight}px`;
    makeDraggable(panelEl, panelEl.querySelector('#psim-header'));
    makeResizable(
      panelEl,
      panelEl.querySelector('#psim-resize-handle'),
      panelEl.querySelector('#psim-resize-top-handle'),
      panelEl.querySelector('#psim-resize-bottom-handle')
    );
    panelEl.querySelector('#psim-header').ondblclick = (event) => {
      if (event.target && event.target.closest && event.target.closest('button')) return;
      const nextHeight = getDefaultPanelHeight();
      panelEl.style.width = `${PANEL_WIDTH.DEFAULT}px`;
      panelEl.style.height = `${nextHeight}px`;
      engine.patchUI({ panelWidth: PANEL_WIDTH.DEFAULT, panelHeight: nextHeight });
    };

    panelEl.querySelector('#psim-collapse-btn').onclick = () => {
      const nextUI = engine.patchUI({ collapsed: !getUI().collapsed });
      panelEl.classList.toggle('psim-collapsed', nextUI.collapsed);
      render();
    };
    panelEl.querySelector('#psim-close-btn').onclick = () => {
      panelEl.remove();
      if (refreshTimer) clearInterval(refreshTimer);
    };
    panelEl.classList.toggle('psim-collapsed', ui.collapsed);
  }

  function updatePrices() {
    const summary = engine.getPortfolioSummary();
    const setText = (id, text) => { const el = root.document.getElementById(id); if (el) el.textContent = text; };
    setText('psim-val-cash', fmt$(summary.cash));
    setText('psim-val-equity', fmt$(summary.equity));
    const pnlEl = root.document.getElementById('psim-val-realized-pnl');
    if (pnlEl) { pnlEl.textContent = fmt$(summary.realizedPnl); pnlEl.className = `psim-value ${summary.realizedPnl >= 0 ? 'psim-positive' : 'psim-negative'}`; }
    const rows = engine.getHoldingsView();
    getState().positions.forEach((pos) => {
      const row = rows.find((r) => r.id === pos.id);
      if (!row) return;
      const priceEl = root.document.getElementById(`psim-pos-price-${pos.id}`);
      const pnlEl2 = root.document.getElementById(`psim-pos-pnl-${pos.id}`);
      const mainDetailEl = root.document.getElementById(`psim-pos-details-main-${pos.id}`);
      const subDetailEl = root.document.getElementById(`psim-pos-details-sub-${pos.id}`);
      if (priceEl) priceEl.textContent = fmt$(row.currentPrice);
      if (pnlEl2) {
        pnlEl2.textContent = `${fmt$(row.pnl)} (${fmtPct(row.pnlPct)})`;
        pnlEl2.className = `psim-pos-pnl ${row.pnl >= 0 ? 'psim-positive' : 'psim-negative'}`;
      }
      if (mainDetailEl) mainDetailEl.textContent = `持仓 ${row.quantity}x · 暴露 ${fmt$(row.marketValue)} (${fmtPct(row.exposurePct)}) · 剩余成本 ${fmt$(row.remainingCost)}`;
      if (subDetailEl) subDetailEl.innerHTML = `回本线 ${fmt$(row.avgPrice)} · 标记 <span id="psim-pos-price-${pos.id}">${fmt$(row.currentPrice)}</span> · 离回本 ${fmtPct(row.breakEvenPct)} · 成本回收 ${fmtPct(row.costRecoveryPct)}`;
    });
  }

  function renderDashboard() {
    const state = getState();
    const summary = engine.getPortfolioSummary();
    const apiStatus = apiErrorCount > 0
      ? `<span class="psim-api-error" title="${escapeHtml(lastApiError ? lastApiError.message : '')}">⚠ API错误: ${apiErrorCount}</span>`
      : `<span class="psim-api-ok">● 已连接</span>`;
    return `
      <div class="psim-section" id="psim-dashboard">
        <div class="psim-section-title">概览 <span id="psim-api-status">${apiStatus}</span></div>
        <div class="psim-dash-strip">
          <div class="psim-dash-item"><span class="psim-label">现金</span><span class="psim-value" id="psim-val-cash">${fmt$(summary.cash)}</span></div>
          <div class="psim-dash-item"><span class="psim-label">总权益</span><span class="psim-value" id="psim-val-equity">${fmt$(summary.equity)}</span></div>
          <div class="psim-dash-item"><span class="psim-label">已实现盈亏</span><span class="psim-value ${summary.realizedPnl >= 0 ? 'psim-positive' : 'psim-negative'}" id="psim-val-realized-pnl">${fmt$(summary.realizedPnl)}</span></div>
        </div>
        <div class="psim-subtle">持仓数: ${state.positions.length} · 警报: ${state.alerts.filter((item) => item.active).length} · 自选: ${state.watchlist.length}</div>
      </div>
    `;
  }

  function renderCurrentMarket() {
    const slug = getPageSlug();
    if (!slug) return `<div class="psim-section" id="psim-current-market-section"><div class="psim-section-title">当前市场</div><div class="psim-empty">当前页面未检测到市场</div></div>`;

    const market = marketCache[slug] && marketCache[slug].data;
    if (!market) {
      if (marketInflight[slug]) {
        return `<div class="psim-section" id="psim-current-market-section"><div class="psim-section-title">当前市场</div><div class="psim-empty">正在加载市场...</div></div>`;
      }
      return `<div class="psim-section" id="psim-current-market-section"><div class="psim-section-title">当前市场</div><div class="psim-empty"><button class="psim-btn" id="psim-load-market">加载市场</button></div></div>`;
    }

    let html = `
      <div class="psim-section" id="psim-current-market-section">
        <div class="psim-section-title">当前市场 <button class="psim-watch-star" id="psim-watch-star" title="加入自选">☆</button></div>
        <div class="psim-market-title" title="${escapeHtml(market.question || market.title)}">${escapeHtml(truncate(market.question || market.title || 'Unknown', 80))}</div>
        <div id="psim-current-market-body" class="psim-empty">正在获取价格...</div>
      </div>
    `;
    {
      const count = getOutcomeCount(market);
      root.console.info('[PolySim] Market outcomes:', count, 'outcomes:', market.outcomes);
      fetchMarketPriceSnapshot(market, (_err, prices) => {
        const body = root.document.getElementById('psim-current-market-body');
        const starBtn = root.document.getElementById('psim-watch-star');
        const stateSnapshot = getState();
        const uiState = getUI();
        const orderQty = getOrderQuantity(uiState, stateSnapshot);
        const orderSizePresets = getOrderSizePresets(stateSnapshot);
        if (starBtn) {
          const isWatched = stateSnapshot.watchlist.some((item) => item.marketSlug === (market.slug || ''));
          starBtn.textContent = isWatched ? '★' : '☆';
          starBtn.onclick = () => {
            if (isWatched) {
              engine.removeWatchlistItem(market.slug || '');
              notify('已从自选移除', 'info');
            } else {
              engine.addWatchlistItem({ marketSlug: market.slug || '', marketTitle: market.question || market.title || '' });
              notify('已添加到自选', 'success');
            }
            render();
          };
        }
        if (!body) return;
        if (_err || !prices || prices.length === 0) {
          body.innerHTML = `<div class="psim-empty" style="color:#ef4444">获取价格失败: ${_err ? _err.message : 'No data'}.</div>`;
          return;
        }
        body.innerHTML = `
          <div class="psim-ticket-toolbar">
            <div class="psim-ticket-size-box">
              <span class="psim-ticket-size-label">下单量</span>
              <input type="number" id="psim-order-qty" class="psim-setting-input psim-order-qty" min="0.1" step="0.1" value="${orderQty}">
            </div>
            <div class="psim-size-presets">
              ${orderSizePresets.map((size) => `<button class="psim-size-chip ${Math.abs(size - orderQty) < 0.000001 ? 'active' : ''}" data-order-size="${size}">${size}x</button>`).join('')}
            </div>
            <div class="psim-ticket-hint">先定 size,再点 YES / NO</div>
          </div>
          <div class="psim-trade-board" id="psim-trade-board"></div>
        `;
        bindOrderSizeControls(body);
        const board = root.document.getElementById('psim-trade-board');
        if (!board) return;
        const readOrderQty = () => {
          const input = root.document.getElementById('psim-order-qty');
          return roundNumber(normalizePositiveNumber(input && input.value, orderQty), 6);
        };
        const openAlertDraftFor = (tokenId, outcomeName, snapshot) => {
          if (!tokenId) { notify('无 token ID', 'error'); return; }
          engine.prefillAlertDraft({
            marketSlug: market.slug || '',
            marketTitle: market.question || market.title || '',
            tokenId,
            outcomeName,
            priceTarget: snapshot.MID || '',
            side: 'ABOVE',
            priceType: 'MID'
          });
          render();
        };
        const count = getOutcomeCount(market);
        const isBinary = count === 2;
        if (isBinary) {
          [0, 1].forEach((index) => {
            const outcomeName = getOutcomeName(market, index);
            const quote = getQuoteSnapshot(prices[index] || {}, 'YES');
            const row = el('div', { class: 'psim-outcome-row psim-trade-ticket' });
            row.innerHTML = `
              <div class="psim-ticket-main">
                <div class="psim-ticket-head">
                  ${renderSideBadge(index === 0 ? 'YES' : 'NO')}
                  <span class="psim-outcome-name" title="${escapeHtml(outcomeName)}">${escapeHtml(outcomeName)}</span>
                </div>
                <div class="psim-quote-group">
                  ${renderQuoteStack(index === 0 ? 'YES' : 'NO', quote, index === 0 ? 'yes' : 'no', '', `
                    <div class="psim-quote-actions">
                      <button class="psim-btn ${index === 0 ? 'psim-buy' : 'psim-buy-no'}" data-action="buy-binary" data-index="${index}">买${index === 0 ? 'Y' : 'N'}</button>
                      <button class="psim-btn psim-alert-sm" data-action="alert-binary" data-index="${index}">🔔</button>
                    </div>
                  `, `data-quote-index="${index}" data-quote-side="YES"`)}
                </div>
              </div>
            `;
            board.appendChild(row);
          });
          board.insertAdjacentHTML('beforeend', `<div class="psim-binary-info">双边报价直接开仓;减仓与平仓在持仓区处理</div>`);
          board.querySelectorAll('[data-action="buy-binary"]').forEach((button) => {
            button.onclick = () => {
              const idx = parseInt(button.dataset.index, 10);
              const quantity = readOrderQty();
              const result = engine.buy(market, idx, quantity, prices);
              if (!result.ok) notify(result.error, 'error');
              else notify(`买入 ${getOutcomeName(market, idx)} ${quantity}x @ ${fmt$(result.execution.execPrice)}`, 'success');
              updateDashboard(); updateHoldings();
            };
          });
          board.querySelectorAll('[data-action="alert-binary"]').forEach((button) => {
            button.onclick = () => {
              const idx = parseInt(button.dataset.index, 10);
              const tokenId = getOutcomeTokenId(market, idx);
              openAlertDraftFor(tokenId, getOutcomeName(market, idx), getQuoteSnapshot(prices[idx] || {}, 'YES'));
            };
          });
        } else {
          // non-binary: sort by probability high→low, keep original order if price unavailable
          const hasSubMarkets = Array.isArray(market._subMarkets) && market._subMarkets.length > 0;
          const sortedIndices = Array.from({ length: count }, (_, i) => i).sort((a, b) => {
            const pa = (prices[a] || {}).midPrice || 0;
            const pb = (prices[b] || {}).midPrice || 0;
            if (pa === pb) return a - b; // stable: original order when equal
            return pb - pa;
          });
          for (let i = 0; i < sortedIndices.length; i += 1) {
            const index = sortedIndices[i];
            const price = prices[index] || {};
            const outcomeName = getOutcomeName(market, index);
            const hasNo = hasSubMarkets && !!getOutcomeTokenId(market, index, 'NO');
            const yesQuote = getQuoteSnapshot(price, 'YES');
            const noQuote = getQuoteSnapshot(price, 'NO');
            const row = el('div', { class: 'psim-outcome-row psim-trade-ticket' });
            row.innerHTML = `
              <div class="psim-ticket-main">
                <div class="psim-ticket-head">
                  <span class="psim-outcome-name" title="${escapeHtml(outcomeName)}">${escapeHtml(outcomeName)}</span>
                </div>
                <div class="psim-quote-group">
                  ${renderQuoteStack('YES', yesQuote, 'yes', '', `
                    <div class="psim-quote-actions">
                      <button class="psim-btn psim-buy" data-action="buy-yes" data-index="${index}">买Y</button>
                      <button class="psim-btn psim-alert-sm" data-action="alert-yes" data-index="${index}">🔔</button>
                    </div>
                  `, `data-quote-index="${index}" data-quote-side="YES"`)}
                  ${hasNo
                    ? renderQuoteStack('NO', noQuote, 'no', '', `
                      <div class="psim-quote-actions">
                        <button class="psim-btn psim-buy-no" data-action="buy-no" data-index="${index}">买N</button>
                        <button class="psim-btn psim-alert-sm" data-action="alert-no" data-index="${index}">🔔</button>
                      </div>
                    `, `data-quote-index="${index}" data-quote-side="NO"`)
                    : renderQuoteStack('NO', null, 'no', 'NO盘口缺失', `<div class="psim-quote-actions"><button class="psim-btn psim-disabled" data-action="buy-no-disabled" disabled title="该腿当前没有可交易的 NO token">等待NO流动性</button></div>`, `data-quote-index="${index}" data-quote-side="NO"`)}
                </div>
              </div>
            `;
            board.appendChild(row);
          }
          board.querySelectorAll('[data-action="buy-yes"]').forEach((button) => {
            button.onclick = () => {
              const index = parseInt(button.dataset.index, 10);
              const quantity = readOrderQty();
              const result = engine.buy(market, index, quantity, prices);
              if (!result.ok) notify(result.error, 'error');
              else notify(`买入 YES ${quantity}x "${getOutcomeName(market, index)}" @ ${fmt$(result.execution.execPrice)}`, 'success');
              updateDashboard(); updateHoldings();
            };
          });
          board.querySelectorAll('[data-action="buy-no"]').forEach((button) => {
            button.onclick = () => {
              const index = parseInt(button.dataset.index, 10);
              const quantity = readOrderQty();
              const result = engine.buy(market, index, quantity, prices, 'NO');
              if (!result.ok) notify(result.error, 'error');
              else notify(`买入 NO ${quantity}x "${getOutcomeName(market, index)}" @ ${fmt$(result.execution.execPrice)}`, 'success');
              updateDashboard(); updateHoldings();
            };
          });
          board.querySelectorAll('[data-action="alert-yes"]').forEach((button) => {
            button.onclick = () => {
              const index = parseInt(button.dataset.index, 10);
              const tokenId = getOutcomeTokenId(market, index);
              openAlertDraftFor(tokenId, getOutcomeName(market, index), getQuoteSnapshot(prices[index] || {}, 'YES'));
            };
          });
          board.querySelectorAll('[data-action="alert-no"]').forEach((button) => {
            button.onclick = () => {
              const index = parseInt(button.dataset.index, 10);
              const tokenId = getOutcomeTokenId(market, index, 'NO');
              openAlertDraftFor(tokenId, getOutcomeName(market, index, 'NO'), getQuoteSnapshot(prices[index] || {}, 'NO'));
            };
          });
        }
      });
    }

    return html;
  }

  function renderAlertForm() {
    const draft = getUI().alertDraft;
    if (!draft) return '';
    const cachedBuy = priceCache[`${draft.tokenId}:BUY`];
    const cachedSell = priceCache[`${draft.tokenId}:SELL`];
    const snapshot = buildPriceSnapshot({
      BUY: cachedBuy && cachedBuy.price,
      SELL: cachedSell && cachedSell.price
    });
    const observed = snapshot[draft.priceType];
    const priceTypes = ['MID', 'BUY', 'SELL'];
    const directions = [{ value: 'ABOVE', label: '高于↑' }, { value: 'BELOW', label: '低于↓' }];
    return `
      <div class="psim-section psim-alert-form">
        <div class="psim-section-title">警报设置</div>
        <div class="psim-alert-outcome">${escapeHtml(draft.outcomeName || '')}</div>
        ${observed != null ? `<div class="psim-alert-observed">当前参考价: <span class="psim-value">${fmt$(observed)}</span></div>` : ''}
        <div class="psim-alert-row">
          <label class="psim-alert-label">目标价格</label>
          <input type="number" id="psim-alert-price" class="psim-setting-input psim-alert-input" min="0.01" max="0.99" step="0.01" value="${draft.priceTarget || ''}" placeholder="0.00">
        </div>
        <div class="psim-alert-row">
          <label class="psim-alert-label">价格类型</label>
          <div class="psim-alert-toggle-group" id="psim-alert-ptype">
            ${priceTypes.map((t) => `<button class="psim-toggle-btn ${draft.priceType === t ? 'active' : ''}" data-ptype="${t}">${t}</button>`).join('')}
          </div>
        </div>
        <div class="psim-alert-row">
          <label class="psim-alert-label">触发条件</label>
          <div class="psim-alert-toggle-group" id="psim-alert-side">
            ${directions.map((d) => `<button class="psim-toggle-btn ${draft.side === d.value ? 'active' : ''}" data-side="${d.value}">${d.label}</button>`).join('')}
          </div>
        </div>
        <div class="psim-alert-btns">
          <button class="psim-btn psim-buy" id="psim-alert-confirm">确认</button>
          <button class="psim-btn" id="psim-alert-cancel">取消</button>
        </div>
      </div>
    `;
  }

  function renderWatchlist() {
    const state = getState();
    const items = state.watchlist;
    let html = `
      <div class="psim-section" id="psim-watchlist-section">
        <div class="psim-section-title">自选 (${items.length})</div>
    `;
    if (items.length === 0) {
      html += '<div class="psim-empty">暂无自选</div>';
    } else {
      const allItems = items.map((item) => {
        const market = marketCache[item.marketSlug] && marketCache[item.marketSlug].data;
        const tokenId = market ? getOutcomeTokenId(market, 0) : null;
        const cached = tokenId && priceCache[`${tokenId}:BUY`] ? priceCache[`${tokenId}:BUY`] : null;
        const cachedSell = tokenId && priceCache[`${tokenId}:SELL`] ? priceCache[`${tokenId}:SELL`] : null;
        const mid = cached && cachedSell ? getMidPrice({ buyPrice: cached.price, sellPrice: cachedSell.price }) : null;
        return { item, midPrice: mid };
      });
      const withPrices2 = allItems.filter(({ midPrice }) => midPrice != null && midPrice > 0).sort((a, b) => b.midPrice - a.midPrice);
      const lowProb = withPrices2.filter(({ midPrice }) => midPrice < 0.01);
      const highProb = withPrices2.filter(({ midPrice }) => midPrice >= 0.01);
      const noPrice = allItems.filter(({ midPrice }) => midPrice == null || midPrice <= 0);
      const orderedItems = [...highProb, ...lowProb, ...noPrice].map(({ item }) => item);
      html += '<div class="psim-watchlist">';
      orderedItems.forEach((item) => {
        const midPrice = allItems.find((x) => x.item.marketSlug === item.marketSlug)?.midPrice;
        const isCollapsed = item.collapsed || (midPrice != null && midPrice < 0.01);
        const prob = midPrice != null ? ` (${(midPrice * 100).toFixed(1)}%)` : '';
        if (isCollapsed) {
          html += `
            <div class="psim-watch-item psim-watch-collapsed">
              <button class="psim-watch-expand" data-watch-expand="${escapeHtml(item.marketSlug)}">▶</button>
              <a class="psim-watch-title" href="/event/${escapeHtml(item.marketSlug)}" target="_blank">${escapeHtml(truncate(item.marketTitle, 44))}</a>${prob}
              <button class="psim-btn psim-alert-rm" data-watch-remove="${escapeHtml(item.marketSlug)}">✕</button>
            </div>
          `;
        } else {
          html += `
            <div class="psim-watch-item">
              <a class="psim-watch-title" href="/event/${escapeHtml(item.marketSlug)}" title="${escapeHtml(item.marketTitle)}" target="_blank">${escapeHtml(truncate(item.marketTitle, 44))}</a>${prob}
              <button class="psim-btn psim-alert-rm" data-watch-remove="${escapeHtml(item.marketSlug)}">✕</button>
            </div>
          `;
        }
      });
      html += '</div>';
    }
    html += '</div>';
    return html;
  }

  function renderHoldings() {
    const ui = getUI();
    const rows = engine.getHoldingsView(ui.holdingsSort);
    let html = `
      <div class="psim-section" id="psim-holdings-section">
        <div class="psim-section-title">持仓</div>
        <div class="psim-search-row">
          <select id="psim-holdings-sort" class="psim-search-input">
            <option value="pnl_desc" ${ui.holdingsSort === 'pnl_desc' ? 'selected' : ''}>按盈亏</option>
            <option value="value_desc" ${ui.holdingsSort === 'value_desc' ? 'selected' : ''}>按市值</option>
            <option value="qty_desc" ${ui.holdingsSort === 'qty_desc' ? 'selected' : ''}>按数量</option>
            <option value="newest" ${ui.holdingsSort === 'newest' ? 'selected' : ''}>按最新</option>
          </select>
          <button class="psim-btn psim-close-all" id="psim-close-all-btn">全部平仓</button>
        </div>
    `;
    if (rows.length === 0) {
      html += '<div class="psim-empty">暂无持仓</div>';
    } else {
      rows.forEach((row) => {
        const side = getOutcomeSide(row.outcome);
        const outcomeName = getOutcomeDisplayName(row.outcome);
        html += `
          <div class="psim-position-row">
            <div class="psim-pos-header">
              <div class="psim-pos-title-wrap">
                ${renderSideBadge(side, true)}
                <a class="psim-pos-outcome" href="/event/${escapeHtml(row.marketSlug)}" target="_blank" title="${escapeHtml(row.outcome)}">${escapeHtml(truncate(outcomeName, 32))}</a>
              </div>
              <span class="psim-pos-pnl ${row.pnl >= 0 ? 'psim-positive' : 'psim-negative'}" id="psim-pos-pnl-${row.id}">${fmt$(row.pnl)} (${fmtPct(row.pnlPct)})</span>
            </div>
            <div class="psim-pos-details" id="psim-pos-details-main-${row.id}">持仓 ${row.quantity}x · 暴露 ${fmt$(row.marketValue)} (${fmtPct(row.exposurePct)}) · 剩余成本 ${fmt$(row.remainingCost)}</div>
            <div class="psim-pos-details psim-pos-subdetails" id="psim-pos-details-sub-${row.id}">回本线 ${fmt$(row.avgPrice)} · 标记 <span id="psim-pos-price-${row.id}">${fmt$(row.currentPrice)}</span> · 离回本 ${fmtPct(row.breakEvenPct)} · 成本回收 ${fmtPct(row.costRecoveryPct)}</div>
            <div class="psim-pos-actions">
              <input type="number" class="psim-qty-input" min="0.1" step="0.1" value="${row.quantity}" data-position-qty="${row.id}">
              <button class="psim-btn psim-sell" data-position-sell="${row.id}">减仓</button>
              <button class="psim-btn psim-close-position" data-position-close="${row.id}">平仓</button>
            </div>
          </div>
        `;
      });
    }
    html += '</div>';
    return html;
  }

  function renderHistory() {
    const history = getState().history;
    let html = `
      <div class="psim-section" id="psim-history-section">
        <div class="psim-section-title">历史 (${history.length})</div>
    `;
    if (history.length === 0) {
      html += '<div class="psim-empty">暂无历史</div>';
    } else {
      html += '<div class="psim-history-list">';
      history.slice(0, 60).forEach((entry) => {
          const side = getOutcomeSide(entry.outcome);
          const outcomeName = getOutcomeDisplayName(entry.outcome);
          html += `
            <div class="psim-history-row" title="${escapeHtml(`${entry.marketTitle} · ${entry.outcome}`)}">
              <div class="psim-h-main">
                <span class="psim-h-time">${escapeHtml(formatTime(entry.time))}</span>
                <span class="psim-h-action">${escapeHtml(formatHistoryAction(entry.action))}</span>
                ${renderSideBadge(side, true)}
                <span class="psim-h-market">${escapeHtml(truncate(outcomeName, 28))}</span>
                <span class="psim-h-fill">${entry.quantity}x @ ${fmt$(entry.price)}</span>
              </div>
              <div class="psim-h-meta">
                <span class="psim-h-net">净额 ${fmt$(entry.netValue || entry.grossValue)}</span>
                <span class="psim-h-fee">费 ${fmt$(entry.fee || 0)}</span>
                <span class="psim-h-pnl ${entry.pnl >= 0 ? 'psim-positive' : 'psim-negative'}">${entry.pnl ? `盈亏 ${fmt$(entry.pnl)}` : '盈亏 —'}</span>
                <span class="psim-h-tail">后: 现金 ${fmt$(entry.cashAfter)} · 持仓 ${entry.positionAfter}x · 已实 ${fmt$(entry.realizedAfter)}</span>
              </div>
            </div>
          `;
      });
      html += '</div>';
      html += `
        <div class="psim-history-btns">
          <button class="psim-btn" id="psim-export-csv">导出 CSV</button>
          <button class="psim-btn" id="psim-export-json">导出 JSON</button>
          <button class="psim-btn" id="psim-import-csv">导入 CSV</button>
          <button class="psim-btn" id="psim-import-json">导入 JSON</button>
        </div>
      `;
    }
    html += '</div>';
    return html;
  }

  function renderSettings() {
    const state = getState();
    const settings = state.settings;
    let html = `
      <div class="psim-section" id="psim-settings-section">
        <div class="psim-section-title">设置</div>
    `;
    html += `
      <div class="psim-collapsible-body">
        <div class="psim-setting-row"><label>刷新(秒)</label><input type="number" class="psim-setting-input" id="psim-set-refresh" min="2" max="60" value="${settings.refreshSeconds}"></div>
        <div class="psim-setting-row"><label>默认数量</label><input type="number" class="psim-setting-input" id="psim-set-qty" min="0.1" step="0.1" value="${settings.defaultQuantity}"></div>
        <div class="psim-setting-row"><label>初始资金</label><input type="number" class="psim-setting-input" id="psim-set-balance" min="100" value="${state.initialBalance}"></div>
        <div class="psim-setting-row"><label>费率(%)</label><input type="number" class="psim-setting-input" id="psim-set-fee" min="0" max="10" step="0.1" value="${roundNumber(settings.feeRate * 100, 2)}"></div>
        <div class="psim-setting-row"><label>滑点(%)</label><input type="number" class="psim-setting-input" id="psim-set-slip" min="0" max="20" step="0.1" value="${roundNumber(settings.slippageRate * 100, 2)}"></div>
        <div class="psim-setting-row"><label>深色模式</label><input type="checkbox" id="psim-set-dark" ${settings.darkMode ? 'checked' : ''}></div>
        <div class="psim-btn-row">
          <button class="psim-btn" id="psim-save-settings">保存</button>
          <button class="psim-btn" id="psim-clear-history">清空历史</button>
          <button class="psim-btn" id="psim-clear-alerts">清空警报</button>
          <button class="psim-btn" id="psim-clear-watchlist">清空自选</button>
          <button class="psim-btn psim-danger" id="psim-reset-state">重置全部</button>
        </div>
      </div>
    `;
    html += '</div>';
    return html;
  }

  function renderAlertsPanel() {
    const state = getState();
    const alerts = [...state.alerts];
    let html = '';
    if (getUI().alertDraft) html += renderAlertForm();
    html += `
      <div class="psim-section" id="psim-alerts-section">
        <div class="psim-section-title">警报 (${alerts.length})</div>
    `;
    if (alerts.length === 0) {
      html += '<div class="psim-empty">暂无警报</div>';
    } else {
      html += '<div class="psim-alerts-list">';
      alerts.forEach((alert) => {
        html += `
          <div class="psim-alert-item">
            <div class="psim-alert-item-main">
              <span class="psim-alert-item-name">${escapeHtml(truncate(alert.outcomeName, 44))}</span>
              <span class="psim-alert-item-meta">${escapeHtml(alert.side === 'ABOVE' ? '≥' : '≤')} ${fmt$(alert.priceTarget)} · ${escapeHtml(alert.priceType)} · ${alert.active ? 'ACTIVE' : 'HIT'}</span>
            </div>
            <button class="psim-btn psim-alert-rm" data-alert-remove="${escapeHtml(alert.id)}">✕</button>
          </div>
        `;
      });
      html += '</div>';
    }
    html += '</div>';
    return html;
  }

  function renderMainTabs() {
    const ui = getUI();
    const state = getState();
    const tabs = [
      { key: 'market', label: '市场' },
      { key: 'holdings', label: `持仓 ${state.positions.length}` },
      { key: 'watchlist', label: `自选 ${state.watchlist.length}` },
      { key: 'history', label: `历史 ${state.history.length}` },
      { key: 'alerts', label: `警报 ${state.alerts.length}` },
      { key: 'settings', label: '设置' }
    ];
    return `
      <div class="psim-main-tabs" id="psim-main-tabs">
        ${tabs.map((tab) => `<button class="psim-main-tab ${ui.activeTab === tab.key ? 'active' : ''}" data-main-tab="${tab.key}">${escapeHtml(tab.label)}</button>`).join('')}
      </div>
    `;
  }

  function renderWorkspace() {
    const ui = getUI();
    const activeTab = normalizeMainTab(ui.activeTab);
    let body = '';
    if (activeTab === 'market') {
      body = `<div class="psim-workspace" id="psim-market-workspace">${renderCurrentMarket()}${renderAlertForm()}</div>`;
    } else if (activeTab === 'holdings') {
      body = `<div class="psim-workspace">${renderHoldings()}</div>`;
    } else if (activeTab === 'watchlist') {
      body = `<div class="psim-workspace">${renderWatchlist()}</div>`;
    } else if (activeTab === 'history') {
      body = `<div class="psim-workspace">${renderHistory()}</div>`;
    } else if (activeTab === 'alerts') {
      body = `<div class="psim-workspace">${renderAlertsPanel()}</div>`;
    } else {
      body = `<div class="psim-workspace">${renderSettings()}</div>`;
    }
    return `<div id="psim-workspace-root">${body}</div>`;
  }

  function render() {
    if (!contentEl) return;
    marketWorkspaceCacheEl = null;
    marketWorkspaceCacheKey = null;
    contentEl.innerHTML = [renderDashboard(), renderMainTabs(), renderWorkspace()].join('');
    bindUi();
  }

  function updateMainTabs() {
    const el = root.document.getElementById('psim-main-tabs');
    if (!el) return;
    const tmp = root.document.createElement('div');
    tmp.innerHTML = renderMainTabs();
    el.outerHTML = tmp.innerHTML;
    bindUi();
  }

  function updateWorkspace() {
    const el = root.document.getElementById('psim-workspace-root');
    if (!el) return;
    const activeTab = normalizeMainTab(getUI().activeTab);
    const cacheKey = getMarketWorkspaceCacheKey();
    if (activeTab === 'market' && marketWorkspaceCacheEl && marketWorkspaceCacheKey === cacheKey) {
      el.innerHTML = '';
      el.appendChild(marketWorkspaceCacheEl);
      bindUi();
      scheduleCurrentMarketPriceUpdate();
      return;
    }
    const tmp = root.document.createElement('div');
    tmp.innerHTML = renderWorkspace();
    el.outerHTML = tmp.innerHTML;
    bindUi();
  }

  function updateDashboard() {
    const el = root.document.getElementById('psim-dashboard');
    if (el) {
      el.outerHTML = renderDashboard();
      updateMainTabs();
    }
    else contentEl.innerHTML = [renderDashboard(), renderMainTabs(), renderWorkspace()].join('');
    bindUi();
  }

  function updateCurrentMarket() {
    const current = root.document.getElementById('psim-current-market-section');
    if (!current) return;
    const tmp = root.document.createElement('div');
    tmp.innerHTML = renderCurrentMarket();
    current.outerHTML = tmp.innerHTML;
    bindUi();
  }

  function updateCurrentMarketPrices() {
    marketUiLastUpdateAt = Date.now();
    const slug = getPageSlug();
    if (!slug) return;
    const market = marketCache[slug] && marketCache[slug].data;
    if (!market) return;
    const board = root.document.getElementById('psim-trade-board');
    if (!board) return;
    fetchMarketPriceSnapshot(market, (_err, prices) => {
      if (_err || !Array.isArray(prices)) return;
      board.querySelectorAll('.psim-quote-stack[data-quote-index][data-quote-side]').forEach((stack) => {
        const index = parseInt(stack.getAttribute('data-quote-index'), 10);
        const side = stack.getAttribute('data-quote-side') === 'NO' ? 'NO' : 'YES';
        const snapshot = getQuoteSnapshot(prices[index] || {}, side);
        const bestEl = stack.querySelector('.psim-quote-best');
        const bestValueEl = stack.querySelector('.psim-quote-best-value');
        const metaEl = stack.querySelector('.psim-quote-meta');
        const hasPrice = snapshot.MID != null || snapshot.BUY != null || snapshot.SELL != null;

        stack.classList.toggle('psim-quote-empty', !hasPrice);
        if (hasPrice) {
          if (bestEl && !bestEl.querySelector('.psim-quote-best-tag')) {
            bestEl.innerHTML = `<span class="psim-quote-best-tag">B</span><span class="psim-quote-best-value">${fmt$(snapshot.BUY || 0)}</span>`;
          }
          updateLiveText(bestValueEl || bestEl, fmt$(snapshot.BUY || 0), Number(snapshot.BUY || 0));
        } else {
          updateLiveText(bestEl, 'NO盘口缺失', Number.NaN, { pulse: false });
        }
        updateLiveText(metaEl, hasPrice ? `M ${fmt$(snapshot.MID || 0)} · 出 ${fmt$(snapshot.SELL || 0)}` : 'M — · 出 —', hasPrice ? Number(snapshot.MID || 0) : Number.NaN, { pulse: false });
      });
    }, true);
  }

  function scheduleCurrentMarketPriceUpdate() {
    const now = Date.now();
    const remaining = MARKET_UI_THROTTLE_MS - (now - marketUiLastUpdateAt);
    if (remaining <= 0) {
      if (marketUiUpdateTimer) {
        root.clearTimeout(marketUiUpdateTimer);
        marketUiUpdateTimer = null;
      }
      updateCurrentMarketPrices();
      return;
    }
    if (marketUiUpdateTimer) return;
    marketUiUpdateTimer = root.setTimeout(() => {
      marketUiUpdateTimer = null;
      updateCurrentMarketPrices();
    }, remaining);
  }

  function schedulePortfolioPriceUpdate() {
    if (portfolioUiUpdateTimer) root.clearTimeout(portfolioUiUpdateTimer);
    portfolioUiUpdateTimer = root.setTimeout(() => {
      portfolioUiUpdateTimer = null;
      updatePrices();
    }, PORTFOLIO_UI_DELAY_MS);
  }

  function ensureSnapGuide() {
    if (snapGuideEl && root.document.body.contains(snapGuideEl)) return snapGuideEl;
    snapGuideEl = root.document.createElement('div');
    snapGuideEl.id = 'psim-snap-guide';
    root.document.body.appendChild(snapGuideEl);
    return snapGuideEl;
  }

  function hideSnapGuide() {
    if (!snapGuideEl) return;
    snapGuideEl.classList.remove('visible');
  }

  function showSnapGuide(side) {
    const guide = ensureSnapGuide();
    const viewportWidth = Math.max(root.innerWidth || 0, 320);
    guide.classList.toggle('left', side === 'left');
    guide.classList.toggle('right', side === 'right');
    guide.style.left = side === 'right' ? `${viewportWidth - PANEL_EDGE_OFFSET}px` : `${PANEL_EDGE_OFFSET}px`;
    guide.classList.add('visible');
  }

  function updateHoldings() {
    const el = root.document.getElementById('psim-holdings-section');
    if (el) {
      const tmp = root.document.createElement('div');
      tmp.innerHTML = renderHoldings();
      el.outerHTML = tmp.innerHTML;
      bindHoldingsEvents();
    }
  }

  function updateHistory() {
    const el = root.document.getElementById('psim-history-section');
    if (el) {
      const tmp = root.document.createElement('div');
      tmp.innerHTML = renderHistory();
      el.outerHTML = tmp.innerHTML;
      bindHistoryEvents();
    }
  }

  function updateWatchlist() {
    const el = root.document.getElementById('psim-watchlist-section');
    if (!el) return;
    const state = getState();
    const items = [...state.watchlist];
    if (items.length === 0) {
      const tmp = root.document.createElement('div');
      tmp.innerHTML = renderWatchlist();
      el.outerHTML = tmp.innerHTML;
      bindWatchlistEvents();
      return;
    }
    let pending = items.length;
    const slugPriceMap = {};
    const slugIdxMap = {};
    items.forEach((item, idx) => { slugIdxMap[item.marketSlug] = idx; });
    items.forEach((item) => {
      getMarketBySlug(item.marketSlug, (err, market) => {
        if (err || !market) { pending -= 1; if (pending === 0) renderWatchlistWithPrices(items, slugPriceMap, el); return; }
        const tokenId = getOutcomeTokenId(market, 0);
        if (!tokenId) { pending -= 1; if (pending === 0) renderWatchlistWithPrices(items, slugPriceMap, el); return; }
        getClobPrice(tokenId, 'BUY', (_err, buyPrice) => {
          getClobPrice(tokenId, 'SELL', (_err2, sellPrice) => {
            slugPriceMap[item.marketSlug] = { buyPrice, sellPrice, midPrice: getMidPrice({ buyPrice, sellPrice }) };
            pending -= 1;
            if (pending === 0) renderWatchlistWithPrices(items, slugPriceMap, el);
          });
        });
      });
    });
  }

  function renderWatchlistWithPrices(items, slugPriceMap, containerEl) {
    const allWithPrice = items.map((item) => ({ item, price: slugPriceMap[item.marketSlug] || {} }))
      .sort((a, b) => {
        const aP = a.price.midPrice;
        const bP = b.price.midPrice;
        const aValid = aP != null && aP > 0;
        const bValid = bP != null && bP > 0;
        if (aValid && bValid) return bP - aP;
        if (aValid) return -1;
        if (bValid) return 1;
        return 0; // both invalid: keep original order
      });
    const lowProb = allWithPrice.filter(({ price }) => price.midPrice != null && price.midPrice > 0 && price.midPrice < 0.01);
    const highProb = allWithPrice.filter(({ price }) => price.midPrice != null && price.midPrice >= 0.01);
    const noPrice = allWithPrice.filter(({ price }) => price.midPrice == null || price.midPrice <= 0);
    const ordered = [...highProb, ...lowProb, ...noPrice];
    let html = `
      <div class="psim-section" id="psim-watchlist-section">
        <div class="psim-section-title">自选 (${items.length})</div>
        <div class="psim-watchlist">
    `;
    ordered.forEach(({ item, price }) => {
      if (item.collapsed === undefined) item.collapsed = price.midPrice != null && price.midPrice < 0.01;
      const isCollapsed = item.collapsed;
      const prob = price.midPrice != null && price.midPrice > 0 ? ` (${(price.midPrice * 100).toFixed(1)}%)` : price.midPrice != null ? ' (?)' : '';
      if (isCollapsed) {
        html += `
          <div class="psim-watch-item psim-watch-collapsed">
            <button class="psim-watch-expand" data-watch-expand="${escapeHtml(item.marketSlug)}">▶</button>
            <a class="psim-watch-title" href="/event/${escapeHtml(item.marketSlug)}" target="_blank">${escapeHtml(truncate(item.marketTitle, 44))}</a>${prob}
            <button class="psim-btn psim-alert-rm" data-watch-remove="${escapeHtml(item.marketSlug)}">✕</button>
          </div>
        `;
      } else {
        html += `
          <div class="psim-watch-item">
            <a class="psim-watch-title" href="/event/${escapeHtml(item.marketSlug)}" title="${escapeHtml(item.marketTitle)}" target="_blank">${escapeHtml(truncate(item.marketTitle, 44))}</a>${prob}
            <button class="psim-btn psim-alert-rm" data-watch-remove="${escapeHtml(item.marketSlug)}">✕</button>
          </div>
        `;
      }
    });
    html += '</div></div>';
    const tmp = root.document.createElement('div');
    tmp.innerHTML = html;
    containerEl.outerHTML = tmp.innerHTML;
    bindWatchlistEvents();
  }

  function bindHoldingsEvents() {
    const sortSelect = root.document.getElementById('psim-holdings-sort');
    if (sortSelect) sortSelect.onchange = () => { engine.patchUI({ holdingsSort: sortSelect.value }); updateHoldings(); };
    const closeAll = root.document.getElementById('psim-close-all-btn');
    if (closeAll) closeAll.onclick = () => {
      const priceMap = {};
      getState().positions.forEach((p) => { if (p.lastPrice) priceMap[p.tokenId] = p.lastPrice; });
      const result = engine.closeAllPositions(priceMap);
      if (!result.ok) notify(result.error, 'error');
      else notify(`已平仓 ${result.closedCount} 个仓位。总盈亏: ${fmt$(result.totalPnl)}`, result.totalPnl >= 0 ? 'success' : 'info');
      updateDashboard(); updateHoldings();
    };
    root.document.querySelectorAll('[data-position-sell]').forEach((btn) => {
      btn.onclick = () => {
        const posId = btn.dataset.positionSell;
        const input = root.document.querySelector(`[data-position-qty="${posId}"]`);
        const qty = parseFloat(input && input.value) || 0;
        const pos = getState().positions.find((p) => p.id === posId);
        if (!pos) return;
        const result = engine.sellByPositionId(posId, qty, pos.lastPrice, 'SELL', pos.marketTitle);
        if (!result.ok) notify(result.error, 'error');
        else notify(`减仓 ${qty}x ${pos.outcome} @ ${fmt$(result.execution.execPrice)} | 盈亏: ${fmt$(result.pnl)}`, result.pnl >= 0 ? 'success' : 'info');
        updateDashboard(); updateHoldings();
      };
    });
    root.document.querySelectorAll('[data-position-close]').forEach((btn) => {
      btn.onclick = () => {
        const posId = btn.dataset.positionClose;
        const pos = getState().positions.find((p) => p.id === posId);
        if (!pos) return;
        const result = engine.closePosition(posId, pos.lastPrice);
        if (!result.ok) notify(result.error, 'error');
        else notify(`已平仓 ${pos.outcome}: ${fmt$(result.pnl)}`, result.pnl >= 0 ? 'success' : 'info');
        updateDashboard(); updateHoldings();
      };
    });
  }

  function bindHistoryEvents() {
    root.document.querySelectorAll('[data-position-sell]').forEach((btn) => {
      btn.onclick = () => {
        const posId = btn.dataset.positionSell;
        const input = root.document.querySelector(`[data-position-qty="${posId}"]`);
        const qty = parseFloat(input && input.value) || 0;
        const pos = getState().positions.find((p) => p.id === posId);
        if (!pos) return;
        const result = engine.sellByPositionId(posId, qty, pos.lastPrice, 'SELL', pos.marketTitle);
        if (!result.ok) notify(result.error, 'error');
        else notify(`减仓 ${qty}x ${pos.outcome} @ ${fmt$(result.execution.execPrice)} | 盈亏: ${fmt$(result.pnl)}`, result.pnl >= 0 ? 'success' : 'info');
        updateDashboard(); updateHoldings();
      };
    });
    root.document.querySelectorAll('[data-position-close]').forEach((btn) => {
      btn.onclick = () => {
        const posId = btn.dataset.positionClose;
        const pos = getState().positions.find((p) => p.id === posId);
        if (!pos) return;
        const result = engine.closePosition(posId, pos.lastPrice);
        if (!result.ok) notify(result.error, 'error');
        else notify(`已平仓 ${pos.outcome}: ${fmt$(result.pnl)}`, result.pnl >= 0 ? 'success' : 'info');
        updateDashboard(); updateHoldings();
      };
    });
  }

  function bindWatchlistEvents() {
    root.document.querySelectorAll('[data-watch-remove]').forEach((btn) => {
      btn.onclick = () => { engine.removeWatchlistItem(btn.dataset.watchRemove); updateWatchlist(); };
    });
    root.document.querySelectorAll('[data-watch-expand]').forEach((btn) => {
      btn.onclick = () => {
        const slug = btn.dataset.watchExpand;
        const item = getState().watchlist.find((i) => i.marketSlug === slug);
        if (item) { item.collapsed = false; updateWatchlist(); }
      };
    });
  }

  function bindUi() {
    const currentPanel = panelEl;
    if (!currentPanel) return;

    const ui = getUI();
    currentPanel.classList.toggle('psim-dark-off', !getState().settings.darkMode);
    currentPanel.classList.toggle('psim-collapsed', ui.collapsed);
    currentPanel.classList.toggle('psim-wide', ui.panelWidth >= 560);
    currentPanel.classList.toggle('psim-narrow', ui.panelWidth <= 380);
    const collapseButton = root.document.getElementById('psim-collapse-btn');
    if (collapseButton) collapseButton.textContent = ui.collapsed ? '+' : '─';

    const loadMarketBtn = root.document.getElementById('psim-load-market');
    if (loadMarketBtn) loadMarketBtn.onclick = () => {
      const slug = getPageSlug();
      if (!slug) return;
      getMarketBySlug(slug, (err, market) => {
        if (err || !market) notify('加载市场失败', 'error');
        else { marketCache[slug] = { data: market, ts: Date.now() }; render(); }
      });
    };

    root.document.querySelectorAll('[data-main-tab]').forEach((btn) => {
      btn.onclick = () => {
        const nextTab = normalizeMainTab(btn.dataset.mainTab);
        const prevTab = normalizeMainTab(getUI().activeTab);
        if (prevTab === 'market' && nextTab !== 'market') stashMarketWorkspace();
        engine.patchUI({ activeTab: nextTab });
        updateMainTabs();
        updateWorkspace();
      };
    });

    const sortSelect = root.document.getElementById('psim-holdings-sort');
    if (sortSelect) sortSelect.onchange = () => { engine.patchUI({ holdingsSort: sortSelect.value }); updateHoldings(); };

    const closeAll = root.document.getElementById('psim-close-all-btn');
    if (closeAll) closeAll.onclick = () => {
      const priceMap = {};
      getState().positions.forEach((position) => { if (position.lastPrice) priceMap[position.tokenId] = position.lastPrice; });
      const result = engine.closeAllPositions(priceMap);
      if (!result.ok) notify(result.error, 'error');
      else notify(`已平仓 ${result.closedCount} 个仓位。总盈亏: ${fmt$(result.totalPnl)}`, result.totalPnl >= 0 ? 'success' : 'info');
      updateDashboard(); updateHoldings();
    };

    // position sell/close events now bound via bindHoldingsEvents()
    bindHoldingsEvents();
    root.document.querySelectorAll('[data-alert-remove]').forEach((btn) => {
      btn.onclick = () => {
        engine.removeAlert(btn.dataset.alertRemove);
        render();
      };
    });

    bindOrderSizeControls(currentPanel);

    root.document.querySelectorAll('[data-watch-remove]').forEach((btn) => {
      btn.onclick = () => { engine.removeWatchlistItem(btn.dataset.watchRemove); updateWatchlist(); };
    });

    const alertConfirm = root.document.getElementById('psim-alert-confirm');
    if (alertConfirm) alertConfirm.onclick = () => {
      const priceInput = root.document.getElementById('psim-alert-price');
      const priceTarget = parseFloat(priceInput && priceInput.value);
      if (!Number.isFinite(priceTarget) || priceTarget <= 0 || priceTarget > 1) { notify('价格无效', 'error'); return; }
      const currentDraft = getUI().alertDraft;
      engine.prefillAlertDraft({ ...currentDraft, priceTarget });
      const result = engine.submitAlertDraft();
      if (!result.ok) { notify(result.error, 'error'); return; }
      notify('警报已添加', 'success');
      render();
    };

    const alertCancel = root.document.getElementById('psim-alert-cancel');
    if (alertCancel) alertCancel.onclick = () => { engine.clearAlertDraft(); render(); };

    root.document.querySelectorAll('#psim-alert-ptype .psim-toggle-btn').forEach((btn) => {
      btn.onclick = () => {
        const draft = getUI().alertDraft;
        engine.prefillAlertDraft({ ...draft, priceType: btn.dataset.ptype });
        render();
      };
    });

    root.document.querySelectorAll('#psim-alert-side .psim-toggle-btn').forEach((btn) => {
      btn.onclick = () => {
        const draft = getUI().alertDraft;
        engine.prefillAlertDraft({ ...draft, side: btn.dataset.side });
        render();
      };
    });

    const exportCsv = root.document.getElementById('psim-export-csv');
    if (exportCsv) exportCsv.onclick = () => downloadFile(engine.exportBackupCSV(), `polymarket_backup_${Date.now()}.csv`, 'text/csv');
    const exportJson = root.document.getElementById('psim-export-json');
    if (exportJson) exportJson.onclick = () => downloadFile(engine.exportBackupJSON(), `polymarket_backup_${Date.now()}.json`, 'application/json');
    const importCsv = root.document.getElementById('psim-import-csv');
    if (importCsv) importCsv.onclick = () => pickFile('.csv', (text) => { engine.importBackupCSV(text); render(); notify('CSV 备份已导入', 'success'); });
    const importJson = root.document.getElementById('psim-import-json');
    if (importJson) importJson.onclick = () => pickFile('.json', (text) => { engine.importBackupJSON(text); render(); notify('JSON 备份已导入', 'success'); });

    const saveSettings = root.document.getElementById('psim-save-settings');
    if (saveSettings) saveSettings.onclick = () => {
      const state = getState();
      const currentUi = getUI();
      const newBalance = parseFloat(root.document.getElementById('psim-set-balance').value) || state.initialBalance;
      const newDefaultQuantity = Math.max(1, parseFloat(root.document.getElementById('psim-set-qty').value) || state.settings.defaultQuantity);
      engine.replaceState({
        ...state,
        initialBalance: newBalance,
        cash: newBalance,
        realizedPnl: 0,
        positions: [],
        history: [],
        settings: {
          ...state.settings,
          refreshSeconds: Math.max(2, parseInt(root.document.getElementById('psim-set-refresh').value, 10) || state.settings.refreshSeconds),
          defaultQuantity: newDefaultQuantity,
          feeRate: (parseFloat(root.document.getElementById('psim-set-fee').value) || 0) / 100,
          slippageRate: (parseFloat(root.document.getElementById('psim-set-slip').value) || 0) / 100,
          darkMode: !!root.document.getElementById('psim-set-dark').checked
        }
      });
      if (Math.abs(Number(currentUi.orderQuantity || 0) - Number(state.settings.defaultQuantity || 0)) < 0.000001) {
        engine.patchUI({ orderQuantity: newDefaultQuantity });
      }
      startRefresh();
      render();
      notify(`资金已重置为 ${fmt$(newBalance)},持仓已清空`, "success");
    };

    const clearHistory = root.document.getElementById('psim-clear-history');
    if (clearHistory) clearHistory.onclick = () => { engine.clearHistory(); updateDashboard(); updateHistory(); notify('历史已清空', 'info'); };
    const clearAlerts = root.document.getElementById('psim-clear-alerts');
    if (clearAlerts) clearAlerts.onclick = () => { engine.clearAlerts(); updateDashboard(); notify('警报已清空', 'info'); };
    const clearWatchlist = root.document.getElementById('psim-clear-watchlist');
    if (clearWatchlist) clearWatchlist.onclick = () => { engine.clearWatchlist(); updateDashboard(); updateWatchlist(); notify('自选已清空', 'info'); };
    const resetState = root.document.getElementById('psim-reset-state');
    if (resetState) resetState.onclick = () => { if (root.confirm('重置所有数据?')) { engine.resetAll(); render(); notify('已全部重置', 'info'); } };
  }

  function pickFile(accept, onLoad) {
    const input = root.document.createElement('input');
    input.type = 'file';
    input.accept = accept;
    input.onchange = (event) => {
      const file = event.target.files && event.target.files[0];
      if (!file) return;
      const reader = new root.FileReader();
      reader.onload = (loadEvent) => onLoad(loadEvent.target.result);
      reader.readAsText(file);
    };
    input.click();
  }

  function makeDraggable(panel, handle) {
    let dragging = false;
    let startX = 0;
    let startY = 0;
    let originX = 0;
    let originY = 0;
    handle.style.cursor = 'move';
    const isInteractiveTarget = (target) => !!(target && target.closest && target.closest('button, input, select, textarea, a, label, #psim-resize-handle, #psim-resize-top-handle, #psim-resize-bottom-handle, .psim-watch-star, [data-action], [data-order-size], [data-watch-remove], [data-watch-expand], [data-main-tab], [data-alert-remove], #psim-alert-ptype, #psim-alert-side'));
    panel.addEventListener('mousedown', (event) => {
      if (isInteractiveTarget(event.target)) return;
      if (event.button !== 0) return;
      dragging = true;
      panel.classList.add('psim-dragging');
      startX = event.clientX;
      startY = event.clientY;
      const rect = panel.getBoundingClientRect();
      originX = rect.left;
      originY = rect.top;
      panel.style.right = 'auto';
      panel.style.left = `${originX}px`;
      panel.style.top = `${originY}px`;
      event.preventDefault();
    });
    root.document.addEventListener('mousemove', (event) => {
      if (!dragging) return;
      const nextLeft = originX + event.clientX - startX;
      const nextTop = originY + event.clientY - startY;
      const clamped = clampPanelPosition(nextLeft, nextTop, panel.offsetWidth, panel.offsetHeight);
      const viewportWidth = Math.max(root.innerWidth || 0, 320);
      const leftDistance = Math.abs(PANEL_EDGE_OFFSET - clamped.left);
      const rightDistance = Math.abs((viewportWidth - panel.offsetWidth - PANEL_EDGE_OFFSET) - clamped.left);
      if (leftDistance <= PANEL_SNAP_GAP) showSnapGuide('left');
      else if (rightDistance <= PANEL_SNAP_GAP) showSnapGuide('right');
      else hideSnapGuide();
      panel.style.left = `${clamped.left}px`;
      panel.style.top = `${clamped.top}px`;
    });
    root.document.addEventListener('mouseup', () => {
      if (!dragging) return;
      dragging = false;
      const rect = panel.getBoundingClientRect();
      const snapped = snapPanelPosition(rect.left, rect.top, rect.width, rect.height);
      const snappedToEdge = Math.abs(snapped.left - rect.left) > 0.5;
      panel.style.left = `${snapped.left}px`;
      panel.style.top = `${snapped.top}px`;
      panel.classList.remove('psim-dragging');
      hideSnapGuide();
      if (snappedToEdge) {
        panel.classList.remove('psim-snap-settle');
        void panel.offsetWidth;
        panel.classList.add('psim-snap-settle');
        root.setTimeout(() => {
          panel.classList.remove('psim-snap-settle');
        }, 220);
      }
      engine.patchUI({ panelX: snapped.left, panelY: snapped.top });
    });
  }

  function makeResizable(panel, widthHandle, topHandle, bottomHandle) {
    let resizeMode = null;
    let startX = 0;
    let startY = 0;
    let startW = 0;
    let startH = 0;
    let startTop = 0;
    let anchoredLeft = false;

    const startResize = (mode, event) => {
      resizeMode = mode;
      startX = event.clientX;
      startY = event.clientY;
      startW = panel.offsetWidth;
      startH = panel.offsetHeight;
      startTop = panel.getBoundingClientRect().top;
      anchoredLeft = !!panel.style.left && panel.style.left !== 'auto';
      panel.classList.add('psim-resizing');
      event.preventDefault();
      event.stopPropagation();
    };

    if (widthHandle) {
      widthHandle.addEventListener('mousedown', (event) => startResize('width', event));
      widthHandle.addEventListener('dblclick', (event) => {
        event.preventDefault();
        event.stopPropagation();
        panel.style.width = `${PANEL_WIDTH.DEFAULT}px`;
        engine.patchUI({ panelWidth: PANEL_WIDTH.DEFAULT });
      });
    }
    if (topHandle) topHandle.addEventListener('mousedown', (event) => startResize('top-height', event));
    if (bottomHandle) bottomHandle.addEventListener('mousedown', (event) => startResize('bottom-height', event));

    root.document.addEventListener('mousemove', (event) => {
      if (!resizeMode) return;
      if (resizeMode === 'width') {
        const diff = anchoredLeft ? (event.clientX - startX) : (startX - event.clientX);
        panel.style.width = `${clampPanelWidth(startW + diff)}px`;
        return;
      }

      const viewportHeight = Math.max(root.innerHeight || 0, 640);
      if (resizeMode === 'bottom-height') {
        const nextHeight = clampPanelHeight(startH + (event.clientY - startY));
        panel.style.height = `${Math.min(nextHeight, viewportHeight - startTop - 8)}px`;
        return;
      }

      if (resizeMode === 'top-height') {
        const bottom = startTop + startH;
        const rawHeight = startH - (event.clientY - startY);
        const nextHeight = clampPanelHeight(rawHeight);
        const nextTop = Math.max(0, bottom - nextHeight);
        panel.style.top = `${nextTop}px`;
        panel.style.height = `${Math.min(nextHeight, bottom - nextTop)}px`;
      }
    });

    root.document.addEventListener('mouseup', () => {
      if (!resizeMode) return;
      const patch = {
        panelWidth: clampPanelWidth(panel.offsetWidth),
        panelHeight: clampPanelHeight(panel.offsetHeight)
      };
      if (resizeMode === 'top-height') patch.panelY = Math.max(0, panel.getBoundingClientRect().top);
      resizeMode = null;
      panel.classList.remove('psim-resizing');
      engine.patchUI(patch);
    });
  }

  function injectStyles() {
    if (root.document.getElementById('psim-styles')) return;
    const style = root.document.createElement('style');
    style.id = 'psim-styles';
    style.textContent = `
      #polymarket-sim-panel { position: fixed; right: 12px; top: 60px; z-index: 2147483647; width: ${PANEL_WIDTH.DEFAULT}px; height: ${getDefaultPanelHeight()}px; overflow: hidden; display: flex; flex-direction: column; background: rgba(10, 10, 15, 0.97); color: #e2e8f0; border: 1px solid rgba(6, 182, 212, 0.15); border-radius: 10px; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); transition: box-shadow 140ms ease, opacity 140ms ease, transform 140ms ease; }
      #polymarket-sim-panel.psim-dark-off { background: rgba(247, 248, 252, 0.97); color: #1f2937; }
      #polymarket-sim-panel.psim-dragging { opacity: 0.94; box-shadow: 0 18px 44px rgba(0,0,0,0.58); transform: translateY(-1px); }
      #polymarket-sim-panel.psim-snap-settle { box-shadow: 0 12px 34px rgba(0,0,0,0.52); transform: translateY(0); }
      #polymarket-sim-panel.psim-resizing { box-shadow: 0 18px 44px rgba(0,0,0,0.58); }
      #psim-snap-guide { position: fixed; top: 10px; bottom: 10px; width: 1px; pointer-events: none; opacity: 0; background: rgba(34, 211, 238, 0.42); border-left: 1px dashed rgba(34, 211, 238, 0.58); transition: opacity 90ms ease; z-index: 2147483646; }
      #psim-snap-guide.visible { opacity: 1; }
      #polymarket-sim-panel.psim-collapsed { width: 58px; overflow: hidden; }
      #polymarket-sim-panel.psim-collapsed #psim-content { display: none; }
      #polymarket-sim-panel.psim-collapsed #psim-resize-handle, #polymarket-sim-panel.psim-collapsed #psim-resize-top-handle, #polymarket-sim-panel.psim-collapsed #psim-resize-bottom-handle { display: none; }
      #psim-resize-handle { position: absolute; top: 42px; right: 0; bottom: 10px; width: 10px; cursor: ew-resize; background: transparent; display: flex; align-items: center; justify-content: center; }
      #psim-resize-handle:hover { background: linear-gradient(90deg, transparent 0%, rgba(6,182,212,0.10) 100%); }
      #psim-resize-handle::after { content: '⋮'; font-size: 14px; color: #334155; line-height: 1; }
      #psim-resize-top-handle, #psim-resize-bottom-handle { position: absolute; left: 10px; right: 10px; height: 10px; cursor: ns-resize; background: transparent; display: flex; align-items: center; justify-content: center; z-index: 2; }
      #psim-resize-top-handle { top: 38px; }
      #psim-resize-bottom-handle { bottom: 0; }
      #psim-resize-top-handle::after, #psim-resize-bottom-handle::after { content: ''; width: 38px; height: 2px; border-radius: 999px; background: rgba(100,116,139,0.6); }
      #psim-resize-top-handle:hover::after, #psim-resize-bottom-handle:hover::after { background: rgba(34,211,238,0.78); }
      #psim-content::-webkit-scrollbar { width: 5px; }
      #psim-content::-webkit-scrollbar-thumb { background: rgba(6,182,212,0.25); border-radius: 3px; }
      #psim-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; background: rgba(15, 15, 25, 0.98); border-radius: 10px 10px 0 0; border-bottom: 1px solid rgba(6, 182, 212, 0.1); }
      #psim-title { font-weight: 700; font-size: 15px; color: #06b6d4; letter-spacing: 1px; }
      #psim-header-btns { display: flex; gap: 4px; }
      #psim-header-btns button { background: rgba(30,30,50,0.8); border: 1px solid rgba(6,182,212,0.2); color: #94a3b8; border-radius: 6px; width: 24px; height: 24px; cursor: pointer; font-size: 12px; }
      #psim-header-btns button:hover { color: #e2e8f0; background: rgba(6,182,212,0.06); border-color: rgba(6,182,212,0.4); }
      #psim-content { padding: 8px 12px 12px 8px; flex: 1; min-height: 0; overflow: auto; }
      .psim-section { margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid rgba(6,182,212,0.06); }
      .psim-section:last-child { border-bottom: none; }
      .psim-section-title { font-size: 11px; font-weight: 600; color: #06b6d4; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 8px; }
      .psim-subtle { color: #64748b; font-size: 11px; margin-top: 6px; }
      .psim-main-tabs { display: flex; gap: 4px; flex-wrap: wrap; padding: 2px 0 8px; border-bottom: 1px solid rgba(6,182,212,0.06); margin-bottom: 8px; }
      .psim-main-tab { background: rgba(15,15,25,0.74); border: 1px solid rgba(6,182,212,0.12); color: #64748b; border-radius: 999px; padding: 4px 9px; font-size: 10px; letter-spacing: 0.3px; cursor: pointer; }
      .psim-main-tab.active { color: #67e8f9; border-color: rgba(34,211,238,0.35); background: rgba(6,182,212,0.12); }
      .psim-workspace { display: flex; flex-direction: column; gap: 8px; min-height: 0; }
      .psim-dash-strip { display: flex; gap: 0; }
      .psim-dash-item { flex: 1; padding: 8px 10px; background: transparent; border-radius: 0; }
      .psim-dash-item + .psim-dash-item { border-left: 1px solid rgba(6,182,212,0.08); }
      .psim-dash-item:nth-child(2) .psim-value { font-size: 16px; font-weight: 700; color: #e2e8f0; }
      .psim-label { display: block; font-size: 10px; color: #64748b; margin-bottom: 2px; text-transform: uppercase; letter-spacing: 0.5px; }
      .psim-value { font-size: 13px; font-weight: 500; color: #e2e8f0; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; }
      .psim-positive { color: #34d399 !important; }
      .psim-negative { color: #f87171 !important; }
      .psim-market-title, .psim-watch-title, .psim-search-title, .psim-outcome-name, .psim-pos-outcome, .psim-h-market { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      .psim-pos-outcome { color: #06b6d4; text-decoration: none; }
      .psim-pos-outcome:hover { color: #22d3ee; text-decoration: underline; }
      .psim-outcomes, .psim-watchlist, .psim-history-list, .psim-positions-list { display: flex; flex-direction: column; gap: 3px; }
      .psim-outcome-row, .psim-position-row, .psim-watch-item { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; background: transparent; border-radius: 6px; padding: 5px; }
      .psim-outcome-row:hover, .psim-position-row:hover { background: rgba(6,182,212,0.06); }
      .psim-watch-item:hover { background: rgba(6,182,212,0.06); border-left: 2px solid #06b6d4; }
      .psim-outcome-name { flex: 1; min-width: 0; color: #cbd5e1; font-size: 12px; }
      .psim-trade-board { display: flex; flex-direction: column; gap: 4px; }
      .psim-ticket-toolbar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 2px; padding: 3px 2px 6px; border-bottom: 1px solid rgba(6,182,212,0.08); }
      .psim-ticket-size-box { display: flex; align-items: center; gap: 6px; }
      .psim-ticket-size-label { font-size: 10px; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }
      .psim-order-qty { width: 84px; text-align: right; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; }
      .psim-size-presets { display: flex; gap: 4px; flex-wrap: wrap; }
      .psim-size-chip { background: rgba(15,15,25,0.8); border: 1px solid rgba(6,182,212,0.14); color: #94a3b8; border-radius: 999px; padding: 3px 9px; font-size: 11px; cursor: pointer; position: relative; z-index: 2; }
      .psim-size-chip.active { background: rgba(6,182,212,0.18); border-color: rgba(34,211,238,0.45); color: #67e8f9; }
      .psim-ticket-hint { margin-left: auto; font-size: 10px; color: #64748b; }
      .psim-trade-ticket { align-items: stretch; padding: 4px 6px; border: 1px solid rgba(6,182,212,0.07); background: rgba(8,12,18,0.16); }
      .psim-ticket-main { display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0; }
      .psim-ticket-head { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1 1 48%; max-width: 48%; }
      .psim-quote-group { display: flex; gap: 3px; flex-wrap: wrap; align-items: stretch; margin-left: auto; flex: 0 0 auto; }
      .psim-quote-stack { min-width: 72px; display: flex; flex-direction: column; align-items: flex-end; gap: 0; padding: 2px 4px; border-radius: 5px; background: rgba(15,15,25,0.42); border: 1px solid rgba(6,182,212,0.07); contain: content; }
      .psim-quote-yes { border-color: rgba(34,197,94,0.18); }
      .psim-quote-no { border-color: rgba(239,68,68,0.18); }
      .psim-quote-empty { opacity: 0.68; border-style: dashed; }
      .psim-quote-label { font-size: 9px; letter-spacing: 0.4px; color: #64748b; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; }
      .psim-quote-mid, .psim-quote-best { min-width: 72px; text-align: right; color: #f8fafc; font-weight: 800; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: 13px; display: block; font-variant-numeric: tabular-nums; letter-spacing: 0.1px; }
      .psim-quote-best { display: flex; justify-content: flex-end; align-items: baseline; gap: 3px; }
      .psim-quote-best-tag { font-size: 8px; font-weight: 600; color: #64748b; letter-spacing: 0.35px; }
      .psim-quote-best-value { display: inline-block; }
      .psim-quote-meta { min-width: 72px; font-size: 9px; color: #64748b; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; display: block; text-align: right; font-variant-numeric: tabular-nums; }
      .psim-quote-actions { display: flex; gap: 3px; margin-top: 2px; }
      .psim-quote-best, .psim-quote-meta { transition: color 120ms ease, background-color 120ms ease; border-radius: 4px; padding: 0 1px; }
      .psim-tick-up { color: #67e8f9 !important; background: rgba(34,197,94,0.08); }
      .psim-tick-down { color: #fda4af !important; background: rgba(239,68,68,0.08); }
      .psim-quote-best.psim-tick-up { background: rgba(34,197,94,0.12); }
      .psim-quote-best.psim-tick-down { background: rgba(239,68,68,0.12); }
      .psim-side-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 30px; height: 18px; padding: 0 6px; border-radius: 999px; font-size: 10px; font-weight: 700; letter-spacing: 0.6px; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; }
      .psim-side-compact { min-width: 24px; height: 16px; font-size: 9px; padding: 0 5px; }
      .psim-side-yes { color: #34d399; background: rgba(34,197,94,0.12); border: 1px solid rgba(34,197,94,0.22); }
      .psim-side-no { color: #fda4af; background: rgba(239,68,68,0.12); border: 1px solid rgba(239,68,68,0.22); }
      .psim-outcome-price { min-width: 48px; text-align: right; color: #06b6d4; font-weight: 500; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: 13px; display: block; }
      .psim-spread { font-size: 10px; color: #475569; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; display: block; text-align: right; margin-top: 2px; }
      .psim-no-price { font-size: 10px; color: #f87171; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; display: block; text-align: right; margin-top: 1px; }
      .psim-qty-input, .psim-setting-input, .psim-search-input, #psim-holdings-sort { background: rgba(15,15,25,0.8); border: 1px solid rgba(6,182,212,0.15); color: #e2e8f0; border-radius: 6px; padding: 4px 7px; font-size: 12px; }
      .psim-qty-input { width: 58px; text-align: center; }
      .psim-btn { background: rgba(30,30,50,0.7); border: 1px solid rgba(6,182,212,0.15); color: #94a3b8; border-radius: 6px; padding: 3px 8px; font-size: 11px; cursor: pointer; }
      .psim-btn:hover { color: #e2e8f0; background: rgba(6,182,212,0.06); border-color: rgba(6,182,212,0.3); }
      .psim-buy { background: rgba(34, 197, 94, 0.12); border-color: rgba(34, 197, 94, 0.25); color: #34d399; padding: 3px 8px; font-size: 11px; }
      .psim-buy-no { background: rgba(239, 68, 68, 0.12); border-color: rgba(239, 68, 68, 0.25); color: #fda4af; padding: 3px 8px; font-size: 11px; }
      .psim-sell { background: rgba(239, 68, 68, 0.12); border-color: rgba(239, 68, 68, 0.25); color: #f87171; padding: 3px 8px; font-size: 11px; }
      .psim-disabled { opacity: 0.45; cursor: not-allowed; border-style: dashed; }
      .psim-danger, .psim-alert-rm { background: rgba(180,40,40,0.2); border-color: rgba(220,60,60,0.2); color: #fca5a5; }
      .psim-close-position { background: rgba(245,158,11,0.12); border-color: rgba(245,158,11,0.24); color: #fbbf24; }
      .psim-close-all { width: 100%; margin-top: 4px; background: rgba(127,29,29,0.28); border-color: rgba(248,113,113,0.3); color: #fecaca; }
      .psim-close-all:hover { background: rgba(153,27,27,0.36); border-color: rgba(248,113,113,0.45); }
      .psim-empty { color: #475569; font-size: 12px; padding: 8px; text-align: center; }
      .psim-search-row, .psim-pos-actions, .psim-search-actions, .psim-history-btns, .psim-btn-row { display: flex; gap: 5px; align-items: center; }
      .psim-trade-actions { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; margin-left: auto; }
      .psim-search-input { flex: 1; }
      .psim-collapsible-header { display: flex; justify-content: space-between; align-items: center; padding: 5px 4px; cursor: pointer; user-select: none; font-size: 12px; font-weight: 600; color: #64748b; }
      .psim-collapsible-header:hover { color: #06b6d4; }
      .psim-chevron { font-size: 10px; color: #475569; }
      .psim-pos-header { display: flex; justify-content: space-between; width: 100%; }
      .psim-pos-title-wrap { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; }
      .psim-pos-details { width: 100%; font-size: 11px; color: #64748b; font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Monaco, monospace; }
      .psim-pos-subdetails { color: #475569; }
      .psim-pos-actions { width: 100%; justify-content: flex-end; padding-top: 2px; border-top: 1px solid rgba(6,182,212,0.06); }
      .psim-pos-pnl { font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: 12px; }
      .psim-pos-pnl.psim-positive { background: rgba(52,211,153,0.08); padding: 2px 4px; border-radius: 3px; }
      .psim-pos-pnl.psim-negative { background: rgba(248,113,113,0.08); padding: 2px 4px; border-radius: 3px; }
      .psim-history-row { display: flex; flex-direction: column; gap: 3px; font-size: 11px; padding: 5px 6px; border-radius: 6px; border: 1px solid rgba(6,182,212,0.05); font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Monaco, monospace; }
      .psim-history-row:hover { background: rgba(6,182,212,0.04); }
      .psim-alerts-list { display: flex; flex-direction: column; gap: 4px; }
      .psim-alert-item { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid rgba(6,182,212,0.05); }
      .psim-alert-item:last-child { border-bottom: none; }
      .psim-alert-item-main { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
      .psim-alert-item-name { color: #cbd5e1; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
      .psim-alert-item-meta { color: #64748b; font-size: 10px; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; }
      .psim-h-main, .psim-h-meta { display: flex; align-items: center; gap: 6px; width: 100%; flex-wrap: wrap; }
      .psim-h-main { color: #cbd5e1; }
      .psim-h-time { color: #64748b; min-width: 46px; }
      .psim-h-action { color: #e2e8f0; min-width: 32px; }
      .psim-h-market { min-width: 0; flex: 1; }
      .psim-h-fill { color: #94a3b8; margin-left: auto; }
      .psim-h-meta { color: #64748b; padding-left: 84px; }
      .psim-h-net, .psim-h-fee { color: #94a3b8; }
      .psim-h-tail { color: #64748b; }
      .psim-setting-row { display: flex; justify-content: space-between; align-items: center; padding: 5px 0; font-size: 12px; color: #94a3b8; }
      .psim-setting-row label { color: #64748b; }
      .psim-api-ok { font-size: 10px; color: #22c55e; font-weight: 400; float: right; }
      .psim-api-error { font-size: 10px; color: #ef4444; font-weight: 400; float: right; cursor: help; }
      #psim-api-status { font-size: 10px; }
      .psim-market-title { font-size: 12px; color: #cbd5e1; margin-bottom: 6px; }
      .psim-watch-item { padding: 4px 6px; }
      .psim-alert-sm { font-size: 9px; padding: 1px 0; width: 20px; min-width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center; }
      .psim-watch-star { background: none; border: none; color: #64748b; cursor: pointer; font-size: 13px; padding: 0 2px; vertical-align: middle; }
      .psim-watch-star:hover { color: #f59e0b; }
      .psim-watch-title { color: #cbd5e1; text-decoration: none; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      .psim-watch-title:hover { color: #06b6d4; }
      .psim-watch-collapsed { opacity: 0.55; }
      .psim-watch-expand { background: none; border: none; color: #64748b; cursor: pointer; padding: 0 4px 0 0; font-size: 10px; }
      .psim-watch-expand:hover { color: #06b6d4; }
      .psim-alert-form { background: rgba(15,15,25,0.6); border: 1px solid rgba(6,182,212,0.12); border-radius: 8px; padding: 10px 12px; }
      .psim-alert-outcome { font-size: 12px; color: #cbd5e1; margin-bottom: 6px; font-weight: 500; }
      .psim-alert-observed { font-size: 11px; color: #64748b; margin-bottom: 8px; }
      .psim-alert-observed .psim-value { font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; color: #06b6d4; }
      .psim-alert-row { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
      .psim-alert-label { font-size: 10px; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; min-width: 52px; }
      .psim-alert-input { flex: 1; text-align: right; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; }
      .psim-alert-toggle-group { display: flex; gap: 3px; }
      .psim-toggle-btn { background: rgba(15,15,25,0.8); border: 1px solid rgba(6,182,212,0.15); color: #64748b; border-radius: 5px; padding: 3px 8px; font-size: 11px; cursor: pointer; transition: all 0.15s; }
      .psim-toggle-btn:hover { color: #94a3b8; border-color: rgba(6,182,212,0.3); }
      .psim-toggle-btn.active { background: rgba(6,182,212,0.15); border-color: rgba(6,182,212,0.4); color: #06b6d4; }
      .psim-binary-toggle { display: flex; gap: 6px; margin-bottom: 8px; }
      .psim-binary-toggle .psim-toggle-btn { flex: 1; text-align: center; font-size: 13px; padding: 5px 12px; font-weight: 600; }
      .psim-binary-toggle .psim-toggle-btn.active { background: rgba(6,182,212,0.2); border-color: #06b6d4; color: #06b6d4; }
      .psim-binary-info { font-size: 11px; color: #64748b; text-align: center; margin-top: 4px; }
      .psim-alert-btns { display: flex; gap: 6px; margin-top: 10px; }
      .psim-alert-btns .psim-btn { flex: 1; text-align: center; }
      #polymarket-sim-panel.psim-wide .psim-quote-group { flex-wrap: nowrap; width: auto; }
      #polymarket-sim-panel.psim-wide .psim-quote-stack { flex: 1; min-width: 0; }
      #polymarket-sim-panel.psim-wide .psim-ticket-toolbar { flex-wrap: nowrap; }
      #polymarket-sim-panel.psim-narrow .psim-ticket-main { flex-wrap: wrap; }
      #polymarket-sim-panel.psim-wide .psim-ticket-head { max-width: 54%; }
      #polymarket-sim-panel.psim-narrow .psim-ticket-head { max-width: 100%; flex-basis: 100%; }
      #polymarket-sim-panel.psim-narrow .psim-quote-group { margin-left: 0; width: 100%; }
      #polymarket-sim-panel.psim-narrow .psim-h-fill { margin-left: 0; width: 100%; }
      #polymarket-sim-panel.psim-narrow .psim-h-meta { padding-left: 0; }
      #polymarket-sim-panel.psim-narrow .psim-h-fee { display: none; }
      #polymarket-sim-panel.psim-narrow .psim-h-tail { width: 100%; }
    `;
    root.document.head.appendChild(style);
  }

  function refreshData() {
    const state = getState();
    const slug = getPageSlug();
    if (slug !== currentPageSlug) {
      currentPageSlug = slug;
      if (slug) {
        getMarketBySlug(slug, (_err, market) => {
          if (market) updateCurrentMarket();
        });
      } else {
        updateCurrentMarket();
      }
    } else if (slug && (!marketCache[slug] || Date.now() - marketCache[slug].ts > MARKET_CACHE_MS)) {
      getMarketBySlug(slug, (_err, market) => {
        if (market) updateCurrentMarket();
      });
    }

    const tokenIds = [...new Set(state.positions.map((position) => position.tokenId).concat(state.alerts.filter((alert) => alert.active).map((alert) => alert.tokenId)))];
    let pending = tokenIds.length;
    if (pending === 0) { schedulePortfolioPriceUpdate(); scheduleCurrentMarketPriceUpdate(); return; }
    tokenIds.forEach((tokenId) => {
      getClobPricePair(tokenId, (_err, pair) => {
        const buyPrice = pair.buyPrice;
        const sellPrice = pair.sellPrice;
        if (Number.isFinite(sellPrice) && sellPrice > 0) engine.updatePositionPrice(tokenId, sellPrice, 'SELL');
        const triggered = engine.evaluateAlerts((alert) => alert.tokenId === tokenId ? { BUY: buyPrice, SELL: sellPrice, MID: pair.midPrice } : null);
        if (triggered.length > 0) triggerNotifications(triggered);
        pending -= 1;
        if (pending === 0) { schedulePortfolioPriceUpdate(); scheduleCurrentMarketPriceUpdate(); }
      });
    });

  }

  function startRefresh() {
    if (refreshTimer) clearInterval(refreshTimer);
    refreshTimer = root.setInterval(refreshData, Math.max(2, getState().settings.refreshSeconds) * 1000);
  }

  function init() {
    root.console.info('[PolySim] Initializing...');
    try {
      injectStyles();
      root.console.info('[PolySim] Styles injected');
    } catch (e) {
      root.console.error('[PolySim] injectStyles failed:', e);
    }
    try {
      createPanel();
      root.console.info('[PolySim] Panel created, panelEl:', !!panelEl);
    } catch (e) {
      root.console.error('[PolySim] createPanel failed:', e);
    }
    const slug = getPageSlug();
    currentPageSlug = slug;
    root.console.info('[PolySim] Page slug:', slug);
    render();
    if (slug) {
      getMarketBySlug(slug, (err, market) => {
        root.console.info('[PolySim] Market loaded:', !!market, 'err:', err && err.message);
        if (!err && market) updateCurrentMarket();
      });
    }
    startRefresh();
    if (getState().watchlist.length > 0) updateWatchlist();
    root.console.info('[PolySim] Init complete');
  }

  if (root.document.readyState === 'complete') init();
  else root.window.addEventListener('load', init);
})(typeof globalThis !== 'undefined' ? globalThis : window);