// ═══════════════════════════════════════════════════════════ // companyintel.js — 公司研究資料:管理層、內部人交易、新聞、產業鏈入口 // ═══════════════════════════════════════════════════════════ const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36'; const SEC_UA = 'EmmyInvestDashboard/1.0 (personal learning tool; contact@example.com)'; async function text(url, headers = {}, ms = 12000) { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), ms); try { const res = await fetch(url, { headers: { 'User-Agent': UA, ...headers }, signal: ctrl.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.text(); } finally { clearTimeout(timer); } } async function json(url, headers = {}, ms = 12000) { return JSON.parse(await text(url, { Accept: 'application/json,text/plain,*/*', ...headers }, ms)); } const strip = (s) => String(s || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); const num = (s) => { if (s == null) return null; const n = Number(String(s).replace(/[$,%\s,]/g, '')); return Number.isFinite(n) ? n : null; }; const tag = (src, name) => src.match(new RegExp(`<${name}>([\\s\\S]*?)<\\/${name}>`, 'i'))?.[1]?.trim() || null; let _tickerMap = null; async function tickerToCik(symbol) { if (!_tickerMap) { const d = await json('https://www.sec.gov/files/company_tickers.json', { 'User-Agent': SEC_UA }); _tickerMap = {}; for (const k of Object.keys(d)) _tickerMap[String(d[k].ticker).toUpperCase()] = { cik: String(d[k].cik_str).padStart(10, '0'), name: d[k].title }; } return _tickerMap[symbol] || null; } let _auth = { cookie: null, crumb: null, at: 0 }; async function yahooAuth() { if (_auth.crumb && Date.now() - _auth.at < 3600e3) return _auth; const r1 = await fetch('https://fc.yahoo.com/', { headers: { 'User-Agent': UA } }).catch(() => null); const cookie = (r1 && (r1.headers.get('set-cookie') || '')).split(';')[0] || ''; const r2 = await fetch('https://query2.finance.yahoo.com/v1/test/getcrumb', { headers: { 'User-Agent': UA, Cookie: cookie } }); const crumb = (await r2.text()).trim(); if (!crumb || crumb.includes('<')) throw new Error('無法取得 Yahoo crumb'); _auth = { cookie, crumb, at: Date.now() }; return _auth; } async function fetchManagement(symbol) { try { const { cookie, crumb } = await yahooAuth(); const d = await json(`https://query2.finance.yahoo.com/v10/finance/quoteSummary/${encodeURIComponent(symbol)}?modules=assetProfile&crumb=${encodeURIComponent(crumb)}`, { Cookie: cookie }); const p = d?.quoteSummary?.result?.[0]?.assetProfile || {}; return { sector: p.sector || null, industry: p.industry || null, website: p.website || null, fullTimeEmployees: p.fullTimeEmployees ?? null, officers: (p.companyOfficers || []).slice(0, 10).map(o => ({ name: o.name || '', title: o.title || '', age: o.age ?? null, fiscalYear: o.fiscalYear ?? null, totalPay: o.totalPay?.raw ?? null, })), source: 'Yahoo assetProfile', }; } catch { return { officers: [], source: null }; } } function parseForm4(txt, filing) { const xml = txt.slice(txt.indexOf('([\s\S]*?)<\/reportingOwner>/i)?.[1] || ''; const issuerBlock = xml.match(/([\s\S]*?)<\/issuer>/i)?.[1] || ''; const relBlock = ownerBlock.match(/([\s\S]*?)<\/reportingOwnerRelationship>/i)?.[1] || ''; const txBlocks = [...xml.matchAll(/([\s\S]*?)<\/nonDerivativeTransaction>/gi)].map(m => m[1]); const transactions = txBlocks.slice(0, 8).map(b => ({ date: tag(b, 'transactionDate') ? tag(tag(b, 'transactionDate'), 'value') : null, code: tag(b, 'transactionCode') || null, acquiredDisposed: tag(tag(b, 'transactionAcquiredDisposedCode') || '', 'value'), shares: num(tag(tag(b, 'transactionShares') || '', 'value')), price: num(tag(tag(b, 'transactionPricePerShare') || '', 'value')), ownedAfter: num(tag(tag(b, 'sharesOwnedFollowingTransaction') || '', 'value')), })).filter(t => t.shares != null || t.code); const acquired = transactions.filter(t => t.acquiredDisposed === 'A').reduce((a, t) => a + (t.shares || 0), 0); const disposed = transactions.filter(t => t.acquiredDisposed === 'D').reduce((a, t) => a + (t.shares || 0), 0); return { filingDate: filing.date, reportDate: tag(xml, 'periodOfReport'), owner: tag(ownerBlock, 'rptOwnerName') || strip(txt.match(/COMPANY CONFORMED NAME:\s*([^\n]+)/)?.[1]), issuer: tag(issuerBlock, 'issuerName'), title: tag(relBlock, 'officerTitle') || (tag(relBlock, 'isDirector') === '1' ? 'Director' : ''), isDirector: tag(relBlock, 'isDirector') === '1', isOfficer: tag(relBlock, 'isOfficer') === '1', acquired, disposed, signal: acquired > disposed ? 'acquire' : disposed > acquired ? 'dispose' : 'mixed', transactions, url: filing.url, }; } async function fetchInsiderTransactions(symbol) { const hit = await tickerToCik(symbol); if (!hit) return []; const sub = await json(`https://data.sec.gov/submissions/CIK${hit.cik}.json`, { 'User-Agent': SEC_UA }); const f = sub.filings?.recent || {}; const filings = []; for (let i = 0; i < (f.form || []).length && filings.length < 8; i++) { if (f.form[i] !== '4') continue; const accn = f.accessionNumber[i]; const accNo = accn.replace(/-/g, ''); filings.push({ date: f.filingDate[i], accn, url: `https://www.sec.gov/Archives/edgar/data/${Number(hit.cik)}/${accNo}/${accn}.txt`, }); } const out = []; for (const filing of filings.slice(0, 5)) { try { out.push(parseForm4(await text(filing.url, { 'User-Agent': SEC_UA }), filing)); } catch { /* keep going */ } } return out; } async function fetchNews(symbol) { const d = await json(`https://api.nasdaq.com/api/news/topic/articlebysymbol?q=${encodeURIComponent(symbol)}|stocks&offset=0&limit=8&fallback=true`, { Accept: 'application/json', Origin: 'https://www.nasdaq.com', Referer: 'https://www.nasdaq.com/', }).catch(() => null); return (d?.data?.rows || []).slice(0, 8).map(r => ({ title: r.title, publisher: r.publisher, created: r.created || r.ago, description: strip(r.description || ''), url: r.url ? (r.url.startsWith('http') ? r.url : `https://www.nasdaq.com${r.url}`) : null, relatedSymbols: (r.related_symbols || []).map(x => String(x).split('|')[0].toUpperCase()).filter(Boolean), })); } function industryChain(symbol, profile = {}) { const industry = `${profile.industry || ''} ${profile.sector || ''}`.toLowerCase(); const maps = [ { match: /semiconductor|chip|accelerated|technology/, upstream: ['EDA/IP 軟體', '晶圓代工', '先進封裝', 'HBM/記憶體', '半導體設備', 'ABF/載板'], peers: ['AMD', 'AVGO', 'QCOM', 'MRVL', 'TSM', 'ASML', 'MU'], downstream: ['雲端資料中心', 'AI 伺服器 OEM/ODM', '企業 AI 軟體', '自駕車/機器人', '遊戲與工作站'], }, { match: /software|internet|communication|media/, upstream: ['雲端基礎設施', '資料中心', '廣告技術', '內容/資料供應商'], peers: ['MSFT', 'GOOGL', 'META', 'AMZN', 'CRM', 'ORCL'], downstream: ['企業客戶', '消費者流量', '開發者生態', '廣告主'], }, { match: /consumer|retail|apparel/, upstream: ['原物料', '製造代工', '物流倉儲', '通路平台'], peers: ['AMZN', 'WMT', 'COST', 'TGT', 'NKE'], downstream: ['消費者', '會員訂閱', '門市/電商通路'], }, ]; const hit = maps.find(m => m.match.test(industry)) || { upstream: ['原物料/零組件', '設備與服務供應商', '物流與通路', '資本支出供應商'], peers: [], downstream: ['終端客戶', '企業採購', '通路夥伴', '替代產品'], }; const q = encodeURIComponent(`${symbol} suppliers customers upstream downstream competitors`); return { upstream: hit.upstream, peers: hit.peers.filter(s => s !== symbol), downstream: hit.downstream, searches: [ { label: '供應商 / 客戶', url: `https://www.google.com/search?q=${q}` }, { label: '10-K supply chain', url: `https://www.google.com/search?q=${encodeURIComponent(`${symbol} 10-K suppliers customers supply chain`)}` }, { label: '同業比較', url: `https://www.google.com/search?q=${encodeURIComponent(`${symbol} competitors industry peers`)}` }, ], }; } export async function getCompanyIntel(symbol, profile = {}) { symbol = String(symbol || '').trim().toUpperCase(); const [management, insiders, news] = await Promise.all([ fetchManagement(symbol), fetchInsiderTransactions(symbol).catch(() => []), fetchNews(symbol).catch(() => []), ]); return { symbol, updatedAt: new Date().toISOString(), management: { ...management, searches: [ { label: '管理層 / Leadership', url: `https://www.google.com/search?q=${encodeURIComponent(`${symbol} executive officers management leadership`)}` }, { label: 'Proxy / DEF 14A', url: `https://www.google.com/search?q=${encodeURIComponent(`${symbol} DEF 14A executive compensation board directors`)}` }, { label: 'Investor relations', url: `https://www.google.com/search?q=${encodeURIComponent(`${symbol} investor relations leadership`)}` }, ], }, insiders, news, industryChain: industryChain(symbol, { ...profile, ...management }), sources: ['Yahoo assetProfile', 'SEC Form 4', 'Nasdaq News'], }; }