Paper

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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);