source: frontend/src/api/twelveData.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: 8.4 KB
Line 
1const BASE_URL =
2 import.meta.env.VITE_TWELVE_DATA_BASE_URL ?? "https://api.twelvedata.com";
3
4const API_KEY = import.meta.env.VITE_TWELVE_DATA_API_KEY ?? "";
5
6const CACHE_TTL_MS = 5 * 60_000;
7const MAX_SYMBOLS_PER_REQUEST = 20;
8
9// Cache by Twelve Data symbol (normalized)
10const priceCache = new Map();
11const inFlight = new Map();
12
13function getCachedPrice(twelveSymbol) {
14 const item = priceCache.get(twelveSymbol);
15 if (!item) return null;
16 if (Date.now() - item.ts > CACHE_TTL_MS) return null;
17 return item.price;
18}
19
20function setCachedPrice(twelveSymbol, price) {
21 if (!twelveSymbol || typeof price !== "number" || !Number.isFinite(price)) return;
22 priceCache.set(twelveSymbol, { price, ts: Date.now() });
23}
24
25function parsePriceJson(json, requestedSymbols) {
26 const out = {};
27 if (!json || typeof json !== "object" || Array.isArray(json)) return out;
28
29 // Single symbol response
30 if (json.price !== undefined && requestedSymbols.length === 1) {
31 const n = Number(json.price);
32 if (Number.isFinite(n)) out[requestedSymbols[0]] = n;
33 return out;
34 }
35
36 // Multi symbol response: object keyed by symbol
37 for (const sym of requestedSymbols) {
38 const n = Number(json?.[sym]?.price);
39 if (Number.isFinite(n)) out[sym] = n;
40 }
41
42 return out;
43}
44
45async function fetchBatchPrices(twelveSymbols) {
46 const params = new URLSearchParams();
47 params.set("symbol", twelveSymbols.join(","));
48 params.set("apikey", API_KEY);
49
50 const url = `${BASE_URL}/price?${params.toString()}`;
51 const resp = await fetch(url, { method: "GET" });
52
53 if (!resp.ok) {
54 const text = await resp.text().catch(() => "");
55 throw new Error(`Twelve Data request failed (HTTP ${resp.status}) ${text}`);
56 }
57
58 const json = await resp.json();
59 if (json?.status === "error") {
60 throw new Error(String(json?.message ?? "Twelve Data error"));
61 }
62
63 return parsePriceJson(json, twelveSymbols);
64}
65
66async function fetchSinglePrice(twelveSymbol) {
67 const cached = getCachedPrice(twelveSymbol);
68 if (typeof cached === "number") return cached;
69
70 if (inFlight.has(twelveSymbol)) return inFlight.get(twelveSymbol);
71
72 const p = (async () => {
73 const params = new URLSearchParams();
74 params.set("symbol", twelveSymbol);
75 params.set("apikey", API_KEY);
76 const url = `${BASE_URL}/price?${params.toString()}`;
77 const resp = await fetch(url, { method: "GET" });
78 if (!resp.ok) {
79 const text = await resp.text().catch(() => "");
80 throw new Error(`Twelve Data request failed (HTTP ${resp.status}) ${text}`);
81 }
82 const json = await resp.json();
83 if (json?.status === "error") {
84 throw new Error(String(json?.message ?? "Twelve Data error"));
85 }
86 const n = Number(json?.price);
87 if (!Number.isFinite(n)) throw new Error("Twelve Data returned no price");
88 setCachedPrice(twelveSymbol, n);
89 return n;
90 })();
91
92 inFlight.set(twelveSymbol, p);
93 try {
94 return await p;
95 } finally {
96 inFlight.delete(twelveSymbol);
97 }
98}
99
100async function fetchSingleExchangeRate(pairSymbol) {
101 const cached = getCachedPrice(pairSymbol);
102 if (typeof cached === "number") return cached;
103
104 if (inFlight.has(pairSymbol)) return inFlight.get(pairSymbol);
105
106 const p = (async () => {
107 const params = new URLSearchParams();
108 params.set("symbol", pairSymbol);
109 params.set("apikey", API_KEY);
110 const url = `${BASE_URL}/exchange_rate?${params.toString()}`;
111 const resp = await fetch(url, { method: "GET" });
112 if (!resp.ok) {
113 const text = await resp.text().catch(() => "");
114 throw new Error(`Twelve Data request failed (HTTP ${resp.status}) ${text}`);
115 }
116 const json = await resp.json();
117 if (json?.status === "error") {
118 throw new Error(String(json?.message ?? "Twelve Data error"));
119 }
120
121 const n = Number(json?.rate);
122 if (!Number.isFinite(n)) throw new Error("Twelve Data returned no rate");
123 setCachedPrice(pairSymbol, n);
124 return n;
125 })();
126
127 inFlight.set(pairSymbol, p);
128 try {
129 return await p;
130 } finally {
131 inFlight.delete(pairSymbol);
132 }
133}
134
135export function toTwelveSymbol(symbol) {
136 if (!symbol) return "";
137 const s = String(symbol).trim().toUpperCase();
138 if (!s) return "";
139
140 // Already Twelve format (common for FX/crypto)
141 if (s.includes("/")) return s;
142
143 // Common Yahoo-style crypto tickers
144 const cryptoMap = {
145 "BTC-USD": "BTC/USD",
146 "ETH-USD": "ETH/USD",
147 "SOL-USD": "SOL/USD",
148 "BNB-USD": "BNB/USD",
149 "XRP-USD": "XRP/USD",
150 "ADA-USD": "ADA/USD",
151 "DOGE-USD": "DOGE/USD",
152 "AVAX-USD": "AVAX/USD",
153 "DOT-USD": "DOT/USD",
154 "LINK-USD": "LINK/USD",
155 "MATIC-USD": "MATIC/USD",
156 "LTC-USD": "LTC/USD",
157 };
158
159 if (cryptoMap[s]) return cryptoMap[s];
160
161 // Default: assume it's a stock/ETF symbol (AAPL, SPY, BRK-B, ...)
162 return s;
163}
164
165/**
166 * Fetch current prices for one or more symbols.
167 * Returns an object keyed by *original input symbols* (uppercased), with numeric prices.
168 */
169export async function fetchCurrentPricesBySymbol(symbols) {
170 const input = Array.isArray(symbols) ? symbols : [];
171 const uniqueOriginal = Array.from(
172 new Set(input.map((s) => String(s ?? "").trim()).filter(Boolean)),
173 );
174
175 if (uniqueOriginal.length === 0) return {};
176
177 if (!API_KEY) {
178 throw new Error(
179 "Missing Twelve Data API key (set VITE_TWELVE_DATA_API_KEY in frontend/.env.local)",
180 );
181 }
182
183 // Map original -> twelve
184 const originalToTwelve = new Map();
185 for (const orig of uniqueOriginal) {
186 originalToTwelve.set(orig, toTwelveSymbol(orig));
187 }
188
189 const allTwelveSymbols = Array.from(
190 new Set(Array.from(originalToTwelve.values()).filter(Boolean)),
191 );
192
193 const pairSymbols = allTwelveSymbols.filter((s) => String(s).includes("/"));
194 const priceSymbols = allTwelveSymbols.filter((s) => !String(s).includes("/"));
195
196 // Gather prices with cache + resilient fetch (batch first, fall back to per-symbol)
197 const twelvePrices = {};
198 const missing = [];
199 let hadAnyError = false;
200 let lastErrorMessage = "";
201
202 for (const sym of allTwelveSymbols) {
203 const cached = getCachedPrice(sym);
204 if (typeof cached === "number") {
205 twelvePrices[sym] = cached;
206 } else {
207 missing.push(sym);
208 }
209 }
210
211 // Fetch pair symbols via exchange_rate (generally the most reliable for FX/crypto pairs)
212 for (const sym of pairSymbols) {
213 if (twelvePrices[sym] !== undefined) continue;
214 try {
215 const n = await fetchSingleExchangeRate(sym);
216 if (Number.isFinite(n)) twelvePrices[sym] = n;
217 } catch (e) {
218 hadAnyError = true;
219 lastErrorMessage = e?.message || lastErrorMessage;
220 }
221 }
222
223 // Chunk batch requests to avoid overly long URLs; if a batch fails, we continue
224 // with per-symbol fetches so one invalid symbol doesn't break everything.
225 const missingPrices = missing.filter((s) => priceSymbols.includes(s));
226
227 for (let i = 0; i < missingPrices.length; i += MAX_SYMBOLS_PER_REQUEST) {
228 const chunk = missingPrices.slice(i, i + MAX_SYMBOLS_PER_REQUEST);
229 try {
230 const got = await fetchBatchPrices(chunk);
231 for (const [sym, price] of Object.entries(got)) {
232 const n = Number(price);
233 if (Number.isFinite(n)) {
234 twelvePrices[sym] = n;
235 setCachedPrice(sym, n);
236 }
237 }
238
239 // Some symbols might not come back in a multi-symbol response; fetch them individually.
240 for (const sym of chunk) {
241 if (twelvePrices[sym] !== undefined) continue;
242 try {
243 const n = await fetchSinglePrice(sym);
244 if (Number.isFinite(n)) twelvePrices[sym] = n;
245 } catch (e) {
246 // ignore single-symbol failures
247 hadAnyError = true;
248 lastErrorMessage = e?.message || lastErrorMessage;
249 }
250 }
251 } catch (e) {
252 // Fall back to per-symbol if the whole batch failed.
253 hadAnyError = true;
254 lastErrorMessage = e?.message || lastErrorMessage;
255 await Promise.all(
256 chunk.map(async (sym) => {
257 try {
258 const n = await fetchSinglePrice(sym);
259 if (Number.isFinite(n)) twelvePrices[sym] = n;
260 } catch (e2) {
261 // ignore
262 hadAnyError = true;
263 lastErrorMessage = e2?.message || lastErrorMessage;
264 }
265 }),
266 );
267 }
268 }
269
270 // Convert back to original symbols
271 const out = {};
272 for (const [orig, twelve] of originalToTwelve.entries()) {
273 const p = twelvePrices[twelve];
274 if (typeof p === "number") {
275 out[String(orig).toUpperCase()] = p;
276 }
277 }
278
279 if (Object.keys(out).length === 0 && hadAnyError) {
280 throw new Error(lastErrorMessage || "Could not fetch any current prices");
281 }
282
283 return out;
284}
Note: See TracBrowser for help on using the repository browser.