Index: frontend/src/api/yahooFinance.js
===================================================================
--- frontend/src/api/yahooFinance.js	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/api/yahooFinance.js	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,148 @@
+import { LOCAL_TICKER_SYMBOLS } from "../data/tickers";
+
+const tickerSearchCache = new Map();
+const TICKER_CACHE_TTL_MS = 15 * 60 * 1000;
+
+const quoteCache = new Map();
+const QUOTE_CACHE_TTL_MS = 5 * 60 * 1000;
+let quoteBackoffUntilMs = 0;
+
+async function tryFetchJson(url) {
+  // Note: We intentionally do NOT set custom headers here.
+  // Custom headers usually trigger a CORS preflight which Yahoo won't allow.
+  const resp = await fetch(url, { method: "GET" });
+  if (!resp.ok) {
+    throw new Error(`Yahoo Finance HTTP ${resp.status}`);
+  }
+  return resp.json();
+}
+
+function toTickerOption(raw) {
+  const symbol = raw?.symbol ? String(raw.symbol) : "";
+  if (!symbol) return null;
+
+  const name = raw?.shortname || raw?.longname || raw?.name || "";
+  const exchange = raw?.exchDisp || raw?.exchange || "";
+
+  return {
+    symbol,
+    name: name ? String(name) : "",
+    exchange: exchange ? String(exchange) : "",
+    quoteType: raw?.quoteType ? String(raw.quoteType) : "",
+  };
+}
+
+/**
+ * Direct Yahoo Finance search (yfinance data source).
+ * Falls back to backend proxy if the browser blocks CORS.
+ */
+export async function searchTickers(query, limit = 20) {
+  const q = String(query ?? "").trim();
+  if (!q) return [];
+  const safeLimit = Math.max(1, Math.min(Number(limit) || 20, 25));
+
+  const cacheKey = `${q.toUpperCase()}:${safeLimit}`;
+  const cached = tickerSearchCache.get(cacheKey);
+  const now = Date.now();
+  if (cached && cached.expiresAt > now) {
+    return cached.value;
+  }
+
+  // Offline/local ticker search: avoids Yahoo 429s and keeps the dropdown reliable.
+  const upperQ = q.toUpperCase();
+  const symbols = Array.isArray(LOCAL_TICKER_SYMBOLS) ? LOCAL_TICKER_SYMBOLS : [];
+  const out = symbols
+    .map((s) => String(s ?? "").trim())
+    .filter(Boolean)
+    .filter((s) => s.toUpperCase().includes(upperQ))
+    .slice(0, safeLimit)
+    .map((symbol) => ({ symbol, name: "", exchange: "" }));
+
+  tickerSearchCache.set(cacheKey, {
+    value: out,
+    expiresAt: now + TICKER_CACHE_TTL_MS,
+  });
+  return out;
+}
+
+/**
+ * Direct Yahoo Finance quotes (yfinance data source).
+ * Returns a map { SYMBOL: priceNumber }.
+ * Falls back to backend proxy if the browser blocks CORS.
+ */
+export async function getQuotesBySymbol(symbols) {
+  const list = Array.isArray(symbols) ? symbols : [];
+  const unique = Array.from(
+    new Set(
+      list
+        .map((s) => String(s ?? "").trim())
+        .filter(Boolean)
+        .map((s) => s.toUpperCase()),
+    ),
+  );
+
+  if (unique.length === 0) return {};
+
+  const parseYahoo = (json) => {
+    const results = Array.isArray(json?.quoteResponse?.result)
+      ? json.quoteResponse.result
+      : [];
+    const map = {};
+    for (const r of results) {
+      const sym = r?.symbol ? String(r.symbol).toUpperCase() : "";
+      const price = r?.regularMarketPrice;
+      if (!sym) continue;
+      if (typeof price !== "number") continue;
+      map[sym] = price;
+    }
+    return map;
+  };
+
+  const now = Date.now();
+  const out = {};
+
+  // Serve cache first.
+  const toFetch = [];
+  for (const sym of unique) {
+    const cached = quoteCache.get(sym);
+    if (cached && cached.expiresAt > now && typeof cached.price === "number") {
+      out[sym] = cached.price;
+    } else {
+      toFetch.push(sym);
+    }
+  }
+
+  // Production builds: browser CORS blocks Yahoo, so quotes are unavailable without a proxy.
+  if (!import.meta.env.DEV) {
+    return out;
+  }
+
+  // Backoff after 429s.
+  if (now < quoteBackoffUntilMs) {
+    return out;
+  }
+
+  if (toFetch.length === 0) {
+    return out;
+  }
+
+  const yahooUrl = `/yahoo/v7/finance/quote?symbols=${encodeURIComponent(
+    toFetch.join(","),
+  )}`;
+
+  try {
+    const json = await tryFetchJson(yahooUrl);
+    const fresh = parseYahoo(json);
+    for (const [sym, price] of Object.entries(fresh)) {
+      quoteCache.set(sym, { price, expiresAt: Date.now() + QUOTE_CACHE_TTL_MS });
+      out[sym] = price;
+    }
+    return out;
+  } catch (e) {
+    const msg = String(e?.message ?? "");
+    if (msg.includes("HTTP 429")) {
+      quoteBackoffUntilMs = Date.now() + 60_000;
+    }
+    return out;
+  }
+}
