Skip to content

Commit e060bd9

Browse files
committed
Improve crypto symbol handling and formatting
Enhanced crypto symbol parsing and candidate generation in alpaca.ts to better support various symbol formats and improve reliability of crypto bar fetching. Updated number formatting in PerformanceChart and PortfolioPositions to use locale-aware formatting with two decimal places for improved readability.
1 parent 9b2da9c commit e060bd9

File tree

3 files changed

+129
-43
lines changed

3 files changed

+129
-43
lines changed

src/components/PerformanceChart.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -605,9 +605,10 @@ const PerformanceChart = React.memo(({ selectedStock: propSelectedStock, selecte
605605
tickLine={false}
606606
axisLine={false}
607607
tickFormatter={(value) =>
608-
selectedStock
609-
? `$${value.toFixed(2)}`
610-
: `$${(value / 1000).toFixed(0)}k`
608+
`$${value.toLocaleString(undefined, {
609+
minimumFractionDigits: 2,
610+
maximumFractionDigits: 2,
611+
})}`
611612
}
612613
/>
613614
<Tooltip
@@ -630,7 +631,10 @@ const PerformanceChart = React.memo(({ selectedStock: propSelectedStock, selecte
630631
strokeDasharray="5 5"
631632
strokeOpacity={0.7}
632633
label={{
633-
value: selectedStock ? `Start: $${startPrice.toFixed(2)}` : `Start: $${(startPrice / 1000).toFixed(0)}k`,
634+
value: `Start: $${startPrice.toLocaleString(undefined, {
635+
minimumFractionDigits: 2,
636+
maximumFractionDigits: 2,
637+
})}`,
634638
position: "left",
635639
fill: "#ffcc00",
636640
fontSize: 10

src/components/PortfolioPositions.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,10 @@ export default function PortfolioPositions({ onSelectStock, selectedStock }: Por
623623
</TableCell>
624624
<TableCell className="text-right text-sm px-2">{position.shares.toFixed(2)}</TableCell>
625625
<TableCell className="text-right font-medium text-sm px-2">
626-
${(position.marketValue / 1000).toFixed(1)}k
626+
${position.marketValue.toLocaleString(undefined, {
627+
minimumFractionDigits: 2,
628+
maximumFractionDigits: 2,
629+
})}
627630
</TableCell>
628631
<TableCell className="text-right px-2">
629632
<div className={`flex items-center justify-end gap-1 ${position.dayChange >= 0 ? 'text-success' : 'text-danger'

src/lib/alpaca.ts

Lines changed: 117 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,86 @@
55
import { useAuth } from './auth';
66
import { supabase } from './supabase';
77

8-
const KNOWN_CRYPTO_QUOTES = ['USD', 'USDT', 'USDC', 'EUR', 'GBP'];
8+
const addUnique = (list: string[], value?: string | null) => {
9+
if (!value) return;
10+
const normalized = value.trim().toUpperCase();
11+
if (!normalized) return;
12+
if (!list.includes(normalized)) {
13+
list.push(normalized);
14+
}
15+
};
916

10-
const ensureCryptoPairSymbol = (symbol: string): string => {
11-
const upper = symbol.toUpperCase();
12-
if (upper.includes('/')) {
13-
return upper;
17+
const buildQuoteLengths = (length: number): number[] => {
18+
const minQuoteLength = 2;
19+
const maxQuoteLength = Math.min(6, length - 1);
20+
21+
if (maxQuoteLength < minQuoteLength) {
22+
return [];
1423
}
1524

16-
for (const quote of KNOWN_CRYPTO_QUOTES) {
17-
if (upper.endsWith(quote) && upper.length > quote.length) {
18-
return `${upper.slice(0, upper.length - quote.length)}/${quote}`;
25+
const values: number[] = [];
26+
for (let candidate = minQuoteLength; candidate <= maxQuoteLength; candidate++) {
27+
values.push(candidate);
28+
}
29+
30+
const ideal = Math.round(length / 2);
31+
32+
return values.sort((a, b) => {
33+
const diff = Math.abs(a - ideal) - Math.abs(b - ideal);
34+
if (diff !== 0) {
35+
return diff;
36+
}
37+
return a - b;
38+
});
39+
};
40+
41+
const generateCryptoSymbolCandidates = (symbol: string): string[] => {
42+
const upper = symbol.trim().toUpperCase();
43+
const sanitized = upper.replace(/[^A-Z0-9]/g, '');
44+
const slashCandidates: string[] = [];
45+
const plainCandidates: string[] = [];
46+
47+
addUnique(plainCandidates, sanitized);
48+
49+
if (upper.includes('/')) {
50+
addUnique(slashCandidates, upper);
51+
} else {
52+
addUnique(plainCandidates, upper);
53+
54+
if (sanitized.length >= 5) {
55+
const quoteLengths = buildQuoteLengths(sanitized.length);
56+
57+
for (const quoteLength of quoteLengths) {
58+
const splitIndex = sanitized.length - quoteLength;
59+
if (splitIndex < 2) {
60+
continue;
61+
}
62+
63+
const base = sanitized.slice(0, splitIndex);
64+
const quote = sanitized.slice(splitIndex);
65+
addUnique(slashCandidates, `${base}/${quote}`);
66+
}
1967
}
2068
}
2169

22-
if (upper.length > 3) {
23-
return `${upper.slice(0, upper.length - 3)}/${upper.slice(-3)}`;
70+
const combined = [...slashCandidates, ...plainCandidates];
71+
return combined.length > 0 ? combined : [upper];
72+
};
73+
74+
const MAX_CRYPTO_CANDIDATES = 3;
75+
76+
const looksLikeCrypto = (symbol: string): boolean => {
77+
const upper = symbol.toUpperCase();
78+
if (upper.includes('/')) {
79+
return true;
80+
}
81+
82+
const sanitized = upper.replace(/[^A-Z0-9]/g, '');
83+
if (sanitized.length >= 6) {
84+
return true;
2485
}
2586

26-
return upper;
87+
return generateCryptoSymbolCandidates(upper).some((value) => value.includes('/'));
2788
};
2889

2990
interface AlpacaConfig {
@@ -324,44 +385,62 @@ class AlpacaAPI {
324385
limit?: number
325386
): Promise<any> {
326387
const upperSymbol = symbol.toUpperCase();
327-
const isLikelyCrypto = upperSymbol.includes('/') || KNOWN_CRYPTO_QUOTES.some(quote => upperSymbol.endsWith(quote) && upperSymbol.length > quote.length);
388+
const isLikelyCrypto = looksLikeCrypto(upperSymbol);
328389

329390
if (isLikelyCrypto) {
330-
const cryptoSymbol = ensureCryptoPairSymbol(upperSymbol);
331-
const params: Record<string, string> = {
332-
symbols: cryptoSymbol,
333-
timeframe,
334-
limit: (limit || 10000).toString()
335-
};
391+
const cryptoCandidates = generateCryptoSymbolCandidates(upperSymbol).slice(0, MAX_CRYPTO_CANDIDATES);
392+
let lastError: Error | null = null;
393+
let lastErrorMessage: string | null = null;
394+
395+
for (const candidate of cryptoCandidates) {
396+
const params: Record<string, string> = {
397+
symbols: candidate,
398+
timeframe,
399+
limit: (limit || 10000).toString()
400+
};
336401

337-
if (start) params.start = start;
338-
if (end) params.end = end;
402+
if (start) params.start = start;
403+
if (end) params.end = end;
339404

340-
const { data, error } = await supabase.functions.invoke('alpaca-proxy', {
341-
body: {
342-
method: 'GET',
343-
endpoint: '/v1beta3/crypto/us/bars',
344-
params
405+
const { data, error } = await supabase.functions.invoke('alpaca-proxy', {
406+
body: {
407+
method: 'GET',
408+
endpoint: '/v1beta3/crypto/us/bars',
409+
params
410+
}
411+
});
412+
413+
if (error) {
414+
console.error(`Failed to fetch crypto bars for ${symbol} via ${candidate}:`, error);
415+
lastError = error;
416+
continue;
345417
}
346-
});
347418

348-
if (error) {
349-
console.error(`Failed to fetch crypto bars for ${symbol}:`, error);
350-
throw new Error(`Failed to fetch bars for ${symbol}: ${error.message}`);
351-
}
419+
if (!data) {
420+
lastErrorMessage = `No bar data received for ${symbol}`;
421+
continue;
422+
}
352423

353-
if (!data) {
354-
throw new Error(`No bar data received for ${symbol}`);
424+
if (data.error) {
425+
console.error(`Failed to fetch crypto bars for ${symbol} via ${candidate}:`, data.error);
426+
lastErrorMessage = typeof data.error === 'string' ? data.error : JSON.stringify(data.error);
427+
continue;
428+
}
429+
430+
const bars = data.bars?.[candidate] || data.bars?.[candidate.replace('/', '')] || [];
431+
if (Array.isArray(bars) && bars.length > 0) {
432+
console.log(`Alpaca crypto bars response for ${symbol} (${timeframe}) via ${candidate}: ${bars.length} bars`);
433+
return bars;
434+
}
435+
436+
console.log(`No crypto bars returned for ${symbol} via ${candidate}`);
355437
}
356438

357-
if (data.error) {
358-
console.error(`Failed to fetch crypto bars for ${symbol}:`, data.error);
359-
throw new Error(`Failed to fetch bars for ${symbol}: ${typeof data.error === 'string' ? data.error : JSON.stringify(data.error)}`);
439+
if (lastError) {
440+
throw new Error(`Failed to fetch bars for ${symbol}: ${lastError.message}`);
360441
}
361442

362-
const bars = data.bars?.[cryptoSymbol] || data.bars?.[cryptoSymbol.replace('/', '')] || [];
363-
console.log(`Alpaca crypto bars response for ${symbol} (${timeframe}): ${Array.isArray(bars) ? bars.length : 0} bars`);
364-
return Array.isArray(bars) ? bars : [];
443+
throw new Error(`Failed to fetch bars for ${symbol}: ${lastErrorMessage ?? 'No historical data returned'}`);
365444
}
366445

367446
const params: Record<string, string> = {

0 commit comments

Comments
 (0)