source: frontend/src/api/yahooFinance.js@ 8c01a1f

Last change on this file since 8c01a1f was 8c01a1f, checked in by Andrej <asumanovski@…>, 5 months ago

Made investing tab and adding assets, starting training tracking and adding training sessions

  • Property mode set to 100644
File size: 4.0 KB
Line 
1import { LOCAL_TICKER_SYMBOLS } from "../data/tickers";
2
3const tickerSearchCache = new Map();
4const TICKER_CACHE_TTL_MS = 15 * 60 * 1000;
5
6const quoteCache = new Map();
7const QUOTE_CACHE_TTL_MS = 5 * 60 * 1000;
8let quoteBackoffUntilMs = 0;
9
10async function tryFetchJson(url) {
11 // Note: We intentionally do NOT set custom headers here.
12 // Custom headers usually trigger a CORS preflight which Yahoo won't allow.
13 const resp = await fetch(url, { method: "GET" });
14 if (!resp.ok) {
15 throw new Error(`Yahoo Finance HTTP ${resp.status}`);
16 }
17 return resp.json();
18}
19
20function toTickerOption(raw) {
21 const symbol = raw?.symbol ? String(raw.symbol) : "";
22 if (!symbol) return null;
23
24 const name = raw?.shortname || raw?.longname || raw?.name || "";
25 const exchange = raw?.exchDisp || raw?.exchange || "";
26
27 return {
28 symbol,
29 name: name ? String(name) : "",
30 exchange: exchange ? String(exchange) : "",
31 quoteType: raw?.quoteType ? String(raw.quoteType) : "",
32 };
33}
34
35/**
36 * Direct Yahoo Finance search (yfinance data source).
37 * Falls back to backend proxy if the browser blocks CORS.
38 */
39export async function searchTickers(query, limit = 20) {
40 const q = String(query ?? "").trim();
41 if (!q) return [];
42 const safeLimit = Math.max(1, Math.min(Number(limit) || 20, 25));
43
44 const cacheKey = `${q.toUpperCase()}:${safeLimit}`;
45 const cached = tickerSearchCache.get(cacheKey);
46 const now = Date.now();
47 if (cached && cached.expiresAt > now) {
48 return cached.value;
49 }
50
51 // Offline/local ticker search: avoids Yahoo 429s and keeps the dropdown reliable.
52 const upperQ = q.toUpperCase();
53 const symbols = Array.isArray(LOCAL_TICKER_SYMBOLS) ? LOCAL_TICKER_SYMBOLS : [];
54 const out = symbols
55 .map((s) => String(s ?? "").trim())
56 .filter(Boolean)
57 .filter((s) => s.toUpperCase().includes(upperQ))
58 .slice(0, safeLimit)
59 .map((symbol) => ({ symbol, name: "", exchange: "" }));
60
61 tickerSearchCache.set(cacheKey, {
62 value: out,
63 expiresAt: now + TICKER_CACHE_TTL_MS,
64 });
65 return out;
66}
67
68/**
69 * Direct Yahoo Finance quotes (yfinance data source).
70 * Returns a map { SYMBOL: priceNumber }.
71 * Falls back to backend proxy if the browser blocks CORS.
72 */
73export async function getQuotesBySymbol(symbols) {
74 const list = Array.isArray(symbols) ? symbols : [];
75 const unique = Array.from(
76 new Set(
77 list
78 .map((s) => String(s ?? "").trim())
79 .filter(Boolean)
80 .map((s) => s.toUpperCase()),
81 ),
82 );
83
84 if (unique.length === 0) return {};
85
86 const parseYahoo = (json) => {
87 const results = Array.isArray(json?.quoteResponse?.result)
88 ? json.quoteResponse.result
89 : [];
90 const map = {};
91 for (const r of results) {
92 const sym = r?.symbol ? String(r.symbol).toUpperCase() : "";
93 const price = r?.regularMarketPrice;
94 if (!sym) continue;
95 if (typeof price !== "number") continue;
96 map[sym] = price;
97 }
98 return map;
99 };
100
101 const now = Date.now();
102 const out = {};
103
104 // Serve cache first.
105 const toFetch = [];
106 for (const sym of unique) {
107 const cached = quoteCache.get(sym);
108 if (cached && cached.expiresAt > now && typeof cached.price === "number") {
109 out[sym] = cached.price;
110 } else {
111 toFetch.push(sym);
112 }
113 }
114
115 // Production builds: browser CORS blocks Yahoo, so quotes are unavailable without a proxy.
116 if (!import.meta.env.DEV) {
117 return out;
118 }
119
120 // Backoff after 429s.
121 if (now < quoteBackoffUntilMs) {
122 return out;
123 }
124
125 if (toFetch.length === 0) {
126 return out;
127 }
128
129 const yahooUrl = `/yahoo/v7/finance/quote?symbols=${encodeURIComponent(
130 toFetch.join(","),
131 )}`;
132
133 try {
134 const json = await tryFetchJson(yahooUrl);
135 const fresh = parseYahoo(json);
136 for (const [sym, price] of Object.entries(fresh)) {
137 quoteCache.set(sym, { price, expiresAt: Date.now() + QUOTE_CACHE_TTL_MS });
138 out[sym] = price;
139 }
140 return out;
141 } catch (e) {
142 const msg = String(e?.message ?? "");
143 if (msg.includes("HTTP 429")) {
144 quoteBackoffUntilMs = Date.now() + 60_000;
145 }
146 return out;
147 }
148}
Note: See TracBrowser for help on using the repository browser.