// 個股技術分析 Stock Detail
const { useState, useEffect, useMemo, useRef } = React;

// ============ Fundamental fallback (eps/roe/peers — not from API) ============
const FUNDAMENTAL_FALLBACK = {
  '2330': {pe:24.6,pb:5.8,dy:2.1,eps:44.12,roe:26.4,rev_yoy:18.5,eps_yoy:24.2,peers:['2454','2379','3034','5347']},
  '2317': {pe:14.8,pb:1.6,dy:3.6,eps:12.32,roe:11.4,rev_yoy:8.6, eps_yoy:15.4,peers:['2382','3231','2376','6669']},
  '2454': {pe:21.4,pb:4.2,dy:3.8,eps:65.18,roe:24.6,rev_yoy:22.4,eps_yoy:28.6,peers:['2330','2379','3034','3035']},
  '6669': {pe:32.5,pb:8.4,dy:1.8,eps:82.45,roe:28.2,rev_yoy:85.4,eps_yoy:124.6,peers:['2382','2376','3231','2317']},
  '2382': {pe:18.4,pb:3.2,dy:2.4,eps:15.48,roe:22.4,rev_yoy:42.5,eps_yoy:56.8,peers:['6669','2376','3231','2317']},
  '3017': {pe:28.2,pb:6.8,dy:1.2,eps:41.86,roe:32.4,rev_yoy:95.6,eps_yoy:148.4,peers:['3324','3653','2421']},
  '3324': {pe:25.6,pb:5.4,dy:1.5,eps:36.12,roe:28.6,rev_yoy:86.2,eps_yoy:124.8,peers:['3017','3653','2421']},
  '2049': {pe:30.8,pb:4.6,dy:1.8,eps:10.15,roe:16.4,rev_yoy:12.4,eps_yoy:18.2,peers:['1504','1503','4576']},
  '1519': {pe:26.2,pb:4.8,dy:1.6,eps:15.74,roe:22.8,rev_yoy:35.6,eps_yoy:52.4,peers:['1503','1513','1514']},
  '3711': {pe:15.8,pb:2.4,dy:2.2,eps:11.24,roe:14.8,rev_yoy:6.2, eps_yoy:14.5,peers:['2449','6239']},
  '2603': {pe:6.8, pb:1.2,dy:8.4,eps:36.42,roe:18.6,rev_yoy:-12.4,eps_yoy:-32.4,peers:['2609','2615']},
  '2881': {pe:8.4, pb:1.1,dy:5.8,eps:11.32,roe:13.2,rev_yoy:14.5,eps_yoy:22.4,peers:['2882','2891','2884']},
  '3037': {pe:24.8,pb:4.2,dy:1.9,eps:6.78, roe:18.6,rev_yoy:28.4,eps_yoy:42.5,peers:['8046','6213']},
  '2308': {pe:22.4,pb:4.8,dy:2.8,eps:18.42,roe:21.6,rev_yoy:15.6,eps_yoy:24.8,peers:['1503','1519']},
  '2379': {pe:18.6,pb:3.6,dy:4.2,eps:28.14,roe:19.4,rev_yoy:12.4,eps_yoy:18.6,peers:['2454','3034','5274']},
};
function getFundamental(code) {
  return FUNDAMENTAL_FALLBACK[code] || {pe:20,pb:3,dy:3,eps:10,roe:15,rev_yoy:0,eps_yoy:0,peers:[]};
}

// 證券分類：list API 混含 ETF/ETN/特別股/TDR，搜尋需區分排序
function classifySecurity(code) {
  if (/^[1-9]\d{3}$/.test(code))       return 'stock'; // 個股（4碼非0開頭）
  if (/^[1-9]\d{3}[A-Z]$/.test(code))  return 'pref';  // 特別股（甲特/乙特）
  if (/^91\d{4}$/.test(code))          return 'tdr';   // 存託憑證
  if (/^0/.test(code))                 return 'etf';   // ETF/ETN/受益證券（0開頭）
  return 'other';
}
const SEC_KIND_LABEL = { pref: '特別股', tdr: 'TDR', etf: 'ETF', other: '其他' };
const SEC_KIND_RANK  = { stock: 0, pref: 1, tdr: 2, etf: 3, other: 4 };

// Chip fallback (seed-based, used when API unavailable)
function genChips(code) {
  const seed = code.charCodeAt(0) + code.charCodeAt(1);
  const out = [];
  for (let i = 4; i >= 0; i--) {
    const d = new Date(); d.setDate(d.getDate() - i);
    out.push({
      date: d.toISOString().slice(5,10),
      foreign: Math.round(Math.sin(seed + i*1.3) * 8000 + Math.cos(i*0.6) * 3000),
      trust:   Math.round(Math.sin(seed*1.4 + i*0.9) * 1500 + 400),
      dealer:  Math.round(Math.cos(seed*0.8 + i*1.2) * 800),
    });
  }
  return out;
}

// quarterly EPS history (8 quarters)
function genEPSHistory(code) {
  const f = getFundamental(code);
  const out = [];
  const today = new Date();
  let q = today.getMonth() < 3 ? 4 : today.getMonth() < 6 ? 1 : today.getMonth() < 9 ? 2 : 3;
  let y = today.getFullYear(); if (q === 4) y--;
  for (let i = 7; i >= 0; i--) {
    const cur = q - (i % 4); const yr = y - Math.floor(i/4) - (cur <= 0 ? 1 : 0);
    const qq = cur <= 0 ? cur + 4 : cur;
    const seed = code.charCodeAt(0) + i;
    const trend = 1 + (7 - i) * 0.04;
    const noise = Math.sin(seed * 1.7) * 0.15;
    out.push({ q:`${yr.toString().slice(2)}Q${qq}`, eps:+(f.eps/4*trend*(1+noise)).toFixed(2) });
  }
  return out;
}

// Parse FinMind chipboard response → [{date,foreign,trust,dealer}]
function parseChipboard(data, days) {
  if (!days) days = 10;
  var byDate = {};
  (data || []).forEach(function(row) {
    var d = row.date || '';
    if (!byDate[d]) byDate[d] = { date: d, foreign: 0, trust: 0, dealer: 0 };
    var net = (+(row.buy) || 0) - (+(row.sell) || 0);
    var n = (row.name || '').replace(/\s/g,'');
    // ⚠ FinMind 回傳英文名稱（Foreign_Investor/Investment_Trust/Dealer_self…），
    //   原本只比對中文導致全歸零→退回 genChips 假資料（2 開頭代碼種子相同→每檔長一樣）。
    //   分類規則與 fundamental.jsx:482 逐字對齊，確保個股頁與企業基本面一致。
    if (n === 'Foreign_Investor' || n === 'Foreign_Dealer_Self' || /外資|陸資/.test(n)) {
      byDate[d].foreign += net;
    } else if (n === 'Investment_Trust' || /投信/.test(n)) {
      byDate[d].trust += net;
    } else if (n === 'Dealer_self' || n === 'Dealer_Hedging' || /自營/.test(n)) {
      byDate[d].dealer += net;
    }
  });
  return Object.values(byDate)
    .sort(function(a, b) { return a.date < b.date ? -1 : 1; })
    .slice(-days)
    .map(function(d) {
      return {
        date: d.date.slice(5),
        foreign: Math.round(d.foreign / 1000),
        trust:   Math.round(d.trust   / 1000),
        dealer:  Math.round(d.dealer  / 1000),
      };
    });
}

// ============ Chip flow bar chart ============
function ChipBarChart({ chips, width = 920, height = 200 }) {
  const P_L = 50, P_R = 24, P_T = 22, P_B = 28;
  const [hover, setHover] = React.useState(null);
  const all = chips.flatMap(c => [c.foreign, c.trust, c.dealer]);
  const max = Math.max(...all, 0);
  const min = Math.min(...all, 0);
  const range = max - min || 1;
  const groupW = (width - P_L - P_R) / chips.length;
  const barW = Math.min(34, groupW / 3.6);
  const yOf = v => P_T + (1 - (v - min) / range) * (height - P_T - P_B);
  const y0 = yOf(0);
  // 三法人各自的金屬幣色（買賣方向由零軸上下疊表達）
  const METAL = {
    foreign: { grad:'url(#ccInk)',  edge:'#0c1322', face:'#9aa8c2', lbl:'外資' },
    trust:   { grad:'url(#ccGold)', edge:'#6e5320', face:'#f2e2af', lbl:'投信' },
    dealer:  { grad:'url(#ccBlue)', edge:'#23405c', face:'#c2d6e8', lbl:'自營' },
  };
  // 一疊硬幣：正值由零軸向上疊，負值向下疊；端面亮色+內圈鑄紋
  const coinStack = (v, cx, m, key) => {
    const rx = barW / 2;
    const ry = Math.min(4, barW * 0.16);
    const gap = ry * 1.55;
    const hPx = Math.abs(y0 - yOf(v));
    const n = Math.max(1, Math.round(hPx / gap));
    const dir = v >= 0 ? -1 : 1;            // 上疊 / 下疊
    const baseY = y0 + dir * ry;
    const endY = baseY + dir * (n - 1) * gap;
    const coins = [];
    for (let k = 0; k < n; k++) {
      coins.push(
        <ellipse key={k} cx={cx} cy={baseY + dir * k * gap} rx={rx} ry={ry}
                 fill={m.grad} stroke={m.edge} strokeWidth=".5"/>
      );
    }
    return (
      <g key={key} opacity={v < 0 ? .88 : 1}>
        {coins}
        <ellipse cx={cx} cy={endY} rx={rx} ry={ry}
                 fill={m.face} stroke={m.edge} strokeWidth=".65"/>
        <ellipse cx={cx} cy={endY} rx={rx * 0.62} ry={ry * 0.62}
                 fill="none" stroke={m.edge} strokeWidth=".45" opacity=".55"/>
      </g>
    );
  };
  return (
    <svg viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none"
         style={{ width:'100%', height:'auto', fontFamily:'Times New Roman' }}
         onMouseLeave={() => setHover(null)}>
      <defs>
        <linearGradient id="ccInk" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%"  stopColor="#0e1626"/>
          <stop offset="22%" stopColor="#3a4a66"/>
          <stop offset="50%" stopColor="#56688a"/>
          <stop offset="78%" stopColor="#2c3a54"/>
          <stop offset="100%" stopColor="#0e1626"/>
        </linearGradient>
        <linearGradient id="ccGold" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%"  stopColor="#7a5c22"/>
          <stop offset="22%" stopColor="#d4b370"/>
          <stop offset="50%" stopColor="#efd79b"/>
          <stop offset="78%" stopColor="#c79d5e"/>
          <stop offset="100%" stopColor="#7a5c22"/>
        </linearGradient>
        <linearGradient id="ccBlue" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%"  stopColor="#1d3850"/>
          <stop offset="22%" stopColor="#5b85a8"/>
          <stop offset="50%" stopColor="#8fb4d0"/>
          <stop offset="78%" stopColor="#4a749a"/>
          <stop offset="100%" stopColor="#1d3850"/>
        </linearGradient>
      </defs>
      {[0,1,2,3,4].map(i => {
        const v = min + (range * i)/4; const y = yOf(v);
        return (
          <g key={i}>
            <line x1={P_L} x2={width-P_R} y1={y} y2={y}
                  stroke={v === 0 ? 'var(--paper-line)' : 'var(--paper-line)'}
                  strokeWidth={v === 0 ? '.8' : '.6'}
                  strokeDasharray={v === 0 ? null : '2 4'}/>
            <text x={P_L - 6} y={y + 3} textAnchor="end" fontSize="10" fill="var(--ink-mute)" fontStyle="italic">
              {v >= 0 ? '+' : ''}{(v/1000).toFixed(1)}K
            </text>
          </g>
        );
      })}
      {/* 零軸桌面線：硬幣立基 */}
      <line x1={P_L} x2={width-P_R} y1={y0} y2={y0} stroke="var(--ink)" strokeWidth="1"/>
      {chips.map((c, i) => {
        const gx = P_L + groupW * i + groupW * 0.5;
        const series = [
          { v: c.foreign, x: gx - barW * 1.25, m: METAL.foreign },
          { v: c.trust,   x: gx,               m: METAL.trust },
          { v: c.dealer,  x: gx + barW * 1.25, m: METAL.dealer },
        ];
        const isHover = hover === i;
        return (
          <g key={i} onMouseEnter={() => setHover(i)} style={{ cursor:'default' }}
             opacity={hover == null || isHover ? 1 : 0.62}>
            {/* 透明熱區：整組好 hover */}
            <rect x={gx - groupW/2} y={P_T} width={groupW} height={height - P_T - P_B} fill="transparent"/>
            {series.map((s, j) => coinStack(s.v, s.x, s.m, j))}
            <text x={gx} y={height - 10} textAnchor="middle" fontSize="11"
                  fill="var(--ink-mute)" fontStyle="italic">
              {c.date}
            </text>
            {isHover && (
              <g pointerEvents="none">
                <rect x={Math.max(P_L, Math.min(gx - 62, width - P_R - 124))} y={P_T + 2}
                      width={124} height={46} fill="var(--ink)" rx="2" opacity=".94"/>
                {series.map((s, j) => (
                  <text key={j} x={Math.max(P_L, Math.min(gx - 62, width - P_R - 124)) + 10}
                        y={P_T + 16 + j * 13} fontSize="10" fill="#fff">
                    {s.m.lbl} {s.v >= 0 ? '+' : ''}{(s.v/1000).toFixed(1)}K張
                  </text>
                ))}
              </g>
            )}
          </g>
        );
      })}
    </svg>
  );
}

// ============ EPS quarterly bar chart ============
function EPSBarChart({ eps, width = 460, height = 170 }) {
  const P_L = 36, P_R = 16, P_T = 28, P_B = 28;
  const max = Math.max(...eps.map(d => d.eps));
  const min = Math.min(0, Math.min(...eps.map(d => d.eps)));
  const range = max - min || 1;
  const stepX = (width - P_L - P_R) / eps.length;
  const yOf = v => P_T + (1 - (v - min) / range) * (height - P_T - P_B);
  return (
    <svg viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none"
         style={{ width:'100%', height:'auto', fontFamily:'Times New Roman' }}>
      {[0,1,2].map(i => {
        const v = min + (range * i) / 2; const y = yOf(v);
        return (
          <g key={i}>
            <line x1={P_L} x2={width-P_R} y1={y} y2={y}
                  stroke="var(--paper-line)" strokeWidth=".6" strokeDasharray="2 4"/>
            <text x={P_L - 4} y={y+3} textAnchor="end" fontSize="9" fill="var(--ink-mute)" fontStyle="italic">
              {v.toFixed(1)}
            </text>
          </g>
        );
      })}
      {eps.map((d, i) => {
        const x = P_L + stepX * (i + 0.5);
        const y = yOf(d.eps);
        const y0 = yOf(0);
        const isLast = i === eps.length - 1;
        // 柱色：年增綠 / 年減紅 / 無YoY資料用墨色；最新一季維持金色
        const barFill = isLast ? 'var(--gold-deep)'
          : d.yoy != null ? (d.yoy >= 0 ? 'var(--bull)' : 'var(--bear)')
          : 'var(--ink)';
        return (
          <g key={i}>
            <rect x={x - stepX*0.35} y={Math.min(y0, y)}
                  width={stepX*0.7} height={Math.max(1, Math.abs(y0 - y))}
                  fill={barFill} opacity={isLast ? '1' : '.72'}/>
            {d.prev != null && (
              <line x1={x - stepX*0.42} x2={x + stepX*0.42}
                    y1={yOf(d.prev)} y2={yOf(d.prev)}
                    stroke="var(--gold-deep)" strokeWidth="1.2" strokeDasharray="3 2" opacity=".85"/>
            )}
            <text x={x} y={y - (d.yoy != null ? 14 : 4)} textAnchor="middle" fontSize="9"
                  fill="var(--ink)" fontWeight="700">{d.eps}</text>
            {d.yoy != null && (
              <text x={x} y={y - 4} textAnchor="middle" fontSize="8" fontStyle="italic"
                    fill={d.yoy >= 0 ? 'var(--bull)' : 'var(--bear)'}>
                {d.yoy >= 0 ? '+' : ''}{Math.abs(d.yoy) >= 100 ? Math.round(d.yoy) : d.yoy.toFixed(0)}%
              </text>
            )}
            <text x={x} y={height - 10} textAnchor="middle" fontSize="9"
                  fill="var(--ink-mute)" fontStyle="italic">{d.q}</text>
          </g>
        );
      })}
    </svg>
  );
}

// ============ Mock K-line generator ============
function generateKLines(base = 1000, days = 120, drift = 0.002, vol = 0.025) {
  const out = [];
  let price = base;
  const today = new Date();
  for (let i = days - 1; i >= 0; i--) {
    const d = new Date(today); d.setDate(d.getDate() - i);
    const drift_i = price * drift;
    const noise = price * vol * (Math.sin(i*0.31) * 0.7 + (Math.random() - 0.5) * 1.2);
    const open = price;
    const close = price + drift_i + noise;
    const high = Math.max(open, close) + Math.abs(noise) * 0.5;
    const low  = Math.min(open, close) - Math.abs(noise) * 0.5;
    const volume = (Math.abs(noise) * 1000 + 20000) | 0;
    out.push({
      date: d.toISOString().slice(5,10),
      fullDate: d.toISOString().slice(0,10),
      open: +open.toFixed(2),
      close: +close.toFixed(2),
      high: +high.toFixed(2),
      low: +low.toFixed(2),
      volume,
    });
    price = close;
  }
  return out;
}

// ============ Indicators ============
function sma(data, n) {
  const out = [];
  for (let i = 0; i < data.length; i++) {
    if (i < n - 1) { out.push(null); continue; }
    let s = 0; for (let k = 0; k < n; k++) s += data[i-k].close;
    out.push(s / n);
  }
  return out;
}
function ema(arr, n) {
  const k = 2 / (n + 1);
  const out = [];
  let prev = arr[0];
  for (let i = 0; i < arr.length; i++) {
    if (i === 0) { out.push(arr[i]); prev = arr[i]; continue; }
    const v = arr[i] * k + prev * (1 - k);
    out.push(v); prev = v;
  }
  return out;
}
function rsi(data, n = 14) {
  const out = []; let gainSum = 0, lossSum = 0;
  for (let i = 0; i < data.length; i++) {
    if (i === 0) { out.push(null); continue; }
    const diff = data[i].close - data[i-1].close;
    const g = Math.max(diff, 0), l = Math.max(-diff, 0);
    if (i < n) { gainSum += g; lossSum += l; out.push(null); continue; }
    if (i === n) { gainSum = (gainSum + g)/n; lossSum = (lossSum + l)/n; }
    else { gainSum = (gainSum*(n-1) + g)/n; lossSum = (lossSum*(n-1) + l)/n; }
    const rs = lossSum === 0 ? 100 : gainSum / lossSum;
    out.push(100 - 100/(1+rs));
  }
  return out;
}
function macd(data) {
  const closes = data.map(d => d.close);
  const e12 = ema(closes, 12);
  const e26 = ema(closes, 26);
  const dif = e12.map((v, i) => v - e26[i]);
  const dea = ema(dif, 9);
  const hist = dif.map((v, i) => (v - dea[i]) * 2);
  return { dif, dea, hist };
}

// Bollinger Bands: 20-day SMA ± k * stddev
function bollinger(data, n = 20, k = 2) {
  const closes = data.map(d => d.close);
  const upper = [], lower = [], mid = [];
  for (let i = 0; i < closes.length; i++) {
    if (i < n - 1) { upper.push(null); lower.push(null); mid.push(null); continue; }
    const slice = closes.slice(i - n + 1, i + 1);
    const mean = slice.reduce((a, b) => a + b, 0) / n;
    const std = Math.sqrt(slice.reduce((a, b) => a + (b - mean) ** 2, 0) / n);
    mid.push(mean);
    upper.push(mean + k * std);
    lower.push(mean - k * std);
  }
  return { upper, lower, mid };
}

// ATR: Average True Range (14-day)
function atr(data, n = 14) {
  const trs = data.map((d, i) => {
    if (i === 0) return d.high - d.low;
    const prevClose = data[i - 1].close;
    return Math.max(d.high - d.low, Math.abs(d.high - prevClose), Math.abs(d.low - prevClose));
  });
  // Wilder's smoothing
  const out = new Array(data.length).fill(null);
  let sum = trs.slice(0, n).reduce((a, b) => a + b, 0);
  out[n - 1] = sum / n;
  for (let i = n; i < trs.length; i++) {
    out[i] = (out[i - 1] * (n - 1) + trs[i]) / n;
  }
  return out;
}

// Pivot Support / Resistance from recent highs and lows
function pivotLevels(data, lookback = 60) {
  const slice = data.slice(-lookback);
  const high = Math.max(...slice.map(d => d.high));
  const low  = Math.min(...slice.map(d => d.low));
  const close = data[data.length - 1].close;
  // Classic pivot
  const pivot = (high + low + close) / 3;
  return {
    support:    +(2 * pivot - high).toFixed(2),
    resistance: +(2 * pivot - low).toFixed(2),
    pivot:      +pivot.toFixed(2),
    high60:     +high.toFixed(2),
    low60:      +low.toFixed(2),
  };
}

// RSI Divergence: check last 3 swing highs/lows for price vs RSI divergence
function detectDivergence(data, rsiArr) {
  const n = data.length;
  if (n < 20) return null;
  const last = data[n - 1], prev = data[n - 5];
  const lastRsi = rsiArr[n - 1], prevRsi = rsiArr[n - 5];
  if (!lastRsi || !prevRsi) return null;
  // Bearish divergence: price higher, RSI lower
  if (last.close > prev.close && lastRsi < prevRsi && lastRsi > 60) return 'bearish';
  // Bullish divergence: price lower, RSI higher
  if (last.close < prev.close && lastRsi > prevRsi && lastRsi < 45) return 'bullish';
  return null;
}

// Chip consecutive direction: count how many days all three are same direction
function chipStreak(chips) {
  if (!chips || chips.length < 2) return { dir: 0, days: 0 };
  let dir = 0, days = 0;
  for (let i = chips.length - 1; i >= 0; i--) {
    const total = chips[i].foreign + chips[i].trust + chips[i].dealer;
    const d = total > 0 ? 1 : total < 0 ? -1 : 0;
    if (d === 0) break;
    if (dir === 0) dir = d;
    else if (d !== dir) break;
    days++;
  }
  return { dir, days };
}

// ============ Stock list (search) ============
const STOCK_LIST = [
  { code: '2330', name: '台積電',     base: 1085, drift: 0.0025, vol: 0.02 },
  { code: '2317', name: '鴻海',       base: 178,  drift: 0.0028, vol: 0.025 },
  { code: '2454', name: '聯發科',     base: 1380, drift: 0.0020, vol: 0.022 },
  { code: '6669', name: '緯穎',       base: 2580, drift: 0.0035, vol: 0.03 },
  { code: '2382', name: '廣達',       base: 280,  drift: 0.0030, vol: 0.026 },
  { code: '3017', name: '奇鋐',       base: 1140, drift: 0.0040, vol: 0.034 },
  { code: '3324', name: '雙鴻',       base: 905,  drift: 0.0038, vol: 0.032 },
  { code: '2049', name: '上銀',       base: 305,  drift: 0.0025, vol: 0.028 },
  { code: '1519', name: '華城',       base: 405,  drift: 0.0030, vol: 0.030 },
  { code: '3711', name: '日月光投控', base: 175,  drift: 0.0018, vol: 0.022 },
  { code: '2603', name: '長榮',       base: 252,  drift: 0.0015, vol: 0.026 },
  { code: '2881', name: '富邦金',     base: 94,   drift: 0.0012, vol: 0.014 },
  { code: '3037', name: '欣興',       base: 165,  drift: 0.0028, vol: 0.026 },
  { code: '2308', name: '台達電',     base: 408,  drift: 0.0024, vol: 0.022 },
  { code: '2379', name: '瑞昱',       base: 525,  drift: 0.0020, vol: 0.024 },
];

function getStock(code) {
  return STOCK_LIST.find(s => s.code === code) || STOCK_LIST[0];
}

// ============ K-line chart with crosshair ============
function KLineChart({ data, ma5, ma20, ma60, bb, width = 920, height = 360, onHover }) {
  const [hoverIdx, setHoverIdx] = useState(null);
  const svgRef = useRef(null);
  const P_L = 18, P_R = 60, P_T = 18, P_B = 24;
  const min = Math.min(...data.map(d => d.low));
  const max = Math.max(...data.map(d => d.high));
  const pad = (max - min) * 0.08;
  const yMin = min - pad, yMax = max + pad;
  const yRange = yMax - yMin || 1;
  const candleW = (width - P_L - P_R) / data.length * 0.7;
  const stepX = (width - P_L - P_R) / data.length;
  const yOf = v => P_T + (1 - (v - yMin) / yRange) * (height - P_T - P_B);
  // y-axis ticks
  const ticks = [];
  for (let i = 0; i <= 5; i++) {
    const v = yMin + (yRange * i) / 5;
    ticks.push({ v, y: yOf(v) });
  }
  const linePath = (arr) => arr.map((v, i) => {
    if (v == null) return null;
    const x = P_L + stepX * (i + 0.5);
    const y = yOf(v);
    return `${i === 0 || arr[i-1] == null ? 'M' : 'L'} ${x} ${y}`;
  }).filter(Boolean).join(' ');

  const last = data[data.length - 1];

  const handleMove = (e) => {
    const rect = svgRef.current.getBoundingClientRect();
    const x = ((e.clientX - rect.left) / rect.width) * width;
    const idx = Math.round((x - P_L) / stepX - 0.5);
    if (idx >= 0 && idx < data.length) {
      setHoverIdx(idx);
      onHover && onHover(data[idx], idx);
    } else {
      setHoverIdx(null);
      onHover && onHover(null);
    }
  };
  const handleLeave = () => { setHoverIdx(null); onHover && onHover(null); };

  const hover = hoverIdx != null ? data[hoverIdx] : null;
  const hoverX = hoverIdx != null ? P_L + stepX * (hoverIdx + 0.5) : null;

  return (
    <svg ref={svgRef} viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none"
         style={{ width:'100%', height: 'auto', fontFamily:'Times New Roman', cursor:'crosshair' }}
         onMouseMove={handleMove} onMouseLeave={handleLeave}>
      {/* gridlines */}
      {ticks.map((t, i) => (
        <g key={i}>
          <line x1={P_L} x2={width - P_R} y1={t.y} y2={t.y}
                stroke="var(--paper-line)" strokeWidth=".6" strokeDasharray="2 4" />
          <text x={width - P_R + 6} y={t.y + 3} fontSize="10" fill="var(--ink-mute)"
                fontStyle="italic">{t.v.toFixed(1)}</text>
        </g>
      ))}

      {/* Bollinger Bands */}
      {bb && (
        <>
          <path d={linePath(bb.upper)} fill="none" stroke="var(--gold)" strokeWidth=".7" strokeDasharray="4 3" opacity=".6"/>
          <path d={linePath(bb.lower)} fill="none" stroke="var(--gold)" strokeWidth=".7" strokeDasharray="4 3" opacity=".6"/>
          <path d={linePath(bb.mid)}   fill="none" stroke="var(--gold)" strokeWidth=".8" opacity=".35"/>
        </>
      )}
      {/* MA lines */}
      <path d={linePath(ma5)}  fill="none" stroke="#b8924d" strokeWidth="1.1" />
      <path d={linePath(ma20)} fill="none" stroke="#2c5e8e" strokeWidth="1.1" />
      <path d={linePath(ma60)} fill="none" stroke="#4a7a4e" strokeWidth="1.1" />

      {/* Candles */}
      {data.map((d, i) => {
        const x = P_L + stepX * (i + 0.5);
        const up = d.close >= d.open;
        const color = up ? 'var(--bull)' : 'var(--bear)';
        return (
          <g key={i}>
            <line x1={x} x2={x} y1={yOf(d.high)} y2={yOf(d.low)}
                  stroke={color} strokeWidth=".9" />
            <rect x={x - candleW/2}
                  y={yOf(Math.max(d.open, d.close))}
                  width={candleW}
                  height={Math.max(1, Math.abs(yOf(d.open) - yOf(d.close)))}
                  fill={up ? color : 'var(--paper-bright)'}
                  stroke={color} strokeWidth=".9" />
          </g>
        );
      })}

      {/* Crosshair */}
      {hover && (
        <g pointerEvents="none">
          <line x1={hoverX} x2={hoverX} y1={P_T} y2={height - P_B}
                stroke="var(--ink)" strokeWidth=".8" strokeDasharray="3 3" opacity=".6"/>
          <line x1={P_L} x2={width - P_R} y1={yOf(hover.close)} y2={yOf(hover.close)}
                stroke="var(--ink)" strokeWidth=".8" strokeDasharray="3 3" opacity=".6"/>
          {/* date label on x-axis */}
          <rect x={hoverX - 30} y={height - P_B + 2} width="60" height="16"
                fill="var(--ink)" />
          <text x={hoverX} y={height - P_B + 13} textAnchor="middle"
                fontSize="10" fill="var(--paper-bright)" fontFamily="Times New Roman">
            {hover.date}
          </text>
          {/* price label on y-axis */}
          <rect x={width - P_R} y={yOf(hover.close) - 9}
                width="46" height="18" fill="var(--ink)" />
          <text x={width - P_R + 23} y={yOf(hover.close) + 4}
                textAnchor="middle" fontSize="11" fill="var(--gold-leaf)" fontWeight="700">
            {hover.close.toFixed(2)}
          </text>
        </g>
      )}

      {/* Current price line (only when no hover) */}
      {!hover && (
        <>
          <line x1={P_L} x2={width - P_R} y1={yOf(last.close)} y2={yOf(last.close)}
                stroke="var(--gold)" strokeWidth=".8" strokeDasharray="3 3" />
          <rect x={width - P_R} y={yOf(last.close) - 9}
                width="46" height="18" fill="var(--gold)" stroke="var(--ink)" />
          <text x={width - P_R + 23} y={yOf(last.close) + 4}
                textAnchor="middle" fontSize="11" fill="var(--ink)" fontWeight="700">
            {last.close.toFixed(2)}
          </text>
        </>
      )}

      {/* x axis dates */}
      {[0, Math.floor(data.length*0.25), Math.floor(data.length*0.5), Math.floor(data.length*0.75), data.length-1].map(i => (
        <text key={i} x={P_L + stepX*(i + 0.5)} y={height - 6}
              textAnchor="middle" fontSize="10" fill="var(--ink-mute)" fontStyle="italic">
          {data[i].date}
        </text>
      ))}
    </svg>
  );
}

// ============ MACD chart ============
function MACDChart({ data, macd, width = 920, height = 140 }) {
  const P_L = 18, P_R = 60, P_T = 14, P_B = 18;
  const all = [...macd.dif, ...macd.dea, ...macd.hist].filter(v => v != null);
  const max = Math.max(...all), min = Math.min(...all);
  const range = (max - min) || 1;
  const stepX = (width - P_L - P_R) / data.length;
  const yOf = v => P_T + (1 - (v - min) / range) * (height - P_T - P_B);
  const linePath = (arr) => arr.map((v, i) => {
    if (v == null) return null;
    const x = P_L + stepX * (i + 0.5); const y = yOf(v);
    return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
  }).filter(Boolean).join(' ');
  return (
    <svg viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none"
         style={{ width:'100%', height: 'auto', fontFamily:'Times New Roman' }}>
      <line x1={P_L} x2={width - P_R} y1={yOf(0)} y2={yOf(0)} stroke="var(--paper-line)" strokeWidth=".6" />
      {macd.hist.map((v, i) => {
        if (v == null) return null;
        const x = P_L + stepX * (i + 0.5);
        const y0 = yOf(0); const y1 = yOf(v);
        return <rect key={i} x={x - stepX*0.3} y={Math.min(y0, y1)}
                     width={stepX*0.6} height={Math.abs(y1 - y0) || 1}
                     fill={v >= 0 ? 'var(--bull)' : 'var(--bear)'} opacity=".7"/>;
      })}
      <path d={linePath(macd.dif)} fill="none" stroke="#b8924d" strokeWidth="1.2" />
      <path d={linePath(macd.dea)} fill="none" stroke="#2c5e8e" strokeWidth="1.2" />
      <text x={P_L} y={12} fontSize="10" fontStyle="italic" fill="var(--ink-mute)">
        MACD(12,26,9)  DIF · DEA · OSC
      </text>
    </svg>
  );
}

// ============ RSI chart ============
function RSIChart({ data, rsi, width = 920, height = 100 }) {
  const P_L = 18, P_R = 60, P_T = 14, P_B = 14;
  const stepX = (width - P_L - P_R) / data.length;
  const yOf = v => P_T + (1 - (v / 100)) * (height - P_T - P_B);
  const linePath = rsi.map((v, i) => {
    if (v == null) return null;
    const x = P_L + stepX * (i + 0.5); const y = yOf(v);
    return `${i === 0 || rsi[i-1] == null ? 'M' : 'L'} ${x} ${y}`;
  }).filter(Boolean).join(' ');
  return (
    <svg viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none"
         style={{ width:'100%', height: 'auto', fontFamily:'Times New Roman' }}>
      <line x1={P_L} x2={width - P_R} y1={yOf(70)} y2={yOf(70)} stroke="var(--bear)" strokeWidth=".6" strokeDasharray="3 3" opacity=".5"/>
      <line x1={P_L} x2={width - P_R} y1={yOf(50)} y2={yOf(50)} stroke="var(--paper-line)" strokeWidth=".6"/>
      <line x1={P_L} x2={width - P_R} y1={yOf(30)} y2={yOf(30)} stroke="var(--bull)" strokeWidth=".6" strokeDasharray="3 3" opacity=".5"/>
      <path d={linePath} fill="none" stroke="var(--ink)" strokeWidth="1.2" />
      <text x={P_L} y={12} fontSize="10" fontStyle="italic" fill="var(--ink-mute)">RSI(14)</text>
      <text x={width - P_R + 4} y={yOf(70) + 3} fontSize="9" fill="var(--bear)" fontStyle="italic">70</text>
      <text x={width - P_R + 4} y={yOf(30) + 3} fontSize="9" fill="var(--bull)" fontStyle="italic">30</text>
    </svg>
  );
}

// ============ 100-Point Scoring Framework ============
// ─── helper: Altman Z' score (non-manufacturing) ───────────────────────────
function calcAltmanZ(fins, bs) {
  if (!fins || !fins.length || !bs || !bs.length) return null;
  var bsNow = bs[bs.length - 1];
  var ta = bsNow.TotalAssets, ca = bsNow.CurrentAssets, cl2 = bsNow.CurrentLiabilities;
  var tl = bsNow.TotalLiabilities, eq = bsNow.StockholdersEquity;
  if (!ta || ta <= 0) return null;
  var last4rev = fins.slice(-4).reduce(function(s,q){ return s+(q.Revenue||0); }, 0);
  var last4ni  = fins.slice(-4).reduce(function(s,q){ return s+(q.NetIncome||0); }, 0);
  var last4oi  = fins.slice(-4).reduce(function(s,q){ return s+(q.OperatingIncome||0); }, 0);
  var x1 = ca && cl2 ? (ca - cl2) / ta : 0;
  var x2 = eq ? eq / ta : 0;                   // 用帳面股東權益代理保留盈餘
  var x3 = last4oi ? last4oi / ta : 0;          // EBIT proxy = OperatingIncome
  var x4 = tl && tl > 0 ? eq / tl : 0;         // 帳面權益/總負債
  var x5 = last4rev ? last4rev / ta : 0;
  var z = 0.717*x1 + 0.847*x2 + 3.107*x3 + 0.420*x4 + 0.998*x5;
  return +z.toFixed(2);
}

// ─── helper: Piotroski 7 signals ───────────────────────────────────────────
function calcPiotroski(fins, bs, cf) {
  var signals = { p1:0, p2:0, p3:0, p4:0, p5:0, p6:0, p7:0 };
  if (!fins || fins.length < 4 || !bs || bs.length < 2) return signals;
  var bsNow  = bs[bs.length-1],  bsPrev = bs[bs.length-2];
  var taN = bsNow.TotalAssets||0, tAP = bsPrev.TotalAssets||0;
  var eqN = bsNow.StockholdersEquity||0;
  var caN = bsNow.CurrentAssets||0, clN = bsNow.CurrentLiabilities||0;
  var cAP = bsPrev.CurrentAssets||0, cLP = bsPrev.CurrentLiabilities||0;
  var niTTM = fins.slice(-4).reduce(function(s,q){ return s+(q.NetIncome||0);}, 0);
  var niPrev4 = fins.length >= 8 ? fins.slice(-8,-4).reduce(function(s,q){return s+(q.NetIncome||0);},0) : 0;
  var revN4 = fins.slice(-4).reduce(function(s,q){return s+(q.Revenue||0);},0);
  var revP4 = fins.length>=8 ? fins.slice(-8,-4).reduce(function(s,q){return s+(q.Revenue||0);},0):0;
  var gpN4  = fins.slice(-4).reduce(function(s,q){return s+(q.GrossProfit||0);},0);
  var gpP4  = fins.length>=8 ? fins.slice(-8,-4).reduce(function(s,q){return s+(q.GrossProfit||0);},0):0;
  var roaN = taN > 0 ? niTTM/taN : 0;
  var roaP = tAP > 0 ? niPrev4/tAP : 0;
  // P1: ROA > 0
  if (roaN > 0) signals.p1 = 1;
  // P2: ΔROA > 0
  if (roaN > roaP && tAP > 0) signals.p2 = 1;
  // P3: CFO > 0
  if (cf && cf.length) {
    var cfoTTM = cf.slice(-4).reduce(function(s,q){return s+(q.OperatingCF||0);},0);
    if (cfoTTM > 0) signals.p3 = 1;
    // P4: Accruals — CFO/TA > ROA (現金品質優於應計盈餘)
    if (taN > 0 && cfoTTM/taN > roaN) signals.p4 = 1;
  }
  // P5: ΔCurrentRatio > 0
  var crN = clN > 0 ? caN/clN : 0, crP = cLP > 0 ? cAP/cLP : 0;
  if (crN > crP && crP > 0) signals.p5 = 1;
  // P6: ΔGrossMargin > 0
  var gmN = revN4 > 0 ? gpN4/revN4 : 0, gmP = revP4 > 0 ? gpP4/revP4 : 0;
  if (gmN > gmP && revP4 > 0) signals.p6 = 1;
  // P7: ΔAssetTurnover > 0
  var atN = taN > 0 ? revN4/taN : 0, atP = tAP > 0 ? revP4/tAP : 0;
  if (atN > atP && atP > 0) signals.p7 = 1;
  return signals;
}

// ─── helper: Graham Number upside % ────────────────────────────────────────
function calcGrahamUpside(fins, fund, currentPrice) {
  if (!fins || !fins.length || !fund.pb || fund.pb <= 0 || !currentPrice) return null;
  var ttmEps = fins.slice(-4).reduce(function(s,q){return s+(q.EPS||0);},0);
  if (ttmEps <= 0) return null;
  var bps = currentPrice / fund.pb;
  var graham = Math.sqrt(22.5 * ttmEps * bps);
  return { graham: +graham.toFixed(0), upside: +((graham - currentPrice)/currentPrice*100).toFixed(1) };
}

// ─── helper: DCF / Lynch composite upside % ────────────────────────────────
function calcValuationUpside(fins, cf, fund, currentPrice) {
  if (!fins || fins.length < 8 || !currentPrice) return null;
  var epsRows = fins.map(function(q){return q.EPS;}).filter(function(v){return v!=null&&v>0;});
  if (epsRows.length < 4) return null;
  var ttmEps = epsRows.slice(-4).reduce(function(a,b){return a+b;},0);
  if (ttmEps <= 0) return null;
  var r4 = epsRows.slice(-4).reduce(function(a,b){return a+b;},0);
  var p4 = epsRows.length>=8 ? epsRows.slice(-8,-4).reduce(function(a,b){return a+b;},0) : 0;
  var epsG = p4 > 0 ? (r4-p4)/Math.abs(p4)*100 : 0;
  // Lynch: EPS × (growthRate + dividendYield)
  var lynchPe = Math.max(0, Math.min(epsG, 30)) + (fund.dy||0);
  var lynch = lynchPe > 3 ? ttmEps * lynchPe : null;
  // DCF 5-year
  var dcf = null;
  if (cf && cf.length >= 4) {
    var last4ni = fins.slice(-4).reduce(function(s,q){return s+(q.NetIncome||0);},0);
    var last4cf = cf.slice(-4).reduce(function(s,q){return s+(q.FreeCF||0);},0);
    if (last4cf > 0 && last4ni > 0) {
      var fcfPerShare = last4cf * ttmEps / last4ni;
      var g = Math.max(0.03, Math.min(epsG/100, 0.15));
      var disc = 0.10, tg = 0.03, pv = 0, fcf = fcfPerShare;
      for (var i=1;i<=5;i++){fcf*=(1+g);pv+=fcf/Math.pow(1+disc,i);}
      var tv = (fcf*(1+tg))/(disc-tg);
      pv += tv/Math.pow(1+disc,5);
      if (pv > 0) dcf = +pv.toFixed(0);
    }
  }
  var vals = [lynch,dcf].filter(function(v){return v!=null&&v>0;});
  if (!vals.length) return null;
  var consensus = vals.reduce(function(a,b){return a+b;},0)/vals.length;
  return { consensus: +consensus.toFixed(0), upside: +((consensus-currentPrice)/currentPrice*100).toFixed(1) };
}

// ═══ 五力評分 v3 ═══════════════════════════════════════════
// 框架來源：Minervini Trend Template（8準則 Stage 2 確認）+ IBD RS Rating
//（四季加權報酬、最近一季雙倍權重、對比大盤）+ Piotroski F-Score +
// Graham/DCF 估值 + 台股法人籌碼。風險旗標可封頂評級（分數高≠可以買）。
// 五力：趨勢25 / 相對動能20 / 籌碼15 / 品質20 / 價值成長20 = 100

// IBD 加權報酬：0.4×3月 + 0.2×6月 + 0.2×9月 + 0.2×12月（資料不足時權重重分配）
function calcWeightedReturn(closes) {
  var L = closes.length;
  if (L < 64) return null;
  function ret(n) { var p = closes[L - 1 - n]; return p > 0 ? (closes[L - 1] / p - 1) * 100 : null; }
  var parts = [[0.4, ret(63)], [0.2, L > 127 ? ret(126) : null], [0.2, L > 190 ? ret(189) : null], [0.2, L > 251 ? ret(251) : null]]
    .filter(function(p) { return p[1] != null; });
  if (!parts.length) return null;
  var w = parts.reduce(function(s, p) { return s + p[0]; }, 0);
  return parts.reduce(function(s, p) { return s + p[0] * p[1]; }, 0) / w;
}

// 法人籌碼統計：連買/連賣天數 + 累計
function calcChipStats(chips) {
  if (!chips || !chips.length) return null;
  var arr = chips.slice().sort(function(a, b) { return a.date < b.date ? -1 : 1; });
  var cumF = 0, cumT = 0, i;
  arr.forEach(function(d) { cumF += d.foreign || 0; cumT += d.trust || 0; });
  var buyF = 0, sellF = 0, buyT = 0;
  for (i = arr.length - 1; i >= 0; i--) { if ((arr[i].foreign || 0) > 0) buyF++; else break; }
  for (i = arr.length - 1; i >= 0; i--) { if ((arr[i].foreign || 0) < 0) sellF++; else break; }
  for (i = arr.length - 1; i >= 0; i--) { if ((arr[i].trust || 0) > 0) buyT++; else break; }
  return { cumF: cumF, cumT: cumT, buyStreakF: buyF, sellStreakF: sellF, buyStreakT: buyT, days: arr.length };
}

var SCORE_VERDICTS = [null,
  { tag: '強力迴避', en: 'Strong Avoid', color: 'var(--bear)', stars: 1 },
  { tag: '偏空迴避', en: 'Bearish',      color: '#e57c23',     stars: 2 },
  { tag: '中性觀察', en: 'Neutral',      color: 'var(--gold)', stars: 3 },
  { tag: '偏多買進', en: 'Bullish',      color: '#4caf50',     stars: 4 },
  { tag: '強力買進', en: 'Strong Buy',   color: 'var(--bull)', stars: 5 }];

function calcScore100({ data, ma5, ma20, ma60, r, m, bb, fund, epsHist, fins, bs, cf, chips, bench }) {
  var fins_ = (fins && fins.length) ? fins : [];
  var bs_   = (bs   && bs.length)   ? bs   : [];
  var cf_   = (cf   && cf.length)   ? cf   : [];
  var last = data[data.length - 1];
  var prev = data[data.length - 2] || last;
  var cl = last.close;
  var closes = data.map(function(d) { return d.close; });
  var L = closes.length;
  function smaAt(n, back) {  // n日均線在 back 根之前的值；資料不足回傳 null
    back = back || 0;
    if (L - back < n) return null;
    var s = 0;
    for (var i = L - back - n; i < L - back; i++) s += closes[i];
    return s / n;
  }

  // ── ① 趨勢 Trend Template (25) — Minervini 8 準則 ──────────
  var ma50  = smaAt(50),  ma150 = smaAt(150), ma200 = smaAt(200);
  var ma200prev = smaAt(200, 21);
  var w52 = closes.slice(-252);
  var hi52 = Math.max.apply(null, w52), lo52 = Math.min.apply(null, w52);
  var upVol = 0, dnVol = 0;
  data.slice(-50).forEach(function(d, i, a) {
    var p = i > 0 ? a[i - 1].close : d.open;
    if (d.close >= p) upVol += d.volume; else dnVol += d.volume;
  });
  var ttSubs = [
    { label: '價>50MA',          pts: 3, ok: ma50  != null ? cl > ma50  : null },
    { label: '價>150MA',         pts: 3, ok: ma150 != null ? cl > ma150 : null },
    { label: '價>200MA',         pts: 3, ok: ma200 != null ? cl > ma200 : null },
    { label: '50>150>200MA',     pts: 4, ok: (ma50 != null && ma150 != null && ma200 != null) ? (ma50 > ma150 && ma150 > ma200) : null },
    { label: '200MA上揚1月',     pts: 3, ok: (ma200 != null && ma200prev != null) ? ma200 > ma200prev : null },
    { label: '高於52週低點30%',  pts: 3, ok: lo52 > 0 ? cl >= lo52 * 1.30 : null },
    { label: '距52週高點<25%',   pts: 3, ok: hi52 > 0 ? cl >= hi52 * 0.75 : null },
    { label: '上漲量>下跌量',    pts: 3, ok: upVol > dnVol },
  ];
  var pTrend = 0;
  ttSubs.forEach(function(s) { pTrend += s.ok === true ? s.pts : s.ok === null ? Math.round(s.pts / 3) : 0; });

  // ── ② 相對動能 RS (20) — IBD 加權報酬 vs 大盤 ──────────────
  var stockRet = calcWeightedReturn(closes);
  var benchRet = (bench && bench.length >= 64) ? calcWeightedReturn(bench) : null;
  var alpha = (stockRet != null && benchRet != null) ? stockRet - benchRet
            : stockRet != null ? stockRet - 8  /* 無大盤資料時以年化8%充當基準 */ : null;
  var rsPts = alpha == null ? 5 : alpha >= 30 ? 12 : alpha >= 15 ? 10 : alpha >= 5 ? 8 : alpha >= 0 ? 6 : alpha >= -10 ? 3 : 0;
  var mN = m.dif.length, difNow = m.dif[mN-1], deaNow = m.dea[mN-1];
  var difPrev = m.dif[mN-2], deaPrev = m.dea[mN-2];
  var histNow = m.hist[mN-1], histPrev = m.hist[mN-2];
  var goldenX = difPrev < deaPrev && difNow >= deaNow, deadX = difPrev > deaPrev && difNow <= deaNow;
  var macdPts = deadX ? 0 : goldenX ? 4 : difNow > deaNow && deaNow > 0 && histNow > histPrev ? 4
              : difNow > deaNow && deaNow > 0 ? 3 : difNow > deaNow ? 2 : 1;
  var rsiNow = r[r.length-1] || 50, divSig = detectDivergence(data, r);
  var rsiPts = rsiNow >= 55 && rsiNow < 70 ? 4 : rsiNow >= 45 && rsiNow < 55 ? 3
             : (rsiNow >= 70 && rsiNow < 80) || (rsiNow >= 30 && rsiNow < 45) ? 2 : 1;
  if (divSig === 'bullish' && rsiNow < 50) rsiPts = Math.min(4, rsiPts + 1);
  if (divSig === 'bearish' && rsiNow > 50) rsiPts = Math.max(0, rsiPts - 1);
  var pMom = rsPts + macdPts + rsiPts;
  var momSubs = [
    { label: 'RS α ' + (alpha != null ? (alpha > 0 ? '+' : '') + alpha.toFixed(1) + '%' : '—'), pts: 12, val: rsPts },
    { label: 'MACD', pts: 4, val: macdPts },
    { label: 'RSI ' + Math.round(rsiNow), pts: 4, val: rsiPts },
  ];

  // ── ③ 籌碼 Chips (15) — 台股法人動向 ───────────────────────
  var cs = calcChipStats(chips);
  var fPts, tPts, aPts;
  if (cs) {
    fPts = cs.buyStreakF >= 5 ? 7 : cs.buyStreakF >= 3 ? 5 : cs.cumF > 0 ? 4 : cs.sellStreakF >= 3 ? 0 : 2;
    tPts = cs.buyStreakT >= 3 ? 5 : cs.buyStreakT >= 2 ? 4 : cs.cumT > 0 ? 3 : cs.cumT === 0 ? 2 : 0;
    aPts = (cs.cumF > 0 && cs.cumT > 0) ? 3 : (cs.cumF < 0 && cs.cumT < 0) ? 0 : 1;
  } else { fPts = 3; tPts = 2; aPts = 1; }  // 無資料 → 中性 6/15
  var pChip = fPts + tPts + aPts;
  var chipSubs = [
    { label: cs ? ('外資' + (cs.buyStreakF >= 2 ? '連買' + cs.buyStreakF + '日' : cs.sellStreakF >= 2 ? '連賣' + cs.sellStreakF + '日' : cs.cumF > 0 ? '偏買' : '偏賣')) : '外資 無資料', pts: 7, val: fPts },
    { label: cs ? ('投信' + (cs.buyStreakT >= 2 ? '連買' + cs.buyStreakT + '日' : cs.cumT > 0 ? '偏買' : '中性/賣')) : '投信 無資料', pts: 5, val: tPts },
    { label: '法人同向', pts: 3, val: aPts },
  ];

  // ── ④ 品質 Quality (20) — Piotroski + 質量加速度 + Altman ──
  var psig = calcPiotroski(fins_.length ? fins_ : null, bs_.length ? bs_ : null, cf_.length ? cf_ : null);
  var pScore = psig.p1 + psig.p2 + psig.p3 + psig.p4 + psig.p5 + psig.p6 + psig.p7;
  var pioPts = Math.round(pScore / 7 * 10);
  var roePts = 0, omPts = 0;
  if (fins_.length >= 8 && bs_.length >= 2) {
    var ni4n = fins_.slice(-4).reduce(function(s, q) { return s + (q.NetIncome || 0); }, 0);
    var ni4p = fins_.slice(-8, -4).reduce(function(s, q) { return s + (q.NetIncome || 0); }, 0);
    var eqN = bs_[bs_.length-1].StockholdersEquity || 0, eqP = bs_[bs_.length-2].StockholdersEquity || 0;
    var dROE = (eqN > 0 ? ni4n / eqN * 100 : 0) - (eqP > 0 ? ni4p / eqP * 100 : 0);
    roePts = dROE > 3 ? 4 : dROE > 0 ? 2 : 0;
  }
  if (fins_.length >= 8) {
    var omArr = fins_.slice(-4).map(function(q) { return q.Revenue > 0 ? (q.OperatingIncome || 0) / q.Revenue : null; })
                     .filter(function(v) { return v != null; });
    if (omArr.length >= 3) {
      var rising = 0;
      for (var j = 1; j < omArr.length; j++) { if (omArr[j] > omArr[j-1]) rising++; }
      omPts = rising >= omArr.length - 1 ? 3 : rising > 0 ? 1 : 0;
    }
  }
  var altZ = calcAltmanZ(fins_.length ? fins_ : null, bs_.length ? bs_ : null);
  var azPts = altZ == null ? 1 : altZ > 2.99 ? 3 : altZ > 1.8 ? 1 : 0;
  var pQual = fins_.length ? (pioPts + roePts + omPts + azPts) : 8;  // 無財報 → 中性 8/20
  var qualSubs = [
    { label: 'Piotroski ' + pScore + '/7', pts: 10, val: pioPts },
    { label: 'ΔROE', pts: 4, val: roePts },
    { label: '營益率趨勢', pts: 3, val: omPts },
    { label: 'Altman Z' + (altZ != null ? ' ' + altZ : ''), pts: 3, val: azPts },
  ];

  // ── ⑤ 價值成長 Value & Growth (20) ─────────────────────────
  var grahamData = calcGrahamUpside(fins_.length ? fins_ : null, fund, cl);
  var valuData   = calcValuationUpside(fins_.length >= 8 ? fins_ : null, cf_, fund, cl);
  var gUp = grahamData ? grahamData.upside : null;
  var vUp = valuData   ? valuData.upside   : null;
  var gPts = gUp == null ? 2 : gUp > 30 ? 4 : gUp > 15 ? 3 : gUp > 0 ? 2 : 0;
  var vPts = vUp == null ? 2 : vUp > 30 ? 4 : vUp > 15 ? 3 : vUp > 0 ? 2 : 0;
  var epsPts = 0;
  var epsArr = fins_.length >= 2
    ? fins_.map(function(q) { return q.EPS; }).filter(function(v) { return v != null; })
    : (epsHist || []).map(function(e) { return e.eps; });
  if (epsArr.length >= 2) {
    var eL = epsArr.length;
    if (epsArr[eL-1] > epsArr[eL-2]) epsPts += 2;
    if (epsArr[eL-1] > (epsArr[eL-5] || epsArr[0])) epsPts += 2;
    var streak = 0;
    for (var k = eL - 1; k >= 1; k--) { if (epsArr[k] > epsArr[k-1]) streak++; else break; }
    if (streak >= 3) epsPts += 3; else if (streak >= 2) epsPts += 1;
  }
  var revYoy = fund.rev_yoy || 0;
  var revPts = revYoy >= 30 ? 3 : revYoy >= 10 ? 2 : revYoy >= 0 ? 1 : 0;
  var gmPts = 0;
  if (fins_.length >= 8) {
    var gmN = 0, gmP = 0, revN = 0, revP = 0;
    fins_.slice(-4).forEach(function(q) { revN += q.Revenue || 0; gmN += q.GrossProfit || 0; });
    fins_.slice(-8, -4).forEach(function(q) { revP += q.Revenue || 0; gmP += q.GrossProfit || 0; });
    var gmNp = revN > 0 ? gmN / revN : 0, gmPp = revP > 0 ? gmP / revP : 0;
    gmPts = gmNp > gmPp + 0.02 ? 2 : gmNp > gmPp ? 1 : 0;
  }
  var pVal = gPts + vPts + epsPts + revPts + gmPts;
  var valSubs = [
    { label: 'Graham' + (gUp != null ? (gUp > 0 ? ' +' : ' ') + gUp + '%' : ''), pts: 4, val: gPts },
    { label: 'DCF/Lynch' + (vUp != null ? (vUp > 0 ? ' +' : ' ') + vUp + '%' : ''), pts: 4, val: vPts },
    { label: 'EPS 成長', pts: 7, val: epsPts },
    { label: '營收YoY+毛利', pts: 5, val: revPts + gmPts },
  ];

  // ── 風險旗標 → 評級封頂 ─────────────────────────────────────
  var flags = [], cap = 5;
  if (altZ != null && altZ < 1.8) { flags.push('Altman Z=' + altZ + ' 財務危機'); cap = Math.min(cap, 3); }
  if (ma200 != null && cl < ma200) { flags.push('跌破年線 MA200'); cap = Math.min(cap, 3); }
  if (divSig === 'bearish' && rsiNow > 55) { flags.push('RSI 頂背離'); cap = Math.min(cap, 4); }
  if (cs && cs.sellStreakF >= 5) { flags.push('外資連' + cs.sellStreakF + '日賣超'); cap = Math.min(cap, 4); }

  var total = pTrend + pMom + pChip + pQual + pVal;
  var lvl = total >= 85 ? 5 : total >= 70 ? 4 : total >= 55 ? 3 : total >= 40 ? 2 : 1;
  var capped = Math.min(lvl, cap) < lvl;
  var verdict = Object.assign({}, SCORE_VERDICTS[Math.min(lvl, cap)], { capped: capped });

  var pillars = [
    { key: 'trend', label: '趨勢樣板', en: 'Trend Template',  max: 25, val: pTrend, subs: ttSubs },
    { key: 'mom',   label: '相對動能', en: 'RS Momentum',     max: 20, val: pMom,   subs: momSubs },
    { key: 'chip',  label: '法人籌碼', en: 'Smart Money',     max: 15, val: pChip,  subs: chipSubs },
    { key: 'qual',  label: '財務品質', en: 'Quality',         max: 20, val: pQual,  subs: qualSubs },
    { key: 'val',   label: '價值成長', en: 'Value & Growth',  max: 20, val: pVal,   subs: valSubs },
  ];

  return { total: total, pillars: pillars, flags: flags, verdict: verdict,
           tech: pTrend + pMom, chipsScore: pChip, fundamental: pQual + pVal,
           alpha: alpha,
           altman: altZ, piotroski: { signals: psig, score: pScore },
           grahamUpside: gUp, valuUpside: vUp };
}

function ScoreMatrix100({ sc }) {
  var total = sc.total, pillars = sc.pillars, flags = sc.flags || [];
  var altman = sc.altman, piotroski = sc.piotroski;
  var vd = sc.verdict;

  var azBadge = null;
  if (altman != null) {
    azBadge = altman > 2.99
      ? { label: '財務安全 Z='+altman, color:'var(--bull)', bg:'rgba(30,173,137,0.08)' }
      : altman > 1.8
      ? { label: '財務觀察 Z='+altman, color:'var(--gold)', bg:'rgba(184,146,77,0.08)' }
      : { label: '⚠ 破產風險 Z='+altman, color:'var(--bear)', bg:'rgba(200,112,80,0.12)' };
  }

  // 五力雷達圖（ECharts SVG）
  var radarRef = useRef(null);
  useEffect(function() {
    if (!radarRef.current || !window.echarts || !pillars) return;
    var chart = echarts.init(radarRef.current, null, { renderer: 'svg' });
    chart.setOption({
      backgroundColor: 'transparent',
      radar: {
        indicator: pillars.map(function(p) { return { name: p.label, max: 100 }; }),
        radius: '62%', center: ['50%', '52%'],
        axisName: { fontSize: 11.5, fontFamily: 'BiauKai,標楷體,serif', color: '#3a4763' },
        splitArea: { areaStyle: { color: ['rgba(184,146,77,0.04)', 'rgba(184,146,77,0.08)'] } },
        splitLine: { lineStyle: { color: '#d8cdb0' } },
        axisLine:  { lineStyle: { color: '#d8cdb0' } },
      },
      series: [{
        type: 'radar', symbol: 'circle', symbolSize: 3,
        data: [{
          value: pillars.map(function(p) { return Math.round(p.val / p.max * 100); }),
          lineStyle: { color: '#b8924d', width: 2 },
          itemStyle: { color: '#8a6a2a' },
          areaStyle: { color: 'rgba(184,146,77,0.22)' },
        }],
      }],
    });
    var ro = new ResizeObserver(function() { chart.resize(); });
    ro.observe(radarRef.current);
    return function() { chart.dispose(); ro.disconnect(); };
  }, [pillars && pillars.map(function(p) { return p.val; }).join(',')]);

  var psigs = piotroski ? piotroski.signals : null;
  var psigLabels = [
    { key:'p1', label:'ROA > 0' },
    { key:'p2', label:'ΔROA ↑' },
    { key:'p3', label:'CFO > 0' },
    { key:'p4', label:'現金品質' },
    { key:'p5', label:'ΔCurrentRatio ↑' },
    { key:'p6', label:'ΔGrossMargin ↑' },
    { key:'p7', label:'ΔAssetTurnover ↑' },
  ];

  return (
    <div className="frame-card" style={{ padding:'20px 24px' }}>
      <span className="corner-tl"/><span className="corner-tr"/><span className="corner-bl"/><span className="corner-br"/>

      {/* Altman Z badge */}
      {azBadge && (
        <div style={{ display:'inline-flex', alignItems:'center', gap:6, padding:'4px 12px', marginBottom:14,
                      border:'1px solid '+azBadge.color, background:azBadge.bg, fontSize:'var(--fs-nano)' }}>
          <span style={{ fontFamily:'Times New Roman,serif', fontStyle:'italic', color:'var(--ink-mute)' }}>Altman Z</span>
          <span className="kai" style={{ color:azBadge.color, fontWeight:700 }}>{azBadge.label}</span>
        </div>
      )}

      <div style={{ display:'flex', alignItems:'flex-start', justifyContent:'space-between', marginBottom:18, flexWrap:'wrap', gap:12 }}>
        <div>
          <div style={{ fontSize:'var(--fs-nano)', fontStyle:'italic', color:'var(--ink-mute)', letterSpacing:'.12em', marginBottom:4 }}>
            Scoring Matrix · 量化評分
          </div>
          <div style={{ display:'flex', alignItems:'baseline', gap:6 }}>
            <span className="num" style={{ fontSize:40, fontWeight:700, color:vd.color, lineHeight:1 }}>{total}</span>
            <span className="num" style={{ fontSize:18, color:'var(--ink-mute)' }}>/100</span>
            <span className="kai" style={{ fontSize:16, color:vd.color, marginLeft:10, fontWeight:700 }}>{vd.tag}</span>
          </div>
          <div style={{ display:'flex', gap:3, marginTop:6 }}>
            {'★★★★★'.split('').map(function(s,i){
              return <span key={i} style={{ fontSize:14, color:i<vd.stars?vd.color:'var(--paper-line)' }}>{s}</span>;
            })}
          </div>
          {vd.capped && (
            <div className="kai" style={{ fontSize:'var(--fs-nano)', color:'var(--bear)', marginTop:4 }}>
              ⚠ 風險旗標封頂：分數達標但評級受限
            </div>
          )}
          {flags.length > 0 && (
            <div style={{ display:'flex', flexWrap:'wrap', gap:5, marginTop:8 }}>
              {flags.map(function(f, i) {
                return (
                  <span key={i} className="kai" style={{
                    fontSize:'var(--fs-nano)', padding:'2px 8px', border:'1px solid var(--bear)',
                    color:'var(--bear)', background:'rgba(200,112,80,0.07)' }}>
                    ⚠ {f}
                  </span>
                );
              })}
            </div>
          )}
        </div>
        <div ref={radarRef} style={{ width:230, height:190, flex:'0 0 auto' }} />
      </div>

      <div style={{ borderTop:'1px solid var(--paper-line)', paddingTop:12 }}>
        <div style={{ fontSize:'var(--fs-nano)', fontStyle:'italic', color:'var(--ink-mute)', marginBottom:10, letterSpacing:'.1em' }}>
          FIVE FORCES · 五力分析 — Minervini 趨勢樣板 · IBD 相對強弱 · 法人籌碼 · Piotroski 品質 · Graham 估值
        </div>
        {pillars.map(function(p) {
          var pct = p.val / p.max;
          var bc = pct >= 0.7 ? 'var(--bull)' : pct >= 0.4 ? 'var(--gold)' : 'var(--bear)';
          return (
            <div key={p.key} style={{ marginBottom:13 }}>
              <div style={{ display:'flex', alignItems:'baseline', marginBottom:3, gap:6 }}>
                <span style={{ fontFamily:'BiauKai,標楷體,serif', fontSize:13, flex:'0 0 auto', fontWeight:700 }}>{p.label}</span>
                <span style={{ fontStyle:'italic', color:'var(--ink-mute)', fontSize:'var(--fs-nano)' }}>{p.en}</span>
                <span style={{ flex:'1 1 auto' }} />
                <span className="num" style={{ fontSize:13 }}>
                  <span style={{ color:bc, fontWeight:700 }}>{p.val}</span>
                  <span style={{ color:'var(--ink-mute)' }}>/{p.max}</span>
                </span>
              </div>
              <div style={{ height:5, background:'var(--paper-line)', borderRadius:3, marginBottom:4 }}>
                <div style={{ height:'100%', width:(pct*100)+'%', background:bc, borderRadius:3, transition:'width .4s' }}/>
              </div>
              <div style={{ display:'flex', flexWrap:'wrap', gap:4 }}>
                {p.subs.map(function(s, i) {
                  var good = s.ok === true || (s.val != null && s.pts > 0 && s.val / s.pts >= 0.6);
                  var bad  = s.ok === false || (s.val != null && s.val === 0);
                  var c = good ? 'var(--bull)' : bad ? 'var(--bear)' : 'var(--ink-mute)';
                  return (
                    <span key={i} style={{
                      fontSize:'var(--fs-nano)', padding:'1px 7px', fontFamily:'BiauKai,標楷體,serif',
                      border:'1px solid '+(good||bad ? c : 'var(--paper-line)'), color:c,
                      background: good ? 'rgba(30,173,137,0.05)' : bad ? 'rgba(200,112,80,0.05)' : 'transparent' }}>
                      {s.ok === true ? '✓ ' : s.ok === false ? '✗ ' : ''}{s.label}
                      {s.val != null ? ' ' + s.val + '/' + s.pts : ''}
                    </span>
                  );
                })}
              </div>
            </div>
          );
        })}
      </div>

      {/* Piotroski 7 signals */}
      {psigs && (
        <div style={{ marginTop:16, paddingTop:14, borderTop:'1px solid var(--paper-line)' }}>
          <div style={{ fontSize:'var(--fs-nano)', fontStyle:'italic', color:'var(--ink-mute)', marginBottom:8, letterSpacing:'.1em' }}>
            PIOTROSKI F-SCORE · 財務健康 {piotroski.score}/7
          </div>
          <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
            {psigLabels.map(function(s){
              var pass = psigs[s.key] === 1;
              return (
                <div key={s.key} style={{
                  padding:'3px 10px', fontSize:'var(--fs-nano)', fontFamily:'BiauKai,標楷體,serif',
                  border:'1px solid '+(pass?'var(--bull)':'var(--bear)'),
                  color: pass?'var(--bull)':'var(--bear)',
                  background: pass?'rgba(30,173,137,0.06)':'rgba(200,112,80,0.06)',
                }}>
                  {pass?'✓ ':'✗ '}{s.label}
                </div>
              );
            })}
          </div>
        </div>
      )}

      {/* Graham / DCF upside */}
      {(sc.grahamUpside != null || sc.valuUpside != null) && (
        <div style={{ marginTop:12, display:'flex', gap:16, flexWrap:'wrap' }}>
          {sc.grahamUpside != null && (
            <div style={{ fontSize:12 }}>
              <span style={{ fontStyle:'italic', color:'var(--ink-mute)', fontSize:'var(--fs-nano)' }}>Graham 上檔空間 </span>
              <span className="num" style={{ fontWeight:700, color:sc.grahamUpside>0?'var(--bull)':'var(--bear)' }}>
                {sc.grahamUpside>0?'+':''}{sc.grahamUpside}%
              </span>
            </div>
          )}
          {sc.valuUpside != null && (
            <div style={{ fontSize:12 }}>
              <span style={{ fontStyle:'italic', color:'var(--ink-mute)', fontSize:'var(--fs-nano)' }}>DCF/Lynch 上檔空間 </span>
              <span className="num" style={{ fontWeight:700, color:sc.valuUpside>0?'var(--bull)':'var(--bear)' }}>
                {sc.valuUpside>0?'+':''}{sc.valuUpside}%
              </span>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// FinMind OHLCV row → chart candle
function parseFinMindCandle(row) {
  return {
    date:    (row.date || '').slice(5),
    fullDate: row.date || '',
    open:   +(row.open  || 0),
    close:  +(row.close || 0),
    high:   +(row.max   || row.high || 0),
    low:    +(row.min   || row.low  || 0),
    volume: +(row.Trading_Volume || row.volume || 0),
  };
}

// ============ Safe number helper ============
function safeNum(v, fallback) {
  var n = parseFloat(v);
  return (isNaN(n) || !isFinite(n)) ? (fallback || 0) : n;
}

// ============ App ============
function App() {
  const initialCode = new URLSearchParams(location.search).get('code') || '2330';
  const [code, setCode] = useState(initialCode);
  const [query, setQuery] = useState('');
  const [activeTab, setActiveTab] = useState(() => {
    const p = new URLSearchParams(window.location.search);
    const t = p.get('tab');
    return (t === 'signal' || t === 'fund' || t === 'tech' || t === 'margin' || t === 'valuation') ? t : 'tech';
  }); // 'tech' | 'signal' | 'fund' | 'margin' | 'valuation'

  // 從掃描器帶入的進出場價位（URL params: entry, t1, t2, sl）
  const _sp = new URLSearchParams(window.location.search);
  const [signalPrices] = useState(() => {
    const entry = parseFloat(_sp.get('entry'));
    const t1    = parseFloat(_sp.get('t1'));
    const t2    = parseFloat(_sp.get('t2'));
    const sl    = parseFloat(_sp.get('sl'));
    if (!entry) return null;
    return { entry, t1: t1||null, t2: t2||null, sl: sl||null };
  });

  // ===== full market list (for search + peers + stock lookup) =====
  // Must be declared BEFORE stock useMemo so stock can use fullList
  const [fullList, setFullList] = useState(STOCK_LIST);

  useEffect(function() {
    fetch('/api/stock?type=list&market=all')
      .then(function(r) { return r.json(); })
      .then(function(list) {
        if (!Array.isArray(list) || !list.length) return;
        setFullList(list.map(function(s) {
          return {
            code: s.Code,
            name: s.Name,
            base: parseFloat(s.ClosingPrice) || 100,
            kind: classifySecurity(s.Code),
            market: s.market || '',   // twse=上市 / otc=上櫃
            drift: 0.002,
            vol: 0.025,
            price: parseFloat(s.ClosingPrice) || 0,
            chgPct: (function() {
              var close = parseFloat(s.ClosingPrice) || 0;
              var change = parseFloat(s.Change) || 0;
              var prev = close - change;
              return prev > 0 ? (change / prev) * 100 : 0;
            })(),
          };
        }));
      })
      .catch(function() {});
  }, []);

  // Code-tagged price data: prevents stale rows from previous stock polluting new chart
  const [priceData,    setPriceData]    = useState({ code: null, rows: null });
  const [rtQuote,      setRtQuote]      = useState(null);
  const [yuantaQuote,  setYuantaQuote]  = useState(null);  // 元大即時
  const [yuantaOnline, setYuantaOnline] = useState(false);
  const qhN = window.useQhRefresh ? window.useQhRefresh() : 0;  // 全站刷新鈕計數

  // 元大 HTTP 輪詢（每秒查詢一次，Chrome 允許 http://localhost 從 https:// fetch）
  useEffect(function() {
    var alive = true;
    var timer = null;

    function poll() {
      if (!alive || !code) return;
      fetch('http://localhost:8766/quote?code=' + code)
        .then(function(r) { return r.json(); })
        .then(function(d) {
          if (!alive) return;
          if (d && d.price > 0 && d.code === code) {
            setYuantaOnline(true);
            setYuantaQuote(d);
          } else {
            // 伺服器有回應但尚無報價（可能正在訂閱中）
            setYuantaOnline(true);
          }
          timer = setTimeout(poll, 1000);
        })
        .catch(function() {
          if (!alive) return;
          // 伺服器不在線
          setYuantaOnline(false);
          timer = setTimeout(poll, 3000);
        });
    }

    setYuantaQuote(null);
    poll();

    return function() {
      alive = false;
      clearTimeout(timer);
    };
  }, [code]);

  useEffect(function() {
    // Reset immediately — mark as "loading for this code"
    setPriceData({ code: code, rows: null });
    setRtQuote(null);
    var alive = true;

    // Historical K-lines
    fetch('/api/stock?type=price&code=' + code)
      .then(function(r) { return r.json(); })
      .then(function(d) {
        var rows = (d.data || []).map(parseFinMindCandle).filter(function(c) { return c.close > 0; });
        if (rows.length >= 10) {
          setPriceData({ code: code, rows: rows });
        }
      })
      .catch(function() {});

    // Real-time quote — QuoteHub v2 訂閱(保留 rtCacheWrite 跨分頁廣播)
    var unsubRt = window.QuoteHub.subscribe({
      type: 'realtime', codes: [code],
      onData: function(d) {
        if (!alive) return;
        var msg = (d.msgArray || [])[0];
        if (msg) {
          setRtQuote({ _code: code, data: msg });
          var price = parseFloat(msg.z) || parseFloat(msg.b) || 0;
          var prev  = parseFloat(msg.y) || 0;
          var name  = msg.n || code;
          if (price > 0 && window.rtCacheWrite) window.rtCacheWrite(code, price, prev, name);
        }
      }
    });

    return function() { alive = false; unsubRt(); };
  }, [code, qhN]);

  // stock lookup uses fullList (full market) with a safe fallback that shows the
  // actual code — never falls back to TSMC for unknown codes
  const stock = useMemo(function() {
    var found = fullList.find(function(s) { return s.code === code; });
    return found || { code: code, name: code, base: 100, drift: 0.002, vol: 0.025 };
  }, [code, fullList]);

  // Only use priceData if it belongs to the CURRENT code (prevents stale cross-stock data)
  const data = useMemo(function() {
    if (priceData.code === code && priceData.rows && priceData.rows.length >= 10) {
      return priceData.rows;
    }
    return generateKLines(stock.base, 120, stock.drift, stock.vol);
  }, [priceData, stock, code]);
  const ma5  = useMemo(() => sma(data, 5),  [data]);
  const ma20 = useMemo(() => sma(data, 20), [data]);
  const ma60 = useMemo(() => sma(data, 60), [data]);
  const r    = useMemo(() => rsi(data, 14), [data]);
  const m    = useMemo(() => macd(data), [data]);
  const bb   = useMemo(() => bollinger(data, 20, 2), [data]);
  const atrArr = useMemo(() => atr(data, 14), [data]);
  const pivot  = useMemo(() => pivotLevels(data, 60), [data]);
  // SignalPanel（signal.jsx）的純運算函式，父層直接呼叫算出進場/停利/停損，
  // 供 ChartPro 畫線用；不改 signal.jsx 檔案本身，只是呼叫它已暴露的全域函式
  const computedSignal = useMemo(
    () => computeSignal(data, m, r, bb, atrArr, pivot),
    [data, m, r, bb, atrArr, pivot]
  );
  // 外部深連結（例如舊的掃描器）帶 URL signalPrices 時優先用它；否則用 SignalPanel 算出的值
  const effectiveSignalPrices = signalPrices || (computedSignal && computedSignal.entryPrice
    ? { entry: computedSignal.entryPrice, t1: computedSignal.t1, t2: computedSignal.t2, sl: computedSignal.sl }
    : null);

  const [hovered, setHovered] = useState(null);
  const last = data[data.length - 1];
  const prev = data[data.length - 2];
  // Real-time price — 元大優先，fallback TWSE MIS
  const rtMsg = (rtQuote && rtQuote._code === code) ? rtQuote.data : null;
  function parseMIS(v) {
    if (!v || v === '-' || v === '--') return 0;
    return parseFloat(v) || 0;
  }
  const rtClose = rtMsg ? (parseMIS(rtMsg.z) || parseMIS(rtMsg.b)) : 0;
  const rtPrev  = rtMsg ? parseMIS(rtMsg.y) : 0;

  // 元大即時（優先）
  const yuantaPrice = (yuantaQuote && yuantaQuote.code === code && yuantaQuote.price > 0)
    ? yuantaQuote.price : 0;
  const isYuanta    = yuantaPrice > 0;
  const isRealtime  = isYuanta || rtClose > 0;
  const displayClose = isYuanta ? yuantaPrice : (rtClose > 0 ? rtClose : last.close);
  const displayPrev  = rtPrev  > 0 ? rtPrev  : prev.close;
  const chg = displayClose - displayPrev;
  const pctChg = displayPrev > 0 ? (chg / displayPrev) * 100 : 0;
  const last5  = ma5[ma5.length - 1];
  const last20 = ma20[ma20.length - 1];
  const last60 = ma60[ma60.length - 1];
  const lastRsi = r[r.length - 1];
  const lastMacd = m.hist[m.hist.length - 1];

  // Legacy 0-10 score (kept for signals grid logic)
  // Scoring — 修正版（移植 CM_MacD_Ult_MTF 邏輯 + RSI 分段）
  const score = useMemo(() => {
    let s = 0;
    const n = m.dif.length;
    // MACD：交叉訊號（+3）> OSC方向（+1）
    const difNow = m.dif[n-1], deaNow = m.dea[n-1];
    const difPrev = m.dif[n-2], deaPrev = m.dea[n-2];
    if (difPrev < deaPrev && difNow >= deaNow) s += 3; // 黃金交叉
    else if (difPrev > deaPrev && difNow <= deaNow) s -= 3; // 死亡交叉
    else if (difNow > deaNow) s += 1; // DIF 在 DEA 上
    else s -= 1;
    // ATR 位置：多頭且 close > BB 中軌
    const bbMid = bb.mid[bb.mid.length - 1];
    if (bbMid && last.close > bbMid) s += 1;
    // MA 位階
    if (last.close > last5)  s += 0.5;
    if (last.close > last20) s += 1;
    if (last.close > last60) s += 1;
    if (last5 > last20 && last20 > last60) s += 1.5; // 多頭排列
    // RSI 分段（修正）
    if (lastRsi >= 50 && lastRsi < 65) s += 1;
    else if (lastRsi >= 65 && lastRsi < 75) s += 0.5; // 偏強但接近超買
    else if (lastRsi >= 75) s -= 1.5; // 超買
    else if (lastRsi < 30) s += 0.5;  // 超賣反彈機會
    // RSI Divergence
    const div = detectDivergence(data, r);
    if (div === 'bullish') s += 1;
    if (div === 'bearish') s -= 1;
    return Math.max(-4, Math.min(10, Math.round(s)));
  }, [data, m, bb, r]);

  const search = useMemo(function() {
    if (!query.trim()) return [];
    var q = query.trim().toLowerCase();
    var qZh = query.trim();
    // 排序：個股優先（代號完全符合 > 代號開頭 > 名稱），ETF/特別股/TDR 墊後並標示
    return fullList
      .filter(function(s) { return s.code.toLowerCase().includes(q) || (s.name || '').includes(qZh); })
      .map(function(s) {
        var kind = s.kind || classifySecurity(s.code);
        var rel = s.code.toLowerCase() === q ? 0
                : s.code.toLowerCase().indexOf(q) === 0 ? 1
                : (s.name || '').indexOf(qZh) === 0 ? 2 : 3;
        return Object.assign({}, s, { kind: kind, _rank: SEC_KIND_RANK[kind] * 10 + rel });
      })
      .sort(function(a, b) { return a._rank - b._rank || a.code.localeCompare(b.code); })
      .slice(0, 8);
  }, [query, fullList]);

  // ===== fundamental + chip from API, fallback to mock =====
  const [fundApi, setFundApi] = useState(null);
  const [chipRows, setChipRows] = useState(null);
  const [chartDividends, setChartDividends] = useState([]);
  const [chartFinancials, setChartFinancials] = useState([]);
  const [bsQuarters, setBsQuarters] = useState([]);
  const [cfQuarters, setCfQuarters] = useState([]);

  useEffect(() => {
    setFundApi(null);
    setChartDividends([]);
    fetch(`/api/stock?type=fundamental&code=${code}`)
      .then(r => r.json())
      .then(d => {
        const per = d.per || [];
        const last = per[per.length - 1] || {};
        setFundApi({ pe: last.PER || null, pb: last.PBR || null });
        setChartDividends(d.dividend || []);
      })
      .catch(() => setFundApi({}));
  }, [code]);

  // TWSE/TPEx BWIBBU — 即時本益比、淨值比、殖利率（比 FinMind per 更即時且含上櫃）
  const [bwibbu, setBwibbu] = useState(null);
  useEffect(() => {
    setBwibbu(null);
    fetch(`/api/stock?type=bwibbu&code=${code}`)
      .then(r => r.json())
      .then(d => { if (d && (d.pe || d.pb || d.yield)) setBwibbu(d); })
      .catch(() => {});
  }, [code]);

  // 券資比分頁：改用 ?code= 單碼查詢（server 端 KV 快取 12h 不變），
  // 不再拉全市場 ~1845 檔 byCode 查找表（體檢 2026-07-18 效能項）
  const [marginData, setMarginData] = useState(null);
  useEffect(() => {
    setMarginData(null);
    fetch(`/api/stock?type=margin_short&code=${code}`)
      .then(r => r.json())
      .then(d => setMarginData(d))
      .catch(() => setMarginData({}));
  }, [code]);

  useEffect(() => {
    setChartFinancials([]);
    fetch(`/api/stock?type=financials&code=${code}`)
      .then(r => r.json())
      .then(d => setChartFinancials(d.quarters || []))
      .catch(() => {});
  }, [code]);

  useEffect(() => {
    setChipRows(null);
    fetch(`/api/stock?type=chipboard&code=${code}`)
      .then(r => r.json())
      .then(d => setChipRows(parseChipboard(d.data, 10)))
      .catch(() => setChipRows([]));
  }, [code]);

  useEffect(() => {
    setBsQuarters([]);
    fetch(`/api/stock?type=balance_sheet&code=${code}`)
      .then(r => r.json())
      .then(d => { if (d.quarters && d.quarters.length) setBsQuarters(d.quarters); })
      .catch(() => {});
  }, [code]);

  useEffect(() => {
    setCfQuarters([]);
    fetch(`/api/stock?type=cashflow&code=${code}`)
      .then(r => r.json())
      .then(d => { if (d.quarters && d.quarters.length) setCfQuarters(d.quarters); })
      .catch(() => {});
  }, [code]);

  // TAIEX 1年收盤價 — IBD RS 相對強弱的大盤基準（只抓一次）
  const [twiiCloses, setTwiiCloses] = useState(null);
  useEffect(() => {
    fetch('/api/stock?type=us_quote&code=%5ETWII&range=1y&interval=1d')
      .then(r => r.json())
      .then(d => {
        var res = d && d.chart && d.chart.result && d.chart.result[0];
        var cls = ((res && res.indicators && res.indicators.quote && res.indicators.quote[0] && res.indicators.quote[0].close) || [])
          .filter(function(c) { return c > 0; });
        if (cls.length >= 64) setTwiiCloses(cls);
      })
      .catch(() => {});
  }, []);

  const fbk = getFundamental(code);

  // 真實季報衍生指標：EPS TTM / EPS YoY / 營收 YoY / ROE（取代 mock fallback）
  const realFund = useMemo(function() {
    var fins = chartFinancials || [];
    var out = {};
    var epsQ = fins.filter(function(q) { return q.EPS != null; });
    if (epsQ.length >= 4) {
      out.eps = epsQ.slice(-4).reduce(function(a, q) { return a + q.EPS; }, 0);
      if (epsQ.length >= 8) {
        var prev4 = epsQ.slice(-8, -4).reduce(function(a, q) { return a + q.EPS; }, 0);
        if (Math.abs(prev4) > 0.0001) out.eps_yoy = (out.eps - prev4) / Math.abs(prev4) * 100;
      }
    }
    var revQ = fins.filter(function(q) { return q.Revenue != null; });
    if (revQ.length >= 8) {
      var r4 = revQ.slice(-4).reduce(function(a, q) { return a + q.Revenue; }, 0);
      var p4 = revQ.slice(-8, -4).reduce(function(a, q) { return a + q.Revenue; }, 0);
      if (p4 > 0) out.rev_yoy = (r4 - p4) / p4 * 100;
    }
    var niQ = fins.filter(function(q) { return q.NetIncome != null; });
    var eq = (bsQuarters || []).filter(function(q) { return q.StockholdersEquity > 0; });
    if (niQ.length >= 4 && eq.length) {
      var ttmNI = niQ.slice(-4).reduce(function(a, q) { return a + q.NetIncome; }, 0);
      out.roe = ttmNI / eq[eq.length - 1].StockholdersEquity * 100;
    }
    return out;
  }, [chartFinancials, bsQuarters]);

  const fund = {
    pe: safeNum((bwibbu && bwibbu.pe != null) ? bwibbu.pe
        : (fundApi && fundApi.pe != null) ? fundApi.pe : fbk.pe, fbk.pe),
    pb: safeNum((bwibbu && bwibbu.pb != null) ? bwibbu.pb
        : (fundApi && fundApi.pb != null) ? fundApi.pb : fbk.pb, fbk.pb),
    dy: safeNum((bwibbu && bwibbu.yield) ? bwibbu.yield : fbk.dy, 3),
    eps: safeNum(realFund.eps != null ? realFund.eps : fbk.eps, 10),
    roe: safeNum(realFund.roe != null ? realFund.roe : fbk.roe, 15),
    rev_yoy: safeNum(realFund.rev_yoy != null ? realFund.rev_yoy : fbk.rev_yoy, 0),
    eps_yoy: safeNum(realFund.eps_yoy != null ? realFund.eps_yoy : fbk.eps_yoy, 0),
    peers: fbk.peers || [],
    isReal: realFund.eps != null,
  };

  // 8Q EPS：優先用真實季報（含 YoY 與去年同季），季報未到才用模擬
  const epsHist = useMemo(function() {
    var fins = (chartFinancials || []).filter(function(q) { return q.EPS != null && q.date; });
    if (fins.length >= 4) {
      var byKey = {};
      fins.forEach(function(q) {
        var yr = parseInt(q.date.slice(0, 4));
        var qn = Math.ceil(parseInt(q.date.slice(5, 7) || '1') / 3);
        byKey[yr + 'Q' + qn] = q.EPS;
      });
      return fins.slice(-8).map(function(q) {
        var yr = parseInt(q.date.slice(0, 4));
        var qn = Math.ceil(parseInt(q.date.slice(5, 7) || '1') / 3);
        var prev = byKey[(yr - 1) + 'Q' + qn];
        var yoy = (prev != null && Math.abs(prev) > 0.0001)
          ? +((q.EPS - prev) / Math.abs(prev) * 100).toFixed(1) : null;
        return { q: String(yr).slice(2) + 'Q' + qn, eps: +q.EPS.toFixed(2), prev: prev != null ? prev : null, yoy: yoy };
      });
    }
    return genEPSHistory(code);
  }, [code, chartFinancials]);

  // 五力評分 v3 — must be after fund + epsHist are defined
  const score100 = useMemo(function() {
    return calcScore100({ data, ma5, ma20, ma60, r, m, bb, fund, epsHist,
                          fins: chartFinancials, bs: bsQuarters, cf: cfQuarters,
                          chips: chipRows, bench: twiiCloses });
  }, [data, ma5, ma20, ma60, r, m, bb, fund, epsHist, chartFinancials, bsQuarters, cfQuarters, chipRows, twiiCloses]);

  const verdict = score100.verdict;  // 含風險旗標封頂

  const _chipRaw = (chipRows && chipRows.length > 0) ? chipRows : genChips(code);
  const _chipRawAbs = _chipRaw.reduce(function(a, c) {
    return a + Math.abs(c.foreign) + Math.abs(c.trust) + Math.abs(c.dealer);
  }, 0);
  const chips = (_chipRawAbs > 0) ? _chipRaw : genChips(code);
  const totalChip = chips.reduce(function(a, c) {
    return { foreign: a.foreign + c.foreign, trust: a.trust + c.trust, dealer: a.dealer + c.dealer };
  }, { foreign: 0, trust: 0, dealer: 0 });
  const streak = useMemo(() => chipStreak(chips), [chips]);
  const peers = (fund.peers || []).map(function(p) {
    var ps = fullList.find(function(s) { return s.code === p; });
    var pf = getFundamental(p);
    if (!ps) return null;
    return Object.assign({}, ps, pf);
  }).filter(Boolean);

  // AI insight
  const [insight, setInsight] = useState(null);
  const [insightLoading, setInsightLoading] = useState(false);
  const askAI = async () => {
    setInsightLoading(true);
    setInsight(null);
    try {
      const prompt = `你是「諸葛亮 (孔明)」,蜀漢丞相、號臥龍,
今穿越至現代,任 Formosa & Co. 投資週刊總策士。針對 ${stock.code} ${stock.name},
目前股價 ${displayClose.toFixed(2)},日漲跌 ${pctChg.toFixed(2)}%。
技術面:RSI ${lastRsi.toFixed(1)}, MACD OSC ${lastMacd.toFixed(2)},
均線 MA5=${last5?.toFixed(0)}, MA20=${last20?.toFixed(0)}, MA60=${last60?.toFixed(0)},
五力評分 ${score100.total}/100（${score100.pillars.map(p=>p.label+p.val+'/'+p.max).join(' ')}，${verdict.tag}${verdict.capped?'·風險旗標封頂':''}）。
RS相對大盤α ${score100.alpha!=null?score100.alpha.toFixed(1)+'%':'N/A'}，Piotroski ${score100.piotroski?score100.piotroski.score:'-'}/7，Altman Z=${score100.altman!=null?score100.altman:'N/A'}，Graham上檔${score100.grahamUpside!=null?score100.grahamUpside+'%':'N/A'}${score100.flags.length?'。風險：'+score100.flags.join('、'):''}。
基本面:P/E ${fund.pe}, P/B ${fund.pb}, 殖利率 ${fund.dy}%, ROE ${fund.roe}%, EPS年增 ${fund.eps_yoy}%。
請以 150 字內,語氣典雅文白相間,偶用「臣聞」「竊以為」等古文,
給出三點觀察與一句操作方向(不需給具體價位)。`;
      const reply = await window.claudeComplete({
        messages: [{ role: 'user', content: prompt }]
      });
      setInsight(reply);
    } catch (e) {
      var msg = e && e.message ? e.message : String(e);
      var hint = '';
      if (msg.indexOf('not configured') !== -1 || msg.indexOf('503') !== -1) {
        hint = '\n⚠ 請至 Cloudflare Pages → Settings → Functions → AI Bindings，新增 Variable name: AI';
      }
      setInsight('（AI 連線失敗 — ' + msg + hint + '）');
    } finally {
      setInsightLoading(false);
    }
  };

  return (
    <div className="shell">
      <window.Masthead />
      <window.TickerTape />
      <window.NavBar active="stock" />

      {/* Search header */}
      <section className="stock-header">
        <div className="stock-id">
          <div className="stock-eyebrow">
            <span style={{ fontStyle:'italic' }}>Daily Quote</span>
            <span className="kai" style={{ marginLeft: 10 }}>個股檔案</span>
          </div>
          <h1 className="stock-name">
            <span className="stock-code num">{stock.code}</span>
            <span className="kai">{stock.name}</span>
            <span className="stock-en">
              {stock.market === 'otc' ? 'TPEx Listed · 上櫃' : 'TWSE Listed · 上市'}
              {stock.kind && stock.kind !== 'stock' ? ` · ${SEC_KIND_LABEL[stock.kind] || ''}` : ''}
            </span>
            <a
              href={`https://www.tradingview.com/chart/?symbol=${stock.market === 'otc' ? 'TPEX' : 'TWSE'}:${stock.code}`}
              target="_blank" rel="noopener"
              className="kai"
              style={{
                marginLeft: 14, fontSize: 12, padding: '3px 10px',
                border: '1px solid var(--paper-line)', borderRadius: 4,
                color: 'var(--gold-deep)', textDecoration: 'none', verticalAlign: '2px',
              }}
              title="在 TradingView 開啟圖表">
              📈 TradingView
            </a>
            <button
              onClick={() => {
                if (!window.ShareCard || !score100 || !score100.verdict) return;
                window.ShareCard.openStockCard({
                  code: stock.code, name: stock.name, market: stock.market,
                  total: score100.total, verdict: score100.verdict,
                  pillars: score100.pillars, flags: score100.flags,
                });
              }}
              className="kai"
              style={{
                marginLeft: 8, fontSize: 12, padding: '3px 10px',
                border: '1px solid var(--gold)', borderRadius: 4,
                background: 'var(--gold)', color: '#fff',
                cursor: 'pointer', verticalAlign: '2px',
              }}
              title="產生雜誌風分享卡（存成圖片）">
              🎴 分享卡
            </button>
          </h1>
        </div>
        <div className="stock-search">
          <div className="search-box" style={{ minWidth: 280 }}>
            <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
            <input
              placeholder="輸入代號 (2330)、名稱"
              value={query}
              onChange={e => setQuery(e.target.value)}
              onKeyDown={e => {
                if (e.key === 'Enter' && search[0]) { setCode(search[0].code); setQuery(''); }
              }}
            />
          </div>
          {search.length > 0 && (
            <div className="search-results" style={{ position:'absolute', width:280, top: 'calc(100% + 6px)' }}>
              {search.map(s => (
                <div key={s.code} className="search-result"
                     onMouseDown={() => { setCode(s.code); setQuery(''); }}>
                  <div className="code">{s.code}</div>
                  <div><div className="name">
                    {s.name}
                    {s.market && (
                      <span className="kai" style={{
                        marginLeft: 6, fontSize: 'var(--fs-nano)', padding: '0 5px',
                        verticalAlign: '1px', letterSpacing: '.08em',
                        color: s.market === 'otc' ? 'var(--gold-deep)' : 'var(--ink-soft)',
                        border: `1px solid ${s.market === 'otc' ? 'var(--gold)' : 'var(--paper-line)'}`,
                        background: s.market === 'otc' ? 'rgba(184,146,77,.07)' : 'transparent',
                      }}>{s.market === 'otc' ? '上櫃' : '上市'}</span>
                    )}
                    {s.kind && s.kind !== 'stock' && (
                      <span style={{
                        marginLeft: 4, fontSize: 'var(--fs-nano)', padding: '0 5px',
                        border: '1px solid var(--paper-line)', color: 'var(--ink-mute)',
                        verticalAlign: '1px', letterSpacing: '.05em',
                      }}>{SEC_KIND_LABEL[s.kind] || s.kind}</span>
                    )}
                  </div></div>
                </div>
              ))}
            </div>
          )}
        </div>
      </section>

      {/* Tab Bar */}
      <div style={{
        display:'flex', gap:0,
        borderBottom:'1.5px solid var(--paper-line)',
        marginBottom:0,
        background:'var(--paper-bright)',
        paddingLeft:8,
      }}>
        {[
          { id:'tech',   label:'技術分析',   en:'Technical Analysis' },
          { id:'signal', label:'進出場訊號', en:'Entry Signal' },
          { id:'fund',   label:'企業基本面', en:'Fundamentals' },
          { id:'margin', label:'券資比',     en:'Margin/Short' },
          { id:'valuation', label:'機構估值', en:'Institutional Valuation' },
        ].map(function(t) {
          var active = activeTab === t.id;
          return (
            <button key={t.id}
              onClick={function() { setActiveTab(t.id); }}
              style={{
                position:'relative',
                padding:'13px 32px 11px',
                border:'none',
                cursor:'pointer',
                background: active ? 'var(--paper)' : 'transparent',
                marginBottom: -1.5,
                fontFamily:'BiauKai,標楷體,serif',
                fontSize:14,
                color: active ? 'var(--ink)' : 'var(--ink-mute)',
                letterSpacing:'.1em',
                transition:'color .18s',
                outline:'none',
              }}>
              {/* 底線：active 時顯示金色，向內縮以形成精緻效果 */}
              <span style={{
                position:'absolute', bottom:0, left:active?16:32, right:active?16:32,
                height:2,
                background: active ? 'var(--gold)' : 'transparent',
                borderRadius:1,
                transition:'left .2s, right .2s, background .2s',
              }} />
              <span style={{ fontFamily:'BiauKai,標楷體,serif' }}>{t.label}</span>
              <span style={{
                display:'inline-block',
                marginLeft:7,
                fontSize:'var(--fs-nano)',
                fontStyle:'italic',
                fontFamily:'Times New Roman,serif',
                color: active ? 'var(--gold)' : 'var(--ink-mute)',
                letterSpacing:'.04em',
                opacity: active ? 1 : 0.7,
                transition:'color .18s, opacity .18s',
              }}>
                {t.en}
              </span>
            </button>
          );
        })}
      </div>

      {/* 進出場訊號 Tab — SMC 訂單塊/FVG/LONG-WAIT-AVOID 評分引擎 */}
      {activeTab === 'signal' && (
        <div style={{ padding: '0 16px' }}>
          {window.SignalPanel
            ? <window.SignalPanel data={data} macdObj={m} rsiArr={r} bbObj={bb} atrArr={atrArr} pivotObj={pivot} rtClose={rtClose} />
            : <div style={{ padding: 40, textAlign:'center', color:'var(--ink-mute)' }}>訊號模組載入中…</div>
          }
        </div>
      )}

      {/* 企業基本面 Tab */}
      {activeTab === 'fund' && window.FundamentalApp && (
        <window.FundamentalApp externalCode={code} />
      )}
      {activeTab === 'fund' && !window.FundamentalApp && (
        <div style={{ padding:40, textAlign:'center', color:'var(--ink-mute)', fontStyle:'italic' }}>
          基本面模組載入中…
        </div>
      )}

      {/* 機構估值 Tab — 策展DCF/投資論點/可比公司/催化劑，見 valuation.jsx */}
      {activeTab === 'valuation' && window.InstitutionalValuation && (
        <window.InstitutionalValuation code={code} />
      )}
      {activeTab === 'valuation' && !window.InstitutionalValuation && (
        <div style={{ padding:40, textAlign:'center', color:'var(--ink-mute)', fontStyle:'italic' }}>
          機構估值模組載入中…
        </div>
      )}

      {/* 券資比 Tab — 融資使用率/融券佔比，查此代碼的原始數值（見 券資比雷達.html 的 margin_short API） */}
      {activeTab === 'margin' && (
        <div style={{ padding: '20px 16px' }}>
          {!marginData ? (
            <div style={{ padding: 40, textAlign:'center', color:'var(--ink-mute)' }}>券資比資料讀取中…</div>
          ) : (function() {
            const row = (marginData.byCode || {})[code];
            if (!row) return (
              <div style={{ padding: 40, textAlign:'center', color:'var(--ink-mute)' }}>
                查無此代碼的融資融券資料（可能是 ETF/新上市或資料來源當日未收錄）
              </div>
            );
            const inSurge = (marginData.marginSurge || []).some(function(d) { return d.code === code; });
            const inSqueeze = (marginData.shortSqueeze || []).some(function(d) { return d.code === code; });
            const Tile = function(props) {
              return (
                <div style={{ padding:'14px 18px', border:'1px solid var(--paper-line)', borderRadius:6, minWidth:160 }}>
                  <div style={{ fontSize:11.5, color:'var(--ink-mute)', marginBottom:4 }}>{props.label}</div>
                  <div style={{ fontSize:22, fontFamily:'Times New Roman,serif', color: props.color || 'var(--ink)' }}>{props.value}</div>
                </div>
              );
            };
            return (
              <div>
                {(inSurge || inSqueeze) && (
                  <div style={{ marginBottom:14, padding:'8px 14px', background:'rgba(184,146,77,.1)', borderRadius:6, fontSize:13 }}>
                    {inSurge ? '🔥 今日入榜「融資使用率暴增」TOP20　' : ''}
                    {inSqueeze ? '🎯 今日入榜「潛在軋空雷達」TOP20' : ''}
                    　（<a href="券資比雷達.html" style={{ color:'var(--gold-deep)' }}>看全市場排行</a>）
                  </div>
                )}
                <div style={{ display:'flex', gap:14, flexWrap:'wrap', marginBottom:20 }}>
                  <Tile label="融資使用率" value={(row.marginUsage * 100).toFixed(1) + '%'} />
                  <Tile label="較前日Δ" value={(row.marginUsageDeltaPp >= 0 ? '+' : '') + row.marginUsageDeltaPp.toFixed(2) + 'pp'}
                    color={row.marginUsageDeltaPp > 0 ? 'var(--bear)' : 'var(--ink)'} />
                  <Tile label="融券佔比" value={(row.shortRatio * 100).toFixed(2) + '%'} />
                  <Tile label="融券今日餘額" value={row.shortToday.toLocaleString() + ' 張'} />
                </div>
                <p style={{ fontSize: 11.5, color: 'var(--ink-faint)' }}>
                  ⚠️ 純籌碼面統計，不構成任何買賣建議；資料為 T 日盤後公布，涵蓋上市（TWSE）＋上櫃（TPEx）。資料日期：{marginData.date || '—'}
                </p>
              </div>
            );
          })()}
        </div>
      )}

      {/* 技術分析 Tab */}
      {activeTab === 'tech' && <div>

      {/* Quote panel + Verdict */}
      <section className="quote-board">
        <div className="quote-main">
          <div className="quote-price num">
            {displayClose.toLocaleString('en-US', {minimumFractionDigits:2})}
            <span style={{ fontSize:'var(--fs-nano)', marginLeft:8, fontFamily:'Times New Roman,serif',
              fontStyle:'italic',
              color: isYuanta ? 'var(--accent-yuanta)' : (isRealtime ? 'var(--bull)' : 'var(--ink-mute)'),
              fontVariant:'small-caps', letterSpacing:'.12em' }}>
              {isYuanta ? '元大即時' : (isRealtime ? 'live' : '前收')}
            </span>
          </div>
          <div className={`quote-chg num ${chg >= 0 ? 'bull-c' : 'bear-c'}`}>
            <span className={chg >= 0 ? 'arrow-up' : 'arrow-down'}></span>
            {chg >= 0 ? '+' : ''}{chg.toFixed(2)} ({pctChg >= 0 ? '+' : ''}{pctChg.toFixed(2)}%)
          </div>
          <div className="quote-ohlc">
            <span><i>O</i>{last.open}</span>
            <span><i>H</i>{last.high}</span>
            <span><i>L</i>{last.low}</span>
            <span><i>V</i>{(last.volume/1000).toFixed(1)}K</span>
          </div>
        </div>
        <div className="quote-verdict" style={{ borderColor: verdict.color }}>
          <div className="verdict-eyebrow">Editor's Verdict <span className="kai" style={{ marginLeft:6 }}>編輯評斷</span></div>
          <div className="verdict-row">
            <div className="verdict-tag kai" style={{ color: verdict.color, borderColor: verdict.color }}>
              {verdict.tag}
            </div>
            <div className="verdict-en italic-en">{verdict.en}</div>
            <div className="verdict-score num">{score100.total}<small>/100</small></div>
          </div>
          <div className="verdict-bar">
            <div className="verdict-bar-fill" style={{ width: `${score100.total}%`, background: verdict.color }}/>
          </div>
        </div>
      </section>

      {/* K-line + indicators — TradingView pro chart */}
      <window.SectionHead
        kicker="Technical Chart · Pro"
        title="技術線圖"
        titleEn="Chart Pro"
        sub="Click an indicator chip to toggle · drag a timeframe to zoom"
      />
      <window.ChartPro
        twColors
        data={data}
        stock={stock}
        rtQuote={rtMsg}
        last5={last5}
        last20={last20}
        last60={last60}
        bb={bb}
        atrArr={atrArr}
        dividends={chartDividends}
        financials={chartFinancials}
        signalPrices={effectiveSignalPrices}
      />

      {/* Diagnostic rows */}
      <window.SectionHead
        kicker="Diagnostics"
        title="多空訊號"
        titleEn="Signals"
        sub="Auto-scored on six dimensions"
      />
      <div className="signals-grid">
        {/* MACD — 交叉訊號優先 */}
        {(function() {
          const n = m.dif.length;
          const difNow = m.dif[n-1], deaNow = m.dea[n-1];
          const difPrev = m.dif[n-2], deaPrev = m.dea[n-2];
          const crossed = difPrev < deaPrev && difNow >= deaNow;
          const deadCross = difPrev > deaPrev && difNow <= deaNow;
          return (
            <SignalCard name="MACD" cn="動能"
              ok={!deadCross && difNow > deaNow}
              warn={deadCross}
              msg={
                crossed    ? `DIF 穿越 DEA ↑ 黃金交叉，多頭啟動 (OSC ${lastMacd.toFixed(2)})` :
                deadCross  ? `DIF 跌破 DEA ↓ 死亡交叉，動能轉空 (OSC ${lastMacd.toFixed(2)})` :
                difNow > deaNow ? `DIF > DEA，多頭續強 (OSC ${lastMacd.toFixed(2)})` :
                               `DIF < DEA，空頭動能 (OSC ${lastMacd.toFixed(2)})`
              }
            />
          );
        })()}
        {/* RSI — 分段評分 */}
        {(function() {
          const div = detectDivergence(data, r);
          return (
            <SignalCard name="RSI(14)" cn="相對強弱"
              ok={lastRsi >= 45 && lastRsi < 70}
              warn={lastRsi >= 75 || lastRsi < 25}
              msg={
                lastRsi >= 75 ? `${lastRsi.toFixed(1)} 超買區，留意拉回` :
                lastRsi >= 65 ? `${lastRsi.toFixed(1)} 偏強，接近超買` :
                lastRsi >= 45 ? `${lastRsi.toFixed(1)} 多頭健康區${div === 'bearish' ? '，但出現頂背離' : ''}` :
                lastRsi >= 30 ? `${lastRsi.toFixed(1)} 弱勢區${div === 'bullish' ? '，底背離出現' : ''}` :
                               `${lastRsi.toFixed(1)} 超賣，留意反彈機會`
              }
            />
          );
        })()}
        {/* MA Alignment — 交叉天數 */}
        {(function() {
          const bull = last5 > last20 && last20 > last60;
          const bear = last5 < last20 && last20 < last60;
          const ma5Cross = ma5[ma5.length-1] > ma20[ma20.length-1];
          const ma5CrossPrev = ma5[ma5.length-2] <= ma20[ma20.length-2];
          const justCrossed = ma5Cross && ma5CrossPrev;
          return (
            <SignalCard name="MA Alignment" cn="均線排列"
              ok={bull}
              msg={
                justCrossed ? 'MA5 剛穿越 MA20，趨勢轉多頭' :
                bull  ? `多頭排列 5>${last5.toFixed(0)} 20>${last20.toFixed(0)} 60>${last60.toFixed(0)}` :
                bear  ? `空頭排列 5<${last5.toFixed(0)} 20<${last20.toFixed(0)} 60<${last60.toFixed(0)}` :
                        '均線糾結，觀望方向確認'
              }
            />
          );
        })()}
        {/* Volume — 加 ATR 波動率 */}
        {(function() {
          const lastAtr = atrArr[atrArr.length - 1];
          const atrPct = lastAtr && last.close > 0 ? (lastAtr / last.close * 100).toFixed(1) : null;
          const volMa5 = data.slice(-5).reduce((a,d)=>a+d.volume,0)/5;
          const volRatio = last.volume / volMa5;
          return (
            <SignalCard name="Volume · ATR" cn="量能波動率"
              ok={volRatio > 1.2}
              msg={`成交量 ${(last.volume/1000).toFixed(1)}K 張 (均量${volRatio.toFixed(1)}x)${atrPct ? `，ATR ${lastAtr.toFixed(1)} (波動${atrPct}%)` : ''}`}
            />
          );
        })()}
        {/* BB 位置 */}
        {(function() {
          const bbUpper = bb.upper[bb.upper.length-1];
          const bbLower = bb.lower[bb.lower.length-1];
          const bbMid   = bb.mid[bb.mid.length-1];
          const pctB = bbUpper && bbLower ? ((last.close - bbLower) / (bbUpper - bbLower) * 100) : null;
          return (
            <SignalCard name="Bollinger Band" cn="布林通道"
              ok={last.close > bbMid && pctB < 85}
              warn={pctB > 90 || pctB < 10}
              msg={
                !pctB ? '資料不足' :
                pctB > 90 ? `%B ${pctB.toFixed(0)}% 觸及上軌，留意拉回` :
                pctB < 10 ? `%B ${pctB.toFixed(0)}% 觸及下軌，留意反彈` :
                last.close > bbMid ? `%B ${pctB.toFixed(0)}%，位於中軌以上 (帶寬${((bbUpper-bbLower)/bbMid*100).toFixed(1)}%)` :
                                     `%B ${pctB.toFixed(0)}%，位於中軌以下 (帶寬${((bbUpper-bbLower)/bbMid*100).toFixed(1)}%)`
              }
            />
          );
        })()}
        {/* Support / Resistance — Pivot Point */}
        <SignalCard
          name="Support / Resistance" cn="支撐壓力"
          ok={last.close > pivot.support}
          msg={`Pivot ${pivot.pivot} · 支撐 ${pivot.support} · 壓力 ${pivot.resistance} (60日高${pivot.high60} 低${pivot.low60})`}
        />
      </div>

      {/* Scoring Matrix */}
      <window.SectionHead
        kicker="Scoring Matrix v1.0"
        title="綜合評分矩陣"
        titleEn="100-Point Score"
        sub="技術面 50分（均線·MACD·RSI·布林·量能）× 基本面 50分（EPS·營收·估值·殖利率·財務體質）"
      />
      <ScoreMatrix100 sc={score100} />

      {/* Institutional flows */}
      <window.SectionHead
        kicker="Institutional Flows"
        title="三大法人籌碼"
        titleEn="Chip Flow"
        sub="5-day net buy/sell · 單位:張"
      />
      <div className="frame-card">
        <span className="corner-tl"/><span className="corner-tr"/><span className="corner-bl"/><span className="corner-br"/>
        <div className="chip-summary">
          <div className="chip-sum-cell">
            <span className="dot" style={{ background:'var(--ink)' }}></span>
            <span className="cs-name kai">外資</span>
            <span className="cs-en italic-en">Foreign</span>
            <span className={`cs-val num ${totalChip.foreign >= 0 ? 'bull-c' : 'bear-c'}`}>
              {totalChip.foreign >= 0 ? '+' : ''}{(totalChip.foreign/1000).toFixed(1)}<small>K張</small>
            </span>
          </div>
          <div className="chip-sum-cell">
            <span className="dot" style={{ background:'var(--gold)' }}></span>
            <span className="cs-name kai">投信</span>
            <span className="cs-en italic-en">Trust</span>
            <span className={`cs-val num ${totalChip.trust >= 0 ? 'bull-c' : 'bear-c'}`}>
              {totalChip.trust >= 0 ? '+' : ''}{(totalChip.trust/1000).toFixed(1)}<small>K張</small>
            </span>
          </div>
          <div className="chip-sum-cell">
            <span className="dot" style={{ background:'var(--mid-tint)' }}></span>
            <span className="cs-name kai">自營</span>
            <span className="cs-en italic-en">Dealer</span>
            <span className={`cs-val num ${totalChip.dealer >= 0 ? 'bull-c' : 'bear-c'}`}>
              {totalChip.dealer >= 0 ? '+' : ''}{(totalChip.dealer/1000).toFixed(1)}<small>K張</small>
            </span>
          </div>
        </div>
        {streak.days >= 2 && (
          <div className="kai" style={{
            margin:'8px 0 0', padding:'6px 14px',
            background: streak.dir > 0 ? 'rgba(74,166,84,0.08)' : 'rgba(229,57,53,0.08)',
            borderLeft: '3px solid ' + (streak.dir > 0 ? 'var(--bull)' : 'var(--bear)'),
            fontSize:12, color: streak.dir > 0 ? 'var(--bull)' : 'var(--bear)',
          }}>
            ⚡ 法人連續 {streak.days} 日{streak.dir > 0 ? '淨買超' : '淨賣超'}，
            {streak.dir > 0 ? '主力同步布局' : '主力同步退場'}
          </div>
        )}
        <ChipBarChart chips={chips} />
      </div>

      {/* Fundamentals */}
      <window.SectionHead
        kicker="Fundamentals"
        title="基本面"
        titleEn="Numbers"
        sub={<span>本益比、淨值比、ROE 與 EPS 趨勢 · <a href={`企業基本面.html?code=${code}`} style={{color:'var(--gold-deep)',fontFamily:'BiauKai,標楷體, serif',fontSize:12}}>→ 完整基本面分析、月營收、季損益、台美關係</a></span>}
      />
      <div className="fund-grid">
        <div className="frame-card fund-stats">
          <span className="corner-tl"/><span className="corner-tr"/><span className="corner-bl"/><span className="corner-br"/>
          <div className="fund-metrics">
            {[
              { label:'P/E Ratio',  value: fund.pe.toFixed(1),                     unit:'',  name:'本益比',    color: '' },
              { label:'P/B Ratio',  value: fund.pb.toFixed(1),                     unit:'',  name:'股價淨值比', color: '' },
              { label:'Div. Yield', value: fund.dy.toFixed(1),                     unit:'%', name:'現金殖利率', color: fund.dy >= 5 ? 'bull-c' : '' },
              { label:'EPS (TTM)',  value: fund.eps.toFixed(2),                    unit:'',  name:'每股盈餘',  color: '' },
              { label:'ROE',        value: fund.roe.toFixed(1),                    unit:'%', name:'股東報酬率', color: fund.roe >= 20 ? 'bull-c' : '' },
              { label:'Rev. YoY',  value: (fund.rev_yoy >= 0 ? '+' : '') + fund.rev_yoy.toFixed(1), unit:'%', name:'營收年增', color: fund.rev_yoy >= 0 ? 'bull-c' : 'bear-c' },
              { label:'EPS YoY',   value: (fund.eps_yoy >= 0 ? '+' : '') + fund.eps_yoy.toFixed(1), unit:'%', name:'盈餘年增', color: fund.eps_yoy >= 0 ? 'bull-c' : 'bear-c' },
            ].map(function(m) {
              return (
                <div key={m.label} className="fund-metric">
                  <div className="fm-label">{m.label}</div>
                  <div className={`fm-value num ${m.color}`}>{m.value}<small>{m.unit}</small></div>
                  <div className="fm-name kai">{m.name}</div>
                </div>
              );
            })}
          </div>
          <div className="kai" style={{ marginTop:8, fontSize:'var(--fs-nano)', color:'var(--ink-mute)', letterSpacing:'.05em' }}>
            {bwibbu ? `估值：TWSE/TPEx 官方（${bwibbu.date || '當日'}）` : '估值：FinMind'}
            {fund.isReal ? ' · EPS/ROE/年增：季報 TTM 計算' : ' · EPS/ROE/年增：估計值（季報載入中）'}
          </div>
        </div>

        <div className="frame-card">
          <span className="corner-tl"/><span className="corner-tr"/><span className="corner-bl"/><span className="corner-br"/>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:8 }}>
            <h4 style={{ fontFamily:'Times New Roman, serif', fontSize:15, fontWeight:700 }}>
              <span className="kai" style={{ letterSpacing:'.12em' }}>每股盈餘</span>
              <span style={{ fontStyle:'italic', color:'var(--ink-mute)', marginLeft:10, fontSize:12 }}>EPS · 8Q</span>
              {!fund.isReal && (
                <span className="kai" style={{ marginLeft:8, fontSize:'var(--fs-nano)', color:'var(--ink-mute)', border:'1px solid var(--paper-line)', padding:'1px 6px' }}>
                  季報載入中
                </span>
              )}
            </h4>
            <span className={`num ${fund.eps_yoy >= 0 ? 'bull-c' : 'bear-c'}`} style={{ fontSize:13 }}>
              YoY {fund.eps_yoy >= 0 ? '+' : ''}{fund.eps_yoy.toFixed(1)}%
            </span>
          </div>
          <EPSBarChart eps={epsHist} />
          {fund.isReal && (
            <div className="kai" style={{ marginTop:6, fontSize:'var(--fs-nano)', color:'var(--ink-mute)', letterSpacing:'.05em' }}>
              柱色：<span style={{color:'var(--bull)'}}>年增</span> / <span style={{color:'var(--bear)'}}>年減</span> · 金虛線：去年同季 EPS · 資料：FinMind 季報
            </div>
          )}
        </div>
      </div>

      {/* Peer comparison */}
      {peers.length > 0 && (
        <>
          <window.SectionHead
            kicker="Peer Comparison"
            title="同業比較"
            titleEn="Peers"
            sub={`vs ${peers.length} comparable companies`}
          />
          <div className="frame-card">
            <span className="corner-tl"/><span className="corner-tr"/><span className="corner-bl"/><span className="corner-br"/>
            <table className="ed-table">
              <thead>
                <tr>
                  <th>Code</th>
                  <th>Name</th>
                  <th className="right">P/E</th>
                  <th className="right">P/B</th>
                  <th className="right">ROE</th>
                  <th className="right">EPS YoY</th>
                  <th className="right">Div Yld</th>
                </tr>
              </thead>
              <tbody>
                <tr style={{ background:'var(--paper)', fontWeight: 700 }}>
                  <td className="code-cell" style={{ color:'var(--gold-deep)' }}>{stock.code}</td>
                  <td className="kai" style={{ color:'var(--gold-deep)' }}>{stock.name} ★</td>
                  <td className="right num-cell">{fund.pe.toFixed(1)}</td>
                  <td className="right num-cell">{fund.pb.toFixed(1)}</td>
                  <td className="right num-cell">{fund.roe.toFixed(1)}%</td>
                  <td className={`right num-cell ${fund.eps_yoy >= 0 ? 'bull-c' : 'bear-c'}`}>
                    {fund.eps_yoy >= 0 ? '+' : ''}{fund.eps_yoy.toFixed(1)}%
                  </td>
                  <td className="right num-cell">{fund.dy.toFixed(1)}%</td>
                </tr>
                {peers.map(p => {
                  var ppe     = safeNum(p.pe, 0);
                  var ppb     = safeNum(p.pb, 0);
                  var proe    = safeNum(p.roe, 0);
                  var pepsyoy = safeNum(p.eps_yoy, 0);
                  var pdy     = safeNum(p.dy, 0);
                  return (
                    <tr key={p.code} onClick={() => { setCode(p.code); window.scrollTo({top:0, behavior:'smooth'}); }}
                        style={{ cursor:'pointer' }}>
                      <td className="code-cell">{p.code}</td>
                      <td className="kai">{p.name}</td>
                      <td className="right num-cell">{ppe.toFixed(1)}</td>
                      <td className="right num-cell">{ppb.toFixed(1)}</td>
                      <td className="right num-cell">{proe.toFixed(1)}%</td>
                      <td className={`right num-cell ${pepsyoy >= 0 ? 'bull-c' : 'bear-c'}`}>
                        {pepsyoy >= 0 ? '+' : ''}{pepsyoy.toFixed(1)}%
                      </td>
                      <td className="right num-cell">{pdy.toFixed(1)}%</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </>
      )}

      {/* AI insight */}
      <window.SectionHead
        kicker="AI Insight"
        title="顧問速讀"
        titleEn="Adviser's Take"
        sub="One-shot summary by 臥龍先生"
      />
      <div className="frame-card ai-insight-card">
        <span className="corner-tl"/><span className="corner-tr"/><span className="corner-bl"/><span className="corner-br"/>
        {!insight && !insightLoading && (
          <div className="ai-cta">
            <div>
              <div className="ai-cta-title">
                <span style={{ fontStyle:'italic' }}>Ask the editor's adviser</span>
                <span className="kai" style={{ marginLeft: 12 }}>請駐欄顧問為這檔個股下一段速讀。</span>
              </div>
              <div className="ai-cta-sub kai">
                臥龍先生會綜合技術面、基本面與評分,給出 150 字以內的觀察與操作方向。
              </div>
            </div>
            <button className="ai-cta-btn" onClick={askAI}>
              <span className="kai">召喚顧問</span>
              <span style={{ marginLeft:6, fontStyle:'italic' }}>summon →</span>
            </button>
          </div>
        )}
        {insightLoading && (
          <div className="ai-loading kai">
            臥龍先生正在批閱 K 線<span className="dots"><span>.</span><span>.</span><span>.</span></span>
          </div>
        )}
        {insight && (
          <div className="ai-result">
            <div className="ai-quote-mark">"</div>
            <div className="ai-text kai">{insight}</div>
            <div className="ai-sig italic-en">— 諸葛亮 Kongming · Strategist Royal · {new Date().toLocaleString('en-US', {hour12:false})}</div>
            <button className="ai-rerun" onClick={askAI}>↻ rerun</button>
          </div>
        )}
      </div>

      </div>} {/* end tech tab */}

      <window.FooterBar />
    </div>
  );
}

function SignalCard({ name, cn, ok, warn, msg }) {
  const dotColor = warn ? 'var(--gold)' : ok ? 'var(--bull)' : 'var(--bear)';
  return (
    <div className="signal-card">
      <div className="sig-head">
        <span className="sig-dot" style={{ background: dotColor }}/>
        <span className="sig-name">{name}</span>
        <span className="sig-cn kai">{cn}</span>
      </div>
      <div className="sig-msg kai">{msg}</div>
    </div>
  );
}

// 供其他頁面重用五力評分引擎，避免複製一份走樣的評分邏輯
window.calcScore100   = calcScore100;
window.ScoreMatrix100 = ScoreMatrix100;
window.smaIndicator   = sma;
window.rsiIndicator   = rsi;
window.macdIndicator  = macd;
window.bollingerBand  = bollinger;

const _stockRoot = document.getElementById('root');
if (_stockRoot) ReactDOM.createRoot(_stockRoot).render(<App />);
