Index: frontend/src/api/twelveData.js
===================================================================
--- frontend/src/api/twelveData.js	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/api/twelveData.js	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,284 @@
+const BASE_URL =
+  import.meta.env.VITE_TWELVE_DATA_BASE_URL ?? "https://api.twelvedata.com";
+
+const API_KEY = import.meta.env.VITE_TWELVE_DATA_API_KEY ?? "";
+
+const CACHE_TTL_MS = 5 * 60_000;
+const MAX_SYMBOLS_PER_REQUEST = 20;
+
+// Cache by Twelve Data symbol (normalized)
+const priceCache = new Map();
+const inFlight = new Map();
+
+function getCachedPrice(twelveSymbol) {
+  const item = priceCache.get(twelveSymbol);
+  if (!item) return null;
+  if (Date.now() - item.ts > CACHE_TTL_MS) return null;
+  return item.price;
+}
+
+function setCachedPrice(twelveSymbol, price) {
+  if (!twelveSymbol || typeof price !== "number" || !Number.isFinite(price)) return;
+  priceCache.set(twelveSymbol, { price, ts: Date.now() });
+}
+
+function parsePriceJson(json, requestedSymbols) {
+  const out = {};
+  if (!json || typeof json !== "object" || Array.isArray(json)) return out;
+
+  // Single symbol response
+  if (json.price !== undefined && requestedSymbols.length === 1) {
+    const n = Number(json.price);
+    if (Number.isFinite(n)) out[requestedSymbols[0]] = n;
+    return out;
+  }
+
+  // Multi symbol response: object keyed by symbol
+  for (const sym of requestedSymbols) {
+    const n = Number(json?.[sym]?.price);
+    if (Number.isFinite(n)) out[sym] = n;
+  }
+
+  return out;
+}
+
+async function fetchBatchPrices(twelveSymbols) {
+  const params = new URLSearchParams();
+  params.set("symbol", twelveSymbols.join(","));
+  params.set("apikey", API_KEY);
+
+  const url = `${BASE_URL}/price?${params.toString()}`;
+  const resp = await fetch(url, { method: "GET" });
+
+  if (!resp.ok) {
+    const text = await resp.text().catch(() => "");
+    throw new Error(`Twelve Data request failed (HTTP ${resp.status}) ${text}`);
+  }
+
+  const json = await resp.json();
+  if (json?.status === "error") {
+    throw new Error(String(json?.message ?? "Twelve Data error"));
+  }
+
+  return parsePriceJson(json, twelveSymbols);
+}
+
+async function fetchSinglePrice(twelveSymbol) {
+  const cached = getCachedPrice(twelveSymbol);
+  if (typeof cached === "number") return cached;
+
+  if (inFlight.has(twelveSymbol)) return inFlight.get(twelveSymbol);
+
+  const p = (async () => {
+    const params = new URLSearchParams();
+    params.set("symbol", twelveSymbol);
+    params.set("apikey", API_KEY);
+    const url = `${BASE_URL}/price?${params.toString()}`;
+    const resp = await fetch(url, { method: "GET" });
+    if (!resp.ok) {
+      const text = await resp.text().catch(() => "");
+      throw new Error(`Twelve Data request failed (HTTP ${resp.status}) ${text}`);
+    }
+    const json = await resp.json();
+    if (json?.status === "error") {
+      throw new Error(String(json?.message ?? "Twelve Data error"));
+    }
+    const n = Number(json?.price);
+    if (!Number.isFinite(n)) throw new Error("Twelve Data returned no price");
+    setCachedPrice(twelveSymbol, n);
+    return n;
+  })();
+
+  inFlight.set(twelveSymbol, p);
+  try {
+    return await p;
+  } finally {
+    inFlight.delete(twelveSymbol);
+  }
+}
+
+async function fetchSingleExchangeRate(pairSymbol) {
+  const cached = getCachedPrice(pairSymbol);
+  if (typeof cached === "number") return cached;
+
+  if (inFlight.has(pairSymbol)) return inFlight.get(pairSymbol);
+
+  const p = (async () => {
+    const params = new URLSearchParams();
+    params.set("symbol", pairSymbol);
+    params.set("apikey", API_KEY);
+    const url = `${BASE_URL}/exchange_rate?${params.toString()}`;
+    const resp = await fetch(url, { method: "GET" });
+    if (!resp.ok) {
+      const text = await resp.text().catch(() => "");
+      throw new Error(`Twelve Data request failed (HTTP ${resp.status}) ${text}`);
+    }
+    const json = await resp.json();
+    if (json?.status === "error") {
+      throw new Error(String(json?.message ?? "Twelve Data error"));
+    }
+
+    const n = Number(json?.rate);
+    if (!Number.isFinite(n)) throw new Error("Twelve Data returned no rate");
+    setCachedPrice(pairSymbol, n);
+    return n;
+  })();
+
+  inFlight.set(pairSymbol, p);
+  try {
+    return await p;
+  } finally {
+    inFlight.delete(pairSymbol);
+  }
+}
+
+export function toTwelveSymbol(symbol) {
+  if (!symbol) return "";
+  const s = String(symbol).trim().toUpperCase();
+  if (!s) return "";
+
+  // Already Twelve format (common for FX/crypto)
+  if (s.includes("/")) return s;
+
+  // Common Yahoo-style crypto tickers
+  const cryptoMap = {
+    "BTC-USD": "BTC/USD",
+    "ETH-USD": "ETH/USD",
+    "SOL-USD": "SOL/USD",
+    "BNB-USD": "BNB/USD",
+    "XRP-USD": "XRP/USD",
+    "ADA-USD": "ADA/USD",
+    "DOGE-USD": "DOGE/USD",
+    "AVAX-USD": "AVAX/USD",
+    "DOT-USD": "DOT/USD",
+    "LINK-USD": "LINK/USD",
+    "MATIC-USD": "MATIC/USD",
+    "LTC-USD": "LTC/USD",
+  };
+
+  if (cryptoMap[s]) return cryptoMap[s];
+
+  // Default: assume it's a stock/ETF symbol (AAPL, SPY, BRK-B, ...)
+  return s;
+}
+
+/**
+ * Fetch current prices for one or more symbols.
+ * Returns an object keyed by *original input symbols* (uppercased), with numeric prices.
+ */
+export async function fetchCurrentPricesBySymbol(symbols) {
+  const input = Array.isArray(symbols) ? symbols : [];
+  const uniqueOriginal = Array.from(
+    new Set(input.map((s) => String(s ?? "").trim()).filter(Boolean)),
+  );
+
+  if (uniqueOriginal.length === 0) return {};
+
+  if (!API_KEY) {
+    throw new Error(
+      "Missing Twelve Data API key (set VITE_TWELVE_DATA_API_KEY in frontend/.env.local)",
+    );
+  }
+
+  // Map original -> twelve
+  const originalToTwelve = new Map();
+  for (const orig of uniqueOriginal) {
+    originalToTwelve.set(orig, toTwelveSymbol(orig));
+  }
+
+  const allTwelveSymbols = Array.from(
+    new Set(Array.from(originalToTwelve.values()).filter(Boolean)),
+  );
+
+  const pairSymbols = allTwelveSymbols.filter((s) => String(s).includes("/"));
+  const priceSymbols = allTwelveSymbols.filter((s) => !String(s).includes("/"));
+
+  // Gather prices with cache + resilient fetch (batch first, fall back to per-symbol)
+  const twelvePrices = {};
+  const missing = [];
+  let hadAnyError = false;
+  let lastErrorMessage = "";
+
+  for (const sym of allTwelveSymbols) {
+    const cached = getCachedPrice(sym);
+    if (typeof cached === "number") {
+      twelvePrices[sym] = cached;
+    } else {
+      missing.push(sym);
+    }
+  }
+
+  // Fetch pair symbols via exchange_rate (generally the most reliable for FX/crypto pairs)
+  for (const sym of pairSymbols) {
+    if (twelvePrices[sym] !== undefined) continue;
+    try {
+      const n = await fetchSingleExchangeRate(sym);
+      if (Number.isFinite(n)) twelvePrices[sym] = n;
+    } catch (e) {
+      hadAnyError = true;
+      lastErrorMessage = e?.message || lastErrorMessage;
+    }
+  }
+
+  // Chunk batch requests to avoid overly long URLs; if a batch fails, we continue
+  // with per-symbol fetches so one invalid symbol doesn't break everything.
+  const missingPrices = missing.filter((s) => priceSymbols.includes(s));
+
+  for (let i = 0; i < missingPrices.length; i += MAX_SYMBOLS_PER_REQUEST) {
+    const chunk = missingPrices.slice(i, i + MAX_SYMBOLS_PER_REQUEST);
+    try {
+      const got = await fetchBatchPrices(chunk);
+      for (const [sym, price] of Object.entries(got)) {
+        const n = Number(price);
+        if (Number.isFinite(n)) {
+          twelvePrices[sym] = n;
+          setCachedPrice(sym, n);
+        }
+      }
+
+      // Some symbols might not come back in a multi-symbol response; fetch them individually.
+      for (const sym of chunk) {
+        if (twelvePrices[sym] !== undefined) continue;
+        try {
+          const n = await fetchSinglePrice(sym);
+          if (Number.isFinite(n)) twelvePrices[sym] = n;
+        } catch (e) {
+          // ignore single-symbol failures
+          hadAnyError = true;
+          lastErrorMessage = e?.message || lastErrorMessage;
+        }
+      }
+    } catch (e) {
+      // Fall back to per-symbol if the whole batch failed.
+      hadAnyError = true;
+      lastErrorMessage = e?.message || lastErrorMessage;
+      await Promise.all(
+        chunk.map(async (sym) => {
+          try {
+            const n = await fetchSinglePrice(sym);
+            if (Number.isFinite(n)) twelvePrices[sym] = n;
+          } catch (e2) {
+            // ignore
+            hadAnyError = true;
+            lastErrorMessage = e2?.message || lastErrorMessage;
+          }
+        }),
+      );
+    }
+  }
+
+  // Convert back to original symbols
+  const out = {};
+  for (const [orig, twelve] of originalToTwelve.entries()) {
+    const p = twelvePrices[twelve];
+    if (typeof p === "number") {
+      out[String(orig).toUpperCase()] = p;
+    }
+  }
+
+  if (Object.keys(out).length === 0 && hadAnyError) {
+    throw new Error(lastErrorMessage || "Could not fetch any current prices");
+  }
+
+  return out;
+}
