const { useState, useRef, useEffect } = React; const NO_CFD_RULE = "NEVER recommend CFDs, leveraged products, spread bets, derivatives, or leveraged ETFs. Stocks and plain ETFs only. No exceptions."; const STORAGE_KEY = "pielot_watchlist_v1"; const QUESTIONS = [ { id: "horizon", question: "When do you plan to use this money?", options: [ { label: "Less than 1 year", value: "very_short", risk: 4 }, { label: "1–3 years", value: "short", risk: 3 }, { label: "3–7 years", value: "medium", risk: 2 }, { label: "7+ years", value: "long", risk: 1 }, ]}, { id: "reaction", question: "Your portfolio drops 20% in a month. You...", options: [ { label: "Sell everything immediately", value: "panic", risk: 4 }, { label: "Lose sleep but hold", value: "worried", risk: 3 }, { label: "Hold and wait it out", value: "calm", risk: 2 }, { label: "Buy more - it's on sale", value: "greedy", risk: 1 }, ]}, { id: "goal", question: "What's your main investing goal?", options: [ { label: "Protect my savings from inflation", value: "preserve", risk: 1 }, { label: "Slow and steady wealth building", value: "grow_slow", risk: 2 }, { label: "Aggressive growth", value: "grow_fast", risk: 3 }, { label: "High risk, high reward plays", value: "speculate", risk: 4 }, ]}, { id: "knowledge", question: "How would you describe your investing knowledge?", options: [ { label: "Total beginner", value: "beginner", risk: 1 }, { label: "I know the basics", value: "basic", risk: 2 }, { label: "Comfortable with markets", value: "intermediate", risk: 3 }, { label: "I read earnings reports for fun", value: "advanced", risk: 4 }, ]}, { id: "amount", question: "What portion of your savings is this?", options: [ { label: "Just a small experiment", value: "tiny", risk: 4 }, { label: "About 10-25%", value: "small", risk: 3 }, { label: "About 25-50%", value: "medium", risk: 2 }, { label: "Most of my investable savings", value: "large", risk: 1 }, ]}, ]; const STRATEGIES = { long_term_low_risk: { label: "Long-Term - Low Risk", color: "#4ade80", glow: "rgba(74,222,128,0.25)", desc: "Steady compounding over years. ETF-heavy, globally diversified, boring-but-powerful." }, long_term_high_risk: { label: "Long-Term - High Risk", color: "#60a5fa", glow: "rgba(96,165,250,0.25)", desc: "Thematic ETFs, growth stocks, emerging markets. Years of conviction required." }, short_term_high_risk: { label: "Short-Term - High Risk", color: "#f59e0b", glow: "rgba(245,158,11,0.25)", desc: "Momentum plays, earnings season picks, news-driven moves. Active and exciting." }, balanced: { label: "Balanced - All-Weather", color: "#a78bfa", glow: "rgba(167,139,250,0.25)", desc: "Sleep-well-at-night portfolio. Mix of growth, stability, and hedges." }, }; const SLICE_COLORS = [ { hex: "#4ade80", rgb: "74,222,128" }, { hex: "#60a5fa", rgb: "96,165,250" }, { hex: "#f59e0b", rgb: "245,158,11" }, { hex: "#a78bfa", rgb: "167,139,250" }, { hex: "#f87171", rgb: "248,113,113" }, { hex: "#34d399", rgb: "52,211,153" }, { hex: "#818cf8", rgb: "129,140,248" }, { hex: "#fb923c", rgb: "251,146,60" }, ]; const SENT_COLORS = { bullish: { color: "#4ade80", rgb: "74,222,128" }, bearish: { color: "#f87171", rgb: "248,113,113" }, neutral: { color: "#f59e0b", rgb: "245,158,11" }, }; function scoreToStrategy(s) { if (s <= 8) return "long_term_low_risk"; if (s <= 12) return "balanced"; if (s <= 16) return "long_term_high_risk"; return "short_term_high_risk"; } // Storage function loadWL() { try { const r = localStorage.getItem(STORAGE_KEY); return r ? JSON.parse(r) : []; } catch { return []; } } function saveWL(l) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(l)); } catch {} } const PIES_KEY = "pielot_pies_v1"; function loadPies() { try { const r = localStorage.getItem(PIES_KEY); return r ? JSON.parse(r) : []; } catch { return []; } } function savePies(l) { try { localStorage.setItem(PIES_KEY, JSON.stringify(l)); } catch {} } function newPieId() { return "pie_" + Date.now(); } // ─── Ticker Database ────────────────────────────────────────────────────────── // Compact format: [ticker, name, type, sector] // type: "S"=Stock, "E"=ETF const RAW_TICKERS = [ // ── Mega/Large Cap US Stocks ── ["AAPL","Apple Inc","S","Tech"],["MSFT","Microsoft Corp","S","Tech"],["NVDA","NVIDIA Corp","S","Tech"], ["AMZN","Amazon.com Inc","S","Consumer"],["GOOGL","Alphabet Inc (Class A)","S","Tech"],["GOOG","Alphabet Inc (Class C)","S","Tech"], ["META","Meta Platforms Inc","S","Tech"],["TSLA","Tesla Inc","S","Auto/Tech"],["BRK.B","Berkshire Hathaway B","S","Finance"], ["UNH","UnitedHealth Group","S","Healthcare"],["JPM","JPMorgan Chase","S","Finance"],["V","Visa Inc","S","Finance"], ["XOM","ExxonMobil Corp","S","Energy"],["JNJ","Johnson & Johnson","S","Healthcare"],["WMT","Walmart Inc","S","Retail"], ["MA","Mastercard Inc","S","Finance"],["PG","Procter & Gamble","S","Consumer"],["HD","Home Depot Inc","S","Retail"], ["CVX","Chevron Corp","S","Energy"],["MRK","Merck & Co","S","Healthcare"],["ABBV","AbbVie Inc","S","Healthcare"], ["COST","Costco Wholesale","S","Retail"],["AVGO","Broadcom Inc","S","Tech"],["PEP","PepsiCo Inc","S","Consumer"], ["KO","Coca-Cola Co","S","Consumer"],["BAC","Bank of America","S","Finance"],["LLY","Eli Lilly and Co","S","Healthcare"], ["PFE","Pfizer Inc","S","Healthcare"],["TMO","Thermo Fisher Scientific","S","Healthcare"],["CSCO","Cisco Systems","S","Tech"], ["MCD","McDonald's Corp","S","Consumer"],["ACN","Accenture plc","S","Tech"],["ABT","Abbott Laboratories","S","Healthcare"], ["CRM","Salesforce Inc","S","Tech"],["DHR","Danaher Corp","S","Healthcare"],["NKE","Nike Inc","S","Consumer"], ["ADBE","Adobe Inc","S","Tech"],["TXN","Texas Instruments","S","Tech"],["LIN","Linde plc","S","Materials"], ["NEE","NextEra Energy","S","Utilities"],["PM","Philip Morris Intl","S","Consumer"],["RTX","RTX Corp","S","Aerospace"], ["HON","Honeywell Intl","S","Industrial"],["QCOM","Qualcomm Inc","S","Tech"],["UNP","Union Pacific Corp","S","Industrial"], ["AMGN","Amgen Inc","S","Healthcare"],["BMY","Bristol-Myers Squibb","S","Healthcare"],["SBUX","Starbucks Corp","S","Consumer"], ["IBM","IBM Corp","S","Tech"],["GE","GE Aerospace","S","Industrial"],["CAT","Caterpillar Inc","S","Industrial"], ["INTC","Intel Corp","S","Tech"],["LOW","Lowe's Companies","S","Retail"],["INTU","Intuit Inc","S","Tech"], ["DE","Deere & Company","S","Industrial"],["SPGI","S&P Global Inc","S","Finance"],["AMAT","Applied Materials","S","Tech"], ["MS","Morgan Stanley","S","Finance"],["GS","Goldman Sachs","S","Finance"],["BLK","BlackRock Inc","S","Finance"], ["AXP","American Express","S","Finance"],["MDT","Medtronic plc","S","Healthcare"],["GILD","Gilead Sciences","S","Healthcare"], ["TGT","Target Corp","S","Retail"],["ISRG","Intuitive Surgical","S","Healthcare"],["ADI","Analog Devices","S","Tech"], ["VRTX","Vertex Pharmaceuticals","S","Healthcare"],["REGN","Regeneron Pharmaceuticals","S","Healthcare"], ["MMC","Marsh & McLennan","S","Finance"],["SYK","Stryker Corp","S","Healthcare"],["LRCX","Lam Research","S","Tech"], ["MU","Micron Technology","S","Tech"],["ZTS","Zoetis Inc","S","Healthcare"],["CI","Cigna Group","S","Healthcare"], ["TJX","TJX Companies","S","Retail"],["PLD","Prologis Inc","S","Real Estate"],["AMT","American Tower Corp","S","Real Estate"], ["CB","Chubb Ltd","S","Finance"],["CL","Colgate-Palmolive","S","Consumer"],["DUK","Duke Energy","S","Utilities"], ["SO","Southern Co","S","Utilities"],["WM","Waste Management Inc","S","Industrial"],["ICE","Intercontinental Exchange","S","Finance"], ["CME","CME Group Inc","S","Finance"],["AON","Aon plc","S","Finance"],["PNC","PNC Financial Services","S","Finance"], ["USB","US Bancorp","S","Finance"],["TFC","Truist Financial","S","Finance"],["COF","Capital One Financial","S","Finance"], ["AIG","American Intl Group","S","Finance"],["MET","MetLife Inc","S","Finance"],["PRU","Prudential Financial","S","Finance"], ["HCA","HCA Healthcare","S","Healthcare"],["ELV","Elevance Health","S","Healthcare"],["CVS","CVS Health Corp","S","Healthcare"], ["MCK","McKesson Corp","S","Healthcare"],["AMP","Ameriprise Financial","S","Finance"],["ITW","Illinois Tool Works","S","Industrial"], ["ETN","Eaton Corp","S","Industrial"],["EMR","Emerson Electric","S","Industrial"],["PH","Parker-Hannifin","S","Industrial"], ["ROK","Rockwell Automation","S","Industrial"],["DOV","Dover Corp","S","Industrial"],["AME","AMETEK Inc","S","Industrial"], // ── Tech / Growth ── ["NFLX","Netflix Inc","S","Tech"],["AMD","Advanced Micro Devices","S","Tech"],["PYPL","PayPal Holdings","S","Fintech"], ["SQ","Block Inc","S","Fintech"],["SHOP","Shopify Inc","S","Tech"],["SNOW","Snowflake Inc","S","Tech"], ["UBER","Uber Technologies","S","Tech"],["LYFT","Lyft Inc","S","Tech"],["ABNB","Airbnb Inc","S","Tech"], ["DASH","DoorDash Inc","S","Tech"],["COIN","Coinbase Global","S","Fintech"],["RBLX","Roblox Corp","S","Tech"], ["PLTR","Palantir Technologies","S","Tech"],["SOFI","SoFi Technologies","S","Fintech"],["HOOD","Robinhood Markets","S","Fintech"], ["RIVN","Rivian Automotive","S","Auto/Tech"],["LCID","Lucid Group","S","Auto/Tech"],["RKLB","Rocket Lab USA","S","Aerospace"], ["SPCE","Virgin Galactic","S","Aerospace"],["MNTS","Momentus Inc","S","Aerospace"],["RDW","Redwire Corp","S","Aerospace"], ["ASTR","Astra Space Inc","S","Aerospace"],["ASTS","AST SpaceMobile","S","Tech"],["SATL","Satellogic Inc","S","Tech"], ["NET","Cloudflare Inc","S","Tech"],["ZS","Zscaler Inc","S","Tech"],["CRWD","CrowdStrike Holdings","S","Tech"], ["OKTA","Okta Inc","S","Tech"],["DDOG","Datadog Inc","S","Tech"],["MDB","MongoDB Inc","S","Tech"], ["TTD","The Trade Desk","S","Tech"],["TWLO","Twilio Inc","S","Tech"],["U","Unity Software","S","Tech"], ["RGEN","Repligen Corp","S","Healthcare"],["VEEV","Veeva Systems","S","Tech"],["HUBS","HubSpot Inc","S","Tech"], ["ZM","Zoom Video Comm","S","Tech"],["DOCU","DocuSign Inc","S","Tech"],["BILL","Bill Holdings","S","Tech"], ["APP","AppLovin Corp","S","Tech"],["MSTR","MicroStrategy Inc","S","Tech"],["ARM","Arm Holdings","S","Tech"], ["SMCI","Super Micro Computer","S","Tech"],["DELL","Dell Technologies","S","Tech"],["HPQ","HP Inc","S","Tech"], ["HPE","Hewlett Packard Enterprise","S","Tech"],["ORCL","Oracle Corp","S","Tech"],["SAP","SAP SE","S","Tech"], // ── Semiconductors ── ["ASML","ASML Holding NV","S","Semis"],["TSM","Taiwan Semiconductor","S","Semis"],["MRVL","Marvell Technology","S","Semis"], ["ON","ON Semiconductor","S","Semis"],["STM","STMicroelectronics","S","Semis"],["MPWR","Monolithic Power Systems","S","Semis"], ["WOLF","Wolfspeed Inc","S","Semis"],["SWKS","Skyworks Solutions","S","Semis"],["QRVO","Qorvo Inc","S","Semis"], ["KLAC","KLA Corp","S","Semis"],["MCHP","Microchip Technology","S","Semis"],["TER","Teradyne Inc","S","Semis"], // ── Chinese / Asian Stocks ── ["BABA","Alibaba Group","S","Tech"],["BIDU","Baidu Inc","S","Tech"],["JD","JD.com Inc","S","Retail"], ["PDD","PDD Holdings","S","Retail"],["NIO","NIO Inc","S","Auto"],["XPEV","XPeng Inc","S","Auto"], ["LI","Li Auto Inc","S","Auto"],["TCEHY","Tencent Holdings","S","Tech"],["SE","Sea Ltd","S","Tech"], ["GRAB","Grab Holdings","S","Tech"],["GOTO","GoTo Group","S","Tech"],["SONY","Sony Group Corp","S","Tech"], ["TM","Toyota Motor Corp","S","Auto"],["HMC","Honda Motor Co","S","Auto"],["NVS","Novartis AG","S","Healthcare"], ["RHHBY","Roche Holding AG","S","Healthcare"],["NESNY","Nestlé SA","S","Consumer"],["LVMUY","LVMH","S","Luxury"], ["IDEXY","Industria de Diseno","S","Retail"],["SAB","Sabadell","S","Finance"], // ── Energy ── ["SLB","SLB (Schlumberger)","S","Energy"],["HAL","Halliburton Co","S","Energy"],["BKR","Baker Hughes","S","Energy"], ["OXY","Occidental Petroleum","S","Energy"],["COP","ConocoPhillips","S","Energy"],["EOG","EOG Resources","S","Energy"], ["PSX","Phillips 66","S","Energy"],["VLO","Valero Energy","S","Energy"],["MPC","Marathon Petroleum","S","Energy"], ["DVN","Devon Energy","S","Energy"],["FANG","Diamondback Energy","S","Energy"],["APA","APA Corp","S","Energy"], ["ENPH","Enphase Energy","S","Clean Energy"],["SEDG","SolarEdge Technologies","S","Clean Energy"], ["RUN","Sunrun Inc","S","Clean Energy"],["PLUG","Plug Power Inc","S","Clean Energy"],["FSLR","First Solar Inc","S","Clean Energy"], ["BE","Bloom Energy","S","Clean Energy"],["NEP","NextEra Energy Partners","S","Clean Energy"], // ── Finance / Fintech ── ["SCHW","Charles Schwab","S","Finance"],["STT","State Street Corp","S","Finance"],["NTRS","Northern Trust","S","Finance"], ["TROW","T. Rowe Price Group","S","Finance"],["BEN","Franklin Resources","S","Finance"],["IVZ","Invesco Ltd","S","Finance"], ["WFC","Wells Fargo","S","Finance"],["C","Citigroup Inc","S","Finance"],["DB","Deutsche Bank","S","Finance"], ["HSBC","HSBC Holdings","S","Finance"],["BCS","Barclays plc","S","Finance"],["UBS","UBS Group AG","S","Finance"], ["CS","Credit Suisse","S","Finance"],["ING","ING Groep NV","S","Finance"],["SAN","Banco Santander","S","Finance"], ["BBVA","BBVA","S","Finance"],["BNPQY","BNP Paribas","S","Finance"],["AXA","AXA SA","S","Finance"], // ── Consumer / Retail ── ["AMZN","Amazon.com Inc","S","Retail"],["ETSY","Etsy Inc","S","Retail"],["EBAY","eBay Inc","S","Retail"], ["W","Wayfair Inc","S","Retail"],["RH","RH (Restoration Hardware)","S","Retail"],["LULU","Lululemon Athletica","S","Consumer"], ["GPS","Gap Inc","S","Retail"],["URBN","Urban Outfitters","S","Retail"],["ANF","Abercrombie & Fitch","S","Retail"], ["DG","Dollar General","S","Retail"],["DLTR","Dollar Tree Inc","S","Retail"],["KR","Kroger Co","S","Retail"], ["SFM","Sprouts Farmers Market","S","Retail"],["YUM","Yum! Brands","S","Consumer"],["QSR","Restaurant Brands Intl","S","Consumer"], ["CMG","Chipotle Mexican Grill","S","Consumer"],["DPZ","Domino's Pizza","S","Consumer"],["WING","Wingstop Inc","S","Consumer"], ["DKNG","DraftKings Inc","S","Entertainment"],["MGM","MGM Resorts Intl","S","Entertainment"], ["LVS","Las Vegas Sands","S","Entertainment"],["WYNN","Wynn Resorts","S","Entertainment"],["CZR","Caesars Entertainment","S","Entertainment"], // ── Healthcare / Biotech ── ["MRNA","Moderna Inc","S","Biotech"],["BNTX","BioNTech SE","S","Biotech"],["BIIB","Biogen Inc","S","Biotech"], ["ILMN","Illumina Inc","S","Biotech"],["IDXX","Idexx Laboratories","S","Healthcare"],["MTD","Mettler-Toledo Intl","S","Healthcare"], ["A","Agilent Technologies","S","Healthcare"],["WAT","Waters Corp","S","Healthcare"],["TECH","Bio-Techne Corp","S","Healthcare"], ["DXCM","DexCom Inc","S","Healthcare"],["PODD","Insulet Corp","S","Healthcare"],["ALGN","Align Technology","S","Healthcare"], ["EW","Edwards Lifesciences","S","Healthcare"],["BSX","Boston Scientific","S","Healthcare"],["BDX","Becton Dickinson","S","Healthcare"], ["ZBH","Zimmer Biomet","S","Healthcare"],["HOLX","Hologic Inc","S","Healthcare"],["NTRA","Natera Inc","S","Biotech"], ["EXAS","Exact Sciences","S","Biotech"],["PACB","Pacific Biosciences","S","Biotech"], // ── Crypto-adjacent / Meme ── ["GME","GameStop Corp","S","Retail"],["AMC","AMC Entertainment","S","Entertainment"],["BBBY","Bed Bath & Beyond","S","Retail"], ["BB","BlackBerry Ltd","S","Tech"],["NOK","Nokia Corp","S","Tech"],["SNDL","SNDL Inc","S","Cannabis"], ["TLRY","Tilray Brands","S","Cannabis"],["CGC","Canopy Growth Corp","S","Cannabis"], // ── Broad Market ETFs ── ["SPY","SPDR S&P 500 ETF","E","Broad Market"],["VOO","Vanguard S&P 500 ETF","E","Broad Market"], ["IVV","iShares Core S&P 500 ETF","E","Broad Market"],["VTI","Vanguard Total Stock Market ETF","E","Broad Market"], ["ITOT","iShares Core S&P Total US ETF","E","Broad Market"],["SCHB","Schwab US Broad Market ETF","E","Broad Market"], ["VT","Vanguard Total World Stock ETF","E","Global"],["ACWI","iShares MSCI ACWI ETF","E","Global"], ["VXUS","Vanguard Total Intl Stock ETF","E","International"],["VEA","Vanguard FTSE Dev Markets ETF","E","International"], ["VWO","Vanguard FTSE Emerging Markets ETF","E","Emerging Markets"],["IEMG","iShares Core MSCI Emerging Markets ETF","E","Emerging Markets"], ["EEM","iShares MSCI Emerging Markets ETF","E","Emerging Markets"],["EFA","iShares MSCI EAFE ETF","E","International"], ["VWCE","Vanguard FTSE All-World UCITS ETF","E","Global"],["IWDA","iShares Core MSCI World UCITS ETF","E","Global"], ["CSPX","iShares Core S&P 500 UCITS ETF","E","Broad Market"],["EIMI","iShares Core MSCI EM IMI UCITS ETF","E","Emerging Markets"], ["SWRD","SPDR MSCI World UCITS ETF","E","Global"],["VGVF","Vanguard ESG Global All Cap UCITS ETF","E","ESG"], // ── Sector ETFs ── ["QQQ","Invesco QQQ Trust (Nasdaq 100)","E","Tech"],["QQQM","Invesco Nasdaq 100 ETF","E","Tech"], ["XLK","Technology Select Sector SPDR","E","Tech"],["XLF","Financial Select Sector SPDR","E","Finance"], ["XLE","Energy Select Sector SPDR","E","Energy"],["XLV","Health Care Select Sector SPDR","E","Healthcare"], ["XLI","Industrial Select Sector SPDR","E","Industrial"],["XLY","Consumer Discret Select Sector SPDR","E","Consumer"], ["XLP","Consumer Staples Select Sector SPDR","E","Consumer"],["XLU","Utilities Select Sector SPDR","E","Utilities"], ["XLRE","Real Estate Select Sector SPDR","E","Real Estate"],["XLB","Materials Select Sector SPDR","E","Materials"], ["XLC","Communication Services Select Sector SPDR","E","Comms"],["SOXX","iShares Semiconductor ETF","E","Semis"], ["SMH","VanEck Semiconductor ETF","E","Semis"],["AIQ","Global X AI & Technology ETF","E","AI/Tech"], ["BOTZ","Global X Robotics & AI ETF","E","AI/Tech"],["ROBO","Robo Global Robotics ETF","E","AI/Tech"], ["HACK","ETFMG Prime Cyber Security ETF","E","Cybersecurity"],["BUG","Global X Cybersecurity ETF","E","Cybersecurity"], ["CLOUD","Global X Cloud Computing ETF","E","Tech"],["WCLD","WisdomTree Cloud Computing ETF","E","Tech"], ["FINX","Global X FinTech ETF","E","Fintech"],["KURE","KraneShares MSCI All China Health Care ETF","E","Healthcare"], ["ARKG","ARK Genomic Revolution ETF","E","Biotech"],["ARKK","ARK Innovation ETF","E","Growth"], ["ARKW","ARK Next Generation Internet ETF","E","Tech"],["ARKF","ARK Fintech Innovation ETF","E","Fintech"], ["ARKQ","ARK Autonomous Technology ETF","E","Tech"],["ARKX","ARK Space Exploration ETF","E","Aerospace"], ["ICLN","iShares Global Clean Energy ETF","E","Clean Energy"],["QCLN","First Trust NASDAQ Clean Edge ETF","E","Clean Energy"], ["TAN","Invesco Solar ETF","E","Clean Energy"],["FAN","First Trust Global Wind Energy ETF","E","Clean Energy"], ["VNQ","Vanguard Real Estate ETF","E","Real Estate"],["SCHH","Schwab US REIT ETF","E","Real Estate"], ["IYR","iShares US Real Estate ETF","E","Real Estate"],["AMT","American Tower Corp","S","Real Estate"], // ── Dividend / Income ETFs ── ["VYM","Vanguard High Dividend Yield ETF","E","Dividend"],["DVY","iShares Select Dividend ETF","E","Dividend"], ["SCHD","Schwab US Dividend Equity ETF","E","Dividend"],["NOBL","ProShares S&P 500 Div Aristocrats ETF","E","Dividend"], ["JEPI","JPMorgan Equity Premium Income ETF","E","Income"],["JEPQ","JPMorgan Nasdaq Equity Premium Income ETF","E","Income"], ["SPHD","Invesco S&P 500 High Dividend Low Vol ETF","E","Dividend"],["HDV","iShares Core High Dividend ETF","E","Dividend"], ["SDY","SPDR S&P Dividend ETF","E","Dividend"],["VIG","Vanguard Dividend Appreciation ETF","E","Dividend"], ["DGRO","iShares Core Dividend Growth ETF","E","Dividend"], // ── Factor / Smart Beta ETFs ── ["QUAL","iShares MSCI USA Quality Factor ETF","E","Factor"],["MTUM","iShares MSCI USA Momentum Factor ETF","E","Factor"], ["USMV","iShares MSCI USA Min Vol Factor ETF","E","Factor"],["VLUE","iShares MSCI USA Value Factor ETF","E","Factor"], ["EFAV","iShares MSCI EAFE Min Vol Factor ETF","E","Factor"], // ── Bond ETFs ── ["AGG","iShares Core US Aggregate Bond ETF","E","Bonds"],["BND","Vanguard Total Bond Market ETF","E","Bonds"], ["TLT","iShares 20+ Year Treasury Bond ETF","E","Bonds"],["IEF","iShares 7-10 Year Treasury Bond ETF","E","Bonds"], ["SHY","iShares 1-3 Year Treasury Bond ETF","E","Bonds"],["LQD","iShares iBoxx $ Investment Grade Corp Bond ETF","E","Bonds"], ["HYG","iShares iBoxx $ High Yield Corp Bond ETF","E","Bonds"],["JNK","SPDR Bloomberg High Yield Bond ETF","E","Bonds"], ["BNDX","Vanguard Total Intl Bond ETF","E","Bonds"],["EMB","iShares JP Morgan USD Emerging Markets Bond ETF","E","Bonds"], ["TIPS","iShares TIPS Bond ETF","E","Bonds"],["VTIP","Vanguard Short-Term Inflation-Protected ETF","E","Bonds"], // ── Commodity ETFs ── ["GLD","SPDR Gold Shares ETF","E","Gold"],["IAU","iShares Gold Trust","E","Gold"], ["SLV","iShares Silver Trust","E","Silver"],["GLDM","SPDR Gold MiniShares","E","Gold"], ["PDBC","Invesco Optimum Yield Diversified Commodity ETF","E","Commodities"], ["DJP","iPath Bloomberg Commodity Index ETN","E","Commodities"], ["USO","United States Oil Fund","E","Oil"],["BNO","United States Brent Oil Fund","E","Oil"], ["UNG","United States Natural Gas Fund","E","Gas"], // ── Country ETFs ── ["EWJ","iShares MSCI Japan ETF","E","Japan"],["EWG","iShares MSCI Germany ETF","E","Germany"], ["EWU","iShares MSCI United Kingdom ETF","E","UK"],["EWQ","iShares MSCI France ETF","E","France"], ["EWI","iShares MSCI Italy ETF","E","Italy"],["EWC","iShares MSCI Canada ETF","E","Canada"], ["EWA","iShares MSCI Australia ETF","E","Australia"],["EWZ","iShares MSCI Brazil ETF","E","Brazil"], ["FXI","iShares China Large-Cap ETF","E","China"],["MCHI","iShares MSCI China ETF","E","China"], ["KWEB","KraneShares CSI China Internet ETF","E","China"],["INDA","iShares MSCI India ETF","E","India"], ["EWY","iShares MSCI South Korea ETF","E","South Korea"],["EWT","iShares MSCI Taiwan ETF","E","Taiwan"], ["VGK","Vanguard FTSE Europe ETF","E","Europe"],["IEUR","iShares Core MSCI Europe ETF","E","Europe"], ["EZU","iShares MSCI Eurozone ETF","E","Eurozone"], ]; // Build searchable objects from compact format const TICKER_DB = RAW_TICKERS.map(([ticker,name,type,sector])=>({ticker,name,type,sector})); // Fuzzy search: ticker prefix first, then name contains function searchTickers(query, limit=8) { if (!query || query.length < 1) return []; const q = query.toUpperCase().trim(); const ql = query.toLowerCase().trim(); const exact = TICKER_DB.filter(t => t.ticker === q); const tPrefix = TICKER_DB.filter(t => t.ticker !== q && t.ticker.startsWith(q)); const nStart = TICKER_DB.filter(t => !t.ticker.startsWith(q) && t.name.toLowerCase().startsWith(ql)); const nContains = TICKER_DB.filter(t => !t.ticker.startsWith(q) && !t.name.toLowerCase().startsWith(ql) && t.name.toLowerCase().includes(ql)); return [...exact, ...tPrefix, ...nStart, ...nContains].slice(0, limit); } // Validate unknown ticker via Claude (no web search, fast) async function validateTicker(ticker) { const body = { model: "claude-sonnet-4-20250514", max_tokens: 120, messages: [{ role: "user", content: `Is "${ticker}" a real publicly traded stock or ETF on any major exchange? If yes: {"known":true,"ticker":"EXACT_TICKER","name":"Full Company Name","type":"Stock or ETF","exchange":"NYSE/NASDAQ/etc"}. If no: {"known":false}. Raw JSON only. No markdown.` }], }; try { const endpoint = (typeof PielotConfig !== "undefined" && PielotConfig.apiUrl) ? PielotConfig.apiUrl : "https://api.anthropic.com/v1/messages"; const headers = {"Content-Type":"application/json"}; if (typeof PielotConfig !== "undefined" && PielotConfig.nonce) headers["X-WP-Nonce"] = PielotConfig.nonce; const res = await fetch(endpoint, { method:"POST", headers, body:JSON.stringify(body) }); const data = await res.json(); const text = data.content?.find(b=>b.type==="text")?.text?.trim()||"{}"; const s=text.indexOf("{"), e=text.lastIndexOf("}"); if(s===-1||e===-1) return {known:false}; return JSON.parse(text.slice(s,e+1)); } catch { return {known:false}; } } // ─── TickerSearch Component ─────────────────────────────────────────────────── function TickerSearch({ value, onChange, onSelect, placeholder, inputStyle, autoFocus }) { const [query, setQuery] = useState(value||""); const [results, setResults] = useState([]); const [highlighted, setHighlighted] = useState(0); const [open, setOpen] = useState(false); const [validating, setValidating] = useState(false); const wrapRef = useRef(null); // Sync external value changes (e.g. clearing) useEffect(()=>{ if(value==="") { setQuery(""); setResults([]); setOpen(false); } },[value]); function handleInput(e) { const v = e.target.value; setQuery(v); onChange && onChange(v); if (v.trim().length > 0) { const r = searchTickers(v); setResults(r); setOpen(true); setHighlighted(0); } else { setResults([]); setOpen(false); } } function selectItem(item) { setQuery(item.ticker); setResults([]); setOpen(false); onChange && onChange(item.ticker); onSelect && onSelect(item); } async function handleEnterOrSearch() { if (!query.trim()) return; // Check local DB first const exact = TICKER_DB.find(t => t.ticker === query.trim().toUpperCase() || t.name.toLowerCase() === query.trim().toLowerCase()); if (exact) { selectItem(exact); return; } // If there's a highlighted result, select it if (open && results.length > 0) { selectItem(results[highlighted]); return; } // Unknown - validate via Claude setValidating(true); setOpen(false); const v = await validateTicker(query.trim()); setValidating(false); if (v.known) { const item = { ticker: v.ticker||query.trim().toUpperCase(), name: v.name||query.trim(), type: v.type||"Stock", sector: v.exchange||"" }; selectItem(item); } else { onSelect && onSelect({ ticker: query.trim().toUpperCase(), name: query.trim(), type:"Stock", sector:"", unknown:true, notFound:true }); } } function handleKey(e) { if (!open || results.length===0) { if (e.key==="Enter") handleEnterOrSearch(); return; } if (e.key==="ArrowDown") { e.preventDefault(); setHighlighted(h=>Math.min(h+1,results.length-1)); } else if (e.key==="ArrowUp") { e.preventDefault(); setHighlighted(h=>Math.max(h-1,0)); } else if (e.key==="Enter") { e.preventDefault(); selectItem(results[highlighted]); } else if (e.key==="Escape") { setOpen(false); } } // Close on outside click useEffect(()=>{ function handle(e){ if(wrapRef.current&&!wrapRef.current.contains(e.target)) setOpen(false); } document.addEventListener("mousedown",handle); return ()=>document.removeEventListener("mousedown",handle); },[]); const typeColors = { S:"129,140,248", E:"74,222,128" }; return (
{ if(query.trim()&&results.length>0) setOpen(true); }} placeholder={placeholder||"Search ticker or company name..."} /> {validating && (
{[0,1,2].map(i=>
)}
)}
{open && results.length>0 && (
{results.map((r,i)=>(
selectItem(r)} style={{display:"flex",alignItems:"center",gap:"0.75rem",padding:"0.65rem 0.9rem",cursor:"pointer",background:highlighted===i?"rgba(96,165,250,0.08)":"transparent",borderBottom:isetHighlighted(i)}> {r.ticker} {r.name} {r.type}
))}
)}
); } // API function withTimeout(promise, ms) { let tid; const timeout = new Promise((_, reject) => { tid = setTimeout(() => reject(new Error("SERVICE_TIMEOUT")), ms); }); return Promise.race([promise, timeout]).finally(() => clearTimeout(tid)); } async function apiPost(body, logFn, label, cacheKey, forceFresh) { const log = logFn || (() => {}); const t = Date.now(); let res, data; const endpoint = (typeof PielotConfig !== "undefined" && PielotConfig.apiUrl) ? PielotConfig.apiUrl : "https://api.anthropic.com/v1/messages"; const headers = { "Content-Type": "application/json" }; if (typeof PielotConfig !== "undefined" && PielotConfig.nonce) { headers["X-WP-Nonce"] = PielotConfig.nonce; } // Inject cache fields if provided const payload = { ...body }; if (cacheKey) payload.pielot_cache_key = cacheKey; if (forceFresh) payload.pielot_force_fresh = true; res = await fetch(endpoint, { method: "POST", headers, body: JSON.stringify(payload), }); data = await res.json(); if (!res.ok) throw new Error(data.error?.message || data.message || `API error (${label})`); // Show cache hit in debug log const fromCache = data.pielot_from_cache === true; const suffix = fromCache ? " 💾 (cached)" : ""; log(`${label}: ${((Date.now()-t)/1000).toFixed(1)}s - ${data.stop_reason}${suffix}`, fromCache ? "info" : (data.stop_reason === "end_turn" ? "success" : "info")); return data; } async function callClaude(prompt, logFn, cacheKey, forceFresh) { const log = logFn || (() => {}); const baseBody = { model: "claude-sonnet-4-20250514", max_tokens: 4096, tools: [{ type: "web_search_20250305", name: "web_search" }], messages: [{ role: "user", content: prompt }], }; log("Sending to Claude API...", "info"); let data; try { data = await withTimeout(apiPost(baseBody, log, "Round 1", cacheKey, forceFresh), 120000); } catch(e) { throw new Error(e.message === "SERVICE_TIMEOUT" ? "SERVICE_TIMEOUT" : e.message); } if (data.pielot_from_cache) return data.content; let messages = baseBody.messages; let round = 1; while (data.stop_reason === "tool_use" && round <= 3) { const searches = data.content.filter(b => b.type === "tool_use"); log(`Web search round ${round}: ${searches.length} quer${searches.length>1?"ies":"y"}`, "info"); searches.forEach((s,i) => log(` [${i+1}] "${s.input?.query||"..."}"`, "search")); const forceJSON = round >= 3; messages = [ ...messages, { role: "assistant", content: data.content }, { role: "user", content: [{ type: "text", text: forceJSON ? "Output the final JSON now. Raw JSON only - no markdown, no backticks. Start with { and end with }." : "Continue your research, then output the final JSON when ready." }]}, ]; round++; try { data = await withTimeout(apiPost({ ...baseBody, messages }, log, `Round ${round}`, cacheKey), 120000); } catch(e) { throw new Error(e.message === "SERVICE_TIMEOUT" ? "SERVICE_TIMEOUT" : e.message); } } if (data.stop_reason === "tool_use") { messages = [...messages, { role: "assistant", content: data.content }, { role: "user", content: [{ type: "text", text: "Stop searching. Output the JSON now. Raw JSON only. Start with { end with }." }]}]; try { data = await withTimeout(apiPost({ ...baseBody, messages }, log, "Final flush", cacheKey), 120000); } catch(e) { throw new Error(e.message === "SERVICE_TIMEOUT" ? "SERVICE_TIMEOUT" : e.message); } } const block0 = data.content?.find(b => b.type === "text"); const text0 = block0?.text?.trim() || ""; if (!text0.includes("{") && data.stop_reason === "end_turn") { messages = [...messages, { role: "assistant", content: data.content }, { role: "user", content: [{ type: "text", text: "Output ONLY a JSON object. Start with { end with }. If not found: {\"not_found\":true,\"message\":\"reason\"}" }]}]; try { data = await withTimeout(apiPost({ ...baseBody, messages }, log, "JSON recovery", cacheKey), 120000); } catch(e) { throw new Error(e.message === "SERVICE_TIMEOUT" ? "SERVICE_TIMEOUT" : e.message); } } return data.content; } function stripCites(v) { if (typeof v === "string") return v.replace(/]*>/gi,"").replace(/<\/cite>/gi,"").replace(/\s{2,}/g," ").trim(); if (Array.isArray(v)) return v.map(stripCites); if (v && typeof v === "object") { const out={}; for(const k in v) out[k]=stripCites(v[k]); return out; } return v; } function extractJSON(content) { const block = content?.find(b => b.type === "text"); if (!block) throw new Error("No text block in response"); let text = block.text.trim() .replace(/^```json\s*/i,"").replace(/^```\s*/i,"").replace(/```\s*$/i,"").trim(); const s = text.indexOf("{"), e = text.lastIndexOf("}"); if (s === -1 || e === -1) throw new Error(`No JSON found. Model said: "${text.slice(0,200)}"`); try { return stripCites(JSON.parse(text.slice(s, e+1))); } catch(parseErr) { throw new Error(`JSON parse failed: ${parseErr.message}. Preview: ${text.slice(s, s+200)}`); } } // Styles const C = { app: { minHeight:"100vh", background:"#0a0f1e", backgroundImage:"radial-gradient(ellipse at 15% 60%,rgba(96,165,250,0.04) 0%,transparent 55%),radial-gradient(ellipse at 85% 15%,rgba(167,139,250,0.04) 0%,transparent 55%)", color:"white", fontFamily:"'Montserrat',sans-serif", display:"flex", flexDirection:"column", alignItems:"center", paddingBottom:"6rem" }, card: { background:"rgba(255,255,255,0.025)", border:"1px solid rgba(255,255,255,0.07)", borderRadius:"1.25rem", padding:"1.75rem", width:"100%", backdropFilter:"blur(12px)" }, btn: (v="primary") => ({ background: v==="primary" ? "linear-gradient(135deg,rgba(96,165,250,0.18),rgba(167,139,250,0.18))" : "rgba(255,255,255,0.04)", border: v==="primary" ? "1px solid rgba(96,165,250,0.35)" : "1px solid rgba(255,255,255,0.09)", color:"white", padding:"0.85rem 1.5rem", borderRadius:"0.75rem", fontSize:"0.9rem", cursor:"pointer", fontFamily:"'Montserrat',sans-serif", width:"100%" }), label: { fontFamily:"monospace", fontSize:"0.6rem", letterSpacing:"0.15em", color:"rgba(255,255,255,0.25)", marginBottom:"0.5rem", borderBottom:"1px solid rgba(255,255,255,0.07)", paddingBottom:"0.3rem", display:"block" }, tag: (rgb) => ({ display:"inline-block", background:`rgba(${rgb},0.1)`, border:`1px solid rgba(${rgb},0.22)`, borderRadius:"0.3rem", padding:"0.12rem 0.45rem", fontSize:"0.6rem", fontFamily:"monospace", letterSpacing:"0.08em", color:`rgb(${rgb})` }), input: { background:"rgba(255,255,255,0.04)", border:"1px solid rgba(255,255,255,0.1)", borderRadius:"0.75rem", padding:"0.85rem 1.1rem", color:"white", fontSize:"0.95rem", fontFamily:"'Montserrat',sans-serif", width:"100%", outline:"none", boxSizing:"border-box" }, }; function Spinner() { return
{[0,1,2].map(i=>
)}
; } function Badge({ sentiment, reason }) { const sc = SENT_COLORS[sentiment] || SENT_COLORS.neutral; return
{sentiment||"-"}
{reason&&{reason}}
; } function NewsItem({ n }) { if (!n?.headline) return null; return (
{n.headline}
{n.summary &&
{n.summary}
} {n.source_name && (n.source_url?.startsWith("http") ? {n.source_name} → : {n.source_name} )}
); } function PartialDataNotice({ onRetry }) { return (
PARTIAL DATA Some information couldn't be retrieved right now.
{onRetry && }
); } function isPartial(data) { if (!data) return false; const news = data.news || []; const newsOk = news.filter(n => n?.headline).length >= 2; // handles both {overview, sentiment, key_risks} and watchlist disp {sentiment, risks} const hasOverview = !!(data.overview); const hasSentiment = !!(data.sentiment); const hasRisks = !!(data.key_risks || data.risks); return !hasOverview || !hasSentiment || !newsOk || !hasRisks; } // Debug Panel function DebugPanel({ logs }) { const [open, setOpen] = useState(true); const bottomRef = useRef(null); useEffect(() => { if (open) bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [logs, open]); const cm = { info:"rgba(255,255,255,0.5)", success:"#4ade80", error:"#f87171", search:"#60a5fa", warn:"#f59e0b" }; return
setOpen(o=>!o)} style={{background:"#0a0f1e",border:"1px solid rgba(255,255,255,0.12)",borderRadius:open?"0.75rem 0.75rem 0 0":"0.75rem",padding:"0.45rem 0.85rem",display:"flex",alignItems:"center",gap:"0.5rem",cursor:"pointer",userSelect:"none",borderBottom:open?"1px solid rgba(255,255,255,0.06)":undefined}}>
DEBUG {logs.length>0&&`(${logs.length})`} {open?"▼":"▲"}
{open&&
{logs.length===0?Waiting for activity...:logs.map((l,i)=>
{l.ts}{l.msg}
)}
}
; } // Nav function Nav({ tab, setTab, wlCount, pieCount }) { const tabs = [ { key:"research", label:"Research" }, { key:"watchlist", label:`Watchlist${wlCount>0?` (${wlCount})`:""}`}, { key:"pie", label:`Build a Pie${pieCount>0?` (${pieCount})`:""}`}, ]; return
{tabs.map(({key,label})=>)}
PIELOT v0.5
; } // Add-to-Pie Modal function AddToPieModal({ asset, pies, onAddToExisting, onCreateNew, onClose }) { return (
e.stopPropagation()} style={{background:'#0d1526',border:'1px solid rgba(255,255,255,0.1)',borderRadius:'1.25rem',padding:'1.75rem',maxWidth:'420px',width:'100%',boxShadow:'0 0 60px rgba(0,0,0,0.7)'}}>
ADD TO PIE

{asset.ticker} {asset.name}

{pies.length > 0 ? (
YOUR PIES {pies.map(pie => ( ))}
) : (

You have no saved pies yet.

)}
); } // Full asset card function AssetCard({ result, watchlist, onAdd, onRemove, onRetry, pies, onAddToPie, onCreatePieWith }) { const sc = SENT_COLORS[result.sentiment] || SENT_COLORS.neutral; const strat = result.strategy_fit ? STRATEGIES[result.strategy_fit] : null; const isWatched = watchlist.some(w => w.ticker === result.ticker); const partial = isPartial(result); const validNews = result.news?.filter(n => n?.headline) || []; const [showPiePicker, setShowPiePicker] = useState(false); return
{/* Header */}

{result.ticker}

{result.name && {result.name}}
{result.type && {result.type}} {result.sector && {result.sector}}
{result.price_info &&
PRICE
{result.price_info}
} {onAddToPie && } {showPiePicker && {onAddToPie(id,result);setShowPiePicker(false);}} onCreateNew={()=>{onCreatePieWith(result);setShowPiePicker(false);}} onClose={()=>setShowPiePicker(false)}/>}
{/* Partial data notice */} {partial && } {/* Sentiment */} {result.sentiment && } {/* Overview */} {result.overview &&
WHAT IS IT

{result.overview}

} {/* Strategy fit */} {strat &&
BEST STRATEGY FIT
{strat.label}
{result.strategy_fit_reason&&

{result.strategy_fit_reason}

}
} {/* News */} {validNews.length > 0 &&
RECENT NEWS {partial && validNews.length < 3 && - limited results}
{validNews.map((n,i)=>)}
} {/* Risks */} {result.key_risks &&
KEY RISKS

{result.key_risks}

} {/* Reliability */} {result.reliability_note &&
RELIABILITY NOTE {result.reliability_note}
}

AI-generated from public sources (Reuters, Bloomberg, CNBC, Yahoo Finance). Not financial advice.

; } function ServiceError({ onRetry }) { return (
SERVICE UNAVAILABLE

Sorry, the research service is not responding right now.
This is usually temporary - please try again in a moment.

{onRetry && }
); } // Research Tab function ResearchTab({ log, watchlist, onAdd, onRemove, pies, onAddToPie, onCreatePieWith }) { const [selected, setSelected] = useState(null); // confirmed TickerSearch selection const [inputVal, setInputVal] = useState(""); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); const [notFound, setNotFound] = useState(null); const [error, setError] = useState(null); const [history, setHistory] = useState([]); const [elapsed, setElapsed] = useState(0); useEffect(() => { if (!loading) { setElapsed(0); return; } const t = setInterval(() => setElapsed(s => s+1), 1000); return () => clearInterval(t); }, [loading]); async function search(ticker, name, forceFresh = false) { const q = ticker || inputVal; if (!q.trim()) return; setLoading(true); setError(null); setResult(null); setNotFound(null); log(`[Research] "${q}"`, "info"); const hint = name ? ` (${name})` : ""; const prompt = `You are a financial research assistant. Research this asset: "${q}"${hint}\n${NO_CFD_RULE}\nSearch for: what it does, current price, 5 recent news items, analyst sentiment, key risks, best strategy fit.\nIf the asset does not exist or cannot be identified as a real stock/ETF, output ONLY: {"not_found":true,"message":"brief explanation"}\nOtherwise output ALL fields below. Use whatever partial data you find - never leave the response empty.\nCRITICAL: Raw JSON only. No markdown. No backticks. Start { end }. All text max 60w. News max 25w. Real URLs only.\n{"ticker":"","name":"","type":"Stock or ETF","sector":"","price_info":"e.g. $182 (+1.2%)","overview":"max 50w","sentiment":"bullish","sentiment_reason":"max 20w","strategy_fit":"long_term_low_risk","strategy_fit_reason":"max 30w","news":[{"headline":"","summary":"max 25w","source_name":"Reuters","source_url":"https://..."},{"headline":"","summary":"","source_name":"Bloomberg","source_url":""},{"headline":"","summary":"","source_name":"CNBC","source_url":""},{"headline":"","summary":"","source_name":"Yahoo Finance","source_url":""},{"headline":"","summary":"","source_name":"FT","source_url":""}],"key_risks":"max 40w","reliability_note":"max 20w"}`; try { const cacheKey = forceFresh ? null : `research_${q.trim().toUpperCase().replace(/[^A-Z0-9]/g,"")}`; const content = await callClaude(prompt, log, cacheKey, forceFresh); const parsed = extractJSON(content); if (parsed.not_found) { setNotFound(parsed.message || `Couldn't find "${q}" as a known stock or ETF.`); log(`[Research] Not found: ${q}`, "warn"); } else { setResult(parsed); if (parsed.ticker) setHistory(h=>[{ticker:parsed.ticker,name:parsed.name},...h.filter(x=>x.ticker!==parsed.ticker)].slice(0,8)); log(`[Research] Done: ${parsed.ticker} (${parsed.sentiment})`, "success"); } } catch(e) { if (e.message === "SERVICE_TIMEOUT") { setError("SERVICE_TIMEOUT"); log(`[Research] Service timeout`, "error"); } else { setError(e.message); log(`[Research] Error: ${e.message}`, "error"); } } finally { setLoading(false); } } function handleSelect(item) { if (item.notFound) { setNotFound(`"${item.ticker}" doesn't appear to be a real stock or ETF.`); setResult(null); return; } setSelected(item); setInputVal(item.ticker); // Auto-search immediately on selection search(item.ticker, item.name); } return
setInputVal(v)} onSelect={handleSelect} placeholder="Search ticker or company - e.g. NVDA, Apple, VWCE..." inputStyle={{...C.input}} />
{selected && !loading && !result && !notFound && !error && (
{selected.ticker} {selected.name} {selected.type && {selected.type}}
)} {history.length>0&&
RECENT: {history.map((h,i)=>)}
}
{loading&&

Researching across live sources…

{elapsed>=60&&

Taking longer than usual…

}
} {error&&!loading&&(error==="SERVICE_TIMEOUT" ? search(result?.ticker||inputVal, result?.name)}/> :
{error}
)} {notFound&&!loading&&(
NOT FOUND {notFound}

Try the exact ticker symbol (e.g. AAPL, VWCE), or the full company name.

)} {result&&!loading&&search(result.ticker,result.name)} pies={pies} onAddToPie={onAddToPie} onCreatePieWith={onCreatePieWith}/>} {!result&&!notFound&&!loading&&!error&&
RESEARCH ANY ASSET

Search a ticker, company name, or ETF.
Get price, news, risks and strategy fit - powered by live AI research.

}
; } // Watchlist Tab function WatchlistTab({ log, watchlist, onRemove, onUpdateNote, pies, onAddToPie, onCreatePieWith }) { const [expanded, setExpanded] = useState(null); const [refreshing, setRefreshing] = useState(null); const [fresh, setFresh] = useState({}); const [notes, setNotes] = useState({}); const [editingNote, setEditingNote] = useState(null); const [piePicker, setPiePicker] = useState(null); // ticker of item showing pie picker async function refresh(item) { setRefreshing(item.ticker); log(`[Watchlist] Refreshing ${item.ticker}...`, "info"); const prompt = `Quick update on ${item.ticker} - ${item.name}.\n${NO_CFD_RULE}\nSearch latest news and sentiment.\nCRITICAL: Raw JSON only. No markdown. Start { end }.\n{"price_info":"current price and % change","sentiment":"bullish","sentiment_reason":"max 20w","news":[{"headline":"","summary":"max 25w","source_name":"","source_url":"https://..."},{"headline":"","summary":"","source_name":"","source_url":""},{"headline":"","summary":"","source_name":"","source_url":""},{"headline":"","summary":"","source_name":"","source_url":""},{"headline":"","summary":"","source_name":"","source_url":""}],"key_risks":"max 40w"}`; try { const cacheKey = `refresh_${item.ticker.toUpperCase().replace(/[^A-Z0-9]/g,"")}`; const content = await callClaude(prompt, log, cacheKey, false); const parsed = extractJSON(content); setFresh(d=>({...d,[item.ticker]:{...parsed,refreshed_at:new Date().toLocaleString("en-GB")}})); log(`[Watchlist] ${item.ticker} refreshed - ${parsed.sentiment}`, "success"); } catch(e) { log(`[Watchlist] Error: ${e.message}`, "error"); } finally { setRefreshing(null); } } function saveNote(ticker) { const note = notes[ticker] ?? ""; setEditingNote(null); onUpdateNote(ticker, note); log(`[Watchlist] Note saved for ${ticker}`, "success"); } if (watchlist.length===0) return
YOUR WATCHLIST IS EMPTY

Research any asset and click
+ Watchlist to start tracking it.

; return
{watchlist.length} ASSET{watchlist.length!==1?"S":""} TRACKED
{watchlist.map(item=>{ const isOpen=expanded===item.ticker; const isRef=refreshing===item.ticker; const f=fresh[item.ticker]; const disp={sentiment:f?.sentiment||item.sentiment,reason:f?.sentiment_reason||item.sentiment_reason,price:f?.price_info||item.price_info,news:f?.news||item.news,risks:f?.key_risks||item.key_risks}; const sc=SENT_COLORS[disp.sentiment]||SENT_COLORS.neutral; return
setExpanded(isOpen?null:item.ticker)}>
{item.ticker}{item.name}{item.type}
{item.sector&&
{item.sector}
}
{disp.price&&{disp.price}}
{disp.sentiment||"-"}
{isOpen&&
{piePicker===item.ticker&&{onAddToPie(id,item);setPiePicker(null);}} onCreateNew={()=>{onCreatePieWith(item);setPiePicker(null);}} onClose={()=>setPiePicker(null)}/>} Added {item.added_at}{f?.refreshed_at&&` · Updated ${f.refreshed_at}`}
{isPartial(disp)&&!isRef&&refresh(item)}/>} {disp.sentiment&&} {item.overview&&
OVERVIEW

{item.overview}

} {isRef&&} {!isRef&&disp.news?.filter(n=>n?.headline).length>0&&
RECENT NEWS{f?" - REFRESHED":""}
{disp.news.filter(n=>n?.headline).map((n,i)=>)}
} {!isRef&&disp.risks&&
KEY RISKS

{disp.risks}

}
YOUR NOTE {editingNote===item.ticker ?