Skip to content

Commit 9b2da9c

Browse files
committed
Add ticker formatting utilities and update display logic
Introduces `formatTickerForDisplay` and `normalizeTickerForComparison` in a new `src/lib/tickers.ts` utility. Updates portfolio, rebalance, and schedule-rebalance components to use these functions for consistent ticker display, especially for crypto pairs. Also extends position types to include `assetClass` and `assetSymbol`, and updates logic to fetch and use asset symbols for crypto positions.
1 parent 0bba10d commit 9b2da9c

File tree

11 files changed

+216
-96
lines changed

11 files changed

+216
-96
lines changed

src/components/PortfolioPositions.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Badge } from "@/components/ui/badge";
66
import { Button } from "@/components/ui/button";
77
import { TrendingUp, TrendingDown, RefreshCw, Loader2, Eye, Activity, Clock, AlertCircle, AlertTriangle, Lock } from "lucide-react";
88
import { alpacaAPI } from "@/lib/alpaca";
9+
import { formatTickerForDisplay } from "@/lib/tickers";
910
import { useAuth, isSessionValid, hasAlpacaCredentials } from "@/lib/auth";
1011
import { supabase } from "@/lib/supabase";
1112
import { useToast } from "@/hooks/use-toast";
@@ -34,6 +35,7 @@ import ScheduleListModal from "./ScheduleListModal";
3435

3536
interface Position {
3637
symbol: string;
38+
displaySymbol: string;
3739
shares: number;
3840
avgCost: number;
3941
currentPrice: number;
@@ -42,6 +44,8 @@ interface Position {
4244
unrealizedPLPct: number;
4345
dayChange: number;
4446
description?: string;
47+
assetClass?: string;
48+
assetSymbol?: string;
4549
}
4650

4751
interface PortfolioPositionsProps {
@@ -188,6 +192,7 @@ export default function PortfolioPositions({ onSelectStock, selectedStock }: Por
188192

189193
// Calculate today's change from open if we have the data
190194
const stockData = batchData[pos.symbol];
195+
const assetSymbol = stockData?.asset?.symbol;
191196
if (stockData?.currentBar) {
192197
const todayOpen = stockData.currentBar.o;
193198
const priceChange = currentPrice - todayOpen;
@@ -205,14 +210,17 @@ export default function PortfolioPositions({ onSelectStock, selectedStock }: Por
205210

206211
return {
207212
symbol: pos.symbol,
213+
displaySymbol: formatTickerForDisplay(pos.symbol, { assetClass: pos.asset_class, assetSymbol }),
208214
shares: parseFloat(pos.qty),
209215
avgCost: parseFloat(pos.avg_entry_price),
210216
currentPrice: currentPrice,
211217
marketValue: parseFloat(pos.market_value),
212218
unrealizedPL: parseFloat(pos.unrealized_pl),
213219
unrealizedPLPct: parseFloat(pos.unrealized_plpc) * 100,
214220
dayChange: dayChangePercent,
215-
description: description
221+
description: description,
222+
assetClass: pos.asset_class,
223+
assetSymbol
216224
};
217225
});
218226

@@ -610,7 +618,7 @@ export default function PortfolioPositions({ onSelectStock, selectedStock }: Por
610618
>
611619
<TableCell className="font-medium w-[60px]">
612620
<Badge variant={selectedStock === position.symbol ? 'default' : 'outline'}>
613-
{position.symbol}
621+
{position.displaySymbol}
614622
</Badge>
615623
</TableCell>
616624
<TableCell className="text-right text-sm px-2">{position.shares.toFixed(2)}</TableCell>

src/components/rebalance/components/PortfolioComposition.tsx

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { Card } from "@/components/ui/card";
55
import { Progress } from "@/components/ui/progress";
6+
import { formatTickerForDisplay } from "@/lib/tickers";
67
import type { RebalancePosition } from "../types";
78

89
interface PortfolioCompositionProps {
@@ -25,28 +26,31 @@ export function PortfolioComposition({
2526

2627
{/* Stacked Bar */}
2728
<div className="w-full h-10 flex rounded-lg overflow-hidden border">
28-
{positions.map((position) => (
29-
<div
30-
key={position.ticker}
31-
className="relative group transition-opacity hover:opacity-90"
32-
style={{
33-
width: `${position.currentAllocation}%`,
34-
backgroundColor: positionColors[position.ticker]
35-
}}
36-
>
37-
{/* Show percentage if space allows */}
38-
{position.currentAllocation >= 8 && (
39-
<span className="absolute inset-0 flex items-center justify-center text-white text-xs font-medium drop-shadow">
40-
{position.currentAllocation.toFixed(1)}%
41-
</span>
42-
)}
29+
{positions.map((position) => {
30+
const displayTicker = formatTickerForDisplay(position.ticker, { assetClass: position.assetClass, assetSymbol: position.assetSymbol });
31+
return (
32+
<div
33+
key={position.ticker}
34+
className="relative group transition-opacity hover:opacity-90"
35+
style={{
36+
width: `${position.currentAllocation}%`,
37+
backgroundColor: positionColors[position.ticker]
38+
}}
39+
>
40+
{/* Show percentage if space allows */}
41+
{position.currentAllocation >= 8 && (
42+
<span className="absolute inset-0 flex items-center justify-center text-white text-xs font-medium drop-shadow">
43+
{position.currentAllocation.toFixed(1)}%
44+
</span>
45+
)}
4346

44-
{/* Tooltip on hover */}
45-
<div className="opacity-0 group-hover:opacity-100 absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 text-white text-xs rounded whitespace-nowrap z-10 pointer-events-none transition-opacity">
46-
{position.ticker}: {position.currentAllocation.toFixed(1)}%
47+
{/* Tooltip on hover */}
48+
<div className="opacity-0 group-hover:opacity-100 absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 text-white text-xs rounded whitespace-nowrap z-10 pointer-events-none transition-opacity">
49+
{displayTicker}: {position.currentAllocation.toFixed(1)}%
50+
</div>
4751
</div>
48-
</div>
49-
))}
52+
);
53+
})}
5054

5155
{/* Cash portion */}
5256
{cashAllocation > 0 && (
@@ -70,16 +74,19 @@ export function PortfolioComposition({
7074

7175
{/* Legend */}
7276
<div className="flex flex-wrap gap-3 text-xs">
73-
{positions.map((position) => (
74-
<div key={position.ticker} className="flex items-center gap-1.5">
75-
<div
76-
className="w-3 h-3 rounded"
77-
style={{ backgroundColor: positionColors[position.ticker] }}
78-
/>
79-
<span className="font-medium">{position.ticker}:</span>
80-
<span className="text-muted-foreground">{position.currentAllocation.toFixed(1)}%</span>
81-
</div>
82-
))}
77+
{positions.map((position) => {
78+
const displayTicker = formatTickerForDisplay(position.ticker, { assetClass: position.assetClass, assetSymbol: position.assetSymbol });
79+
return (
80+
<div key={position.ticker} className="flex items-center gap-1.5">
81+
<div
82+
className="w-3 h-3 rounded"
83+
style={{ backgroundColor: positionColors[position.ticker] }}
84+
/>
85+
<span className="font-medium">{displayTicker}:</span>
86+
<span className="text-muted-foreground">{position.currentAllocation.toFixed(1)}%</span>
87+
</div>
88+
);
89+
})}
8390
{cashAllocation > 0 && (
8491
<div className="flex items-center gap-1.5">
8592
<div className="w-3 h-3 rounded bg-gray-500" />
@@ -91,4 +98,4 @@ export function PortfolioComposition({
9198
</div>
9299
</Card>
93100
);
94-
}
101+
}

src/components/rebalance/components/StockPositionCard.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Checkbox } from "@/components/ui/checkbox";
55
import { Progress } from "@/components/ui/progress";
66
import { Badge } from "@/components/ui/badge";
77
import { Lock, Eye } from "lucide-react";
8+
import { formatTickerForDisplay } from "@/lib/tickers";
89
import type { RebalancePosition } from "../types";
910

1011
interface StockPositionCardProps {
@@ -24,8 +25,9 @@ export function StockPositionCard({
2425
isWatchlist = false,
2526
onToggle
2627
}: StockPositionCardProps) {
27-
const displayTicker = ticker || position?.ticker || '';
28-
28+
const rawTicker = ticker || position?.ticker || '';
29+
const displayTicker = formatTickerForDisplay(rawTicker, { assetClass: position?.assetClass, assetSymbol: position?.assetSymbol });
30+
2931
if (isWatchlist && ticker) {
3032
// Watchlist stock card (simpler version)
3133
return (
@@ -42,7 +44,7 @@ export function StockPositionCard({
4244
onClick={(e) => e.stopPropagation()}
4345
disabled={isDisabled}
4446
/>
45-
<span className="font-semibold">{ticker}</span>
47+
<span className="font-semibold">{displayTicker}</span>
4648
{isDisabled && <Lock className="h-4 w-4 text-muted-foreground" />}
4749
<Badge variant="outline" className="text-xs">
4850
<Eye className="w-3 h-3 mr-1" />
@@ -81,7 +83,7 @@ export function StockPositionCard({
8183
onClick={(e) => e.stopPropagation()}
8284
disabled={isDisabled}
8385
/>
84-
<span className="font-semibold text-lg">{position.ticker}</span>
86+
<span className="font-semibold text-lg">{displayTicker}</span>
8587
{isDisabled && <Lock className="h-4 w-4 text-muted-foreground" />}
8688
</div>
8789
<div className="text-right">
@@ -116,4 +118,4 @@ export function StockPositionCard({
116118
</div>
117119
</div>
118120
);
119-
}
121+
}

src/components/rebalance/hooks/useRebalanceData.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,42 @@ export function useRebalanceData(isOpen: boolean) {
129129
currentShares: parseFloat(pos.qty || '0'),
130130
currentValue: parseFloat(pos.market_value || '0'),
131131
currentAllocation: (parseFloat(pos.market_value || '0') / totalEquity) * 100,
132-
avgPrice: parseFloat(pos.avg_entry_price || '0')
132+
avgPrice: parseFloat(pos.avg_entry_price || '0'),
133+
assetClass: pos.asset_class
133134
}));
134135

136+
const cryptoTickers = Array.from(new Set(
137+
processedPositions
138+
.filter(pos => (pos.assetClass || '').toLowerCase() === 'crypto')
139+
.map(pos => pos.ticker)
140+
));
141+
142+
if (cryptoTickers.length > 0) {
143+
const assetSymbolEntries = await Promise.all(
144+
cryptoTickers.map(async (ticker) => {
145+
try {
146+
const asset = await alpacaAPI.getAsset(ticker);
147+
const symbol = typeof asset?.symbol === 'string' ? asset.symbol : null;
148+
return [ticker, symbol] as const;
149+
} catch (error) {
150+
console.warn(`Failed to fetch asset metadata for ${ticker}:`, error);
151+
return [ticker, null] as const;
152+
}
153+
})
154+
);
155+
156+
const assetSymbolMap = new Map(
157+
assetSymbolEntries.filter(([, symbol]) => typeof symbol === 'string' && !!symbol)
158+
);
159+
160+
processedPositions.forEach((position) => {
161+
const assetSymbol = assetSymbolMap.get(position.ticker);
162+
if (assetSymbol) {
163+
position.assetSymbol = assetSymbol;
164+
}
165+
});
166+
}
167+
135168
// Sort positions by allocation (descending)
136169
processedPositions.sort((a, b) => b.currentAllocation - a.currentAllocation);
137170

src/components/rebalance/tabs/StockSelectionTab.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Loader2, Eye, Lock, AlertCircle } from "lucide-react";
1111
import { PortfolioComposition } from "../components/PortfolioComposition";
1212
import { StockPositionCard } from "../components/StockPositionCard";
1313
import type { RebalancePosition, RebalanceConfig } from "../types";
14+
import { formatTickerForDisplay } from "@/lib/tickers";
1415

1516
interface StockSelectionTabProps {
1617
loading: boolean;
@@ -99,6 +100,7 @@ export function StockSelectionTab({
99100
{watchlistStocks.map(ticker => {
100101
const isSelected = selectedPositions.has(ticker);
101102
const isDisabled = !isSelected && maxStocks > 0 && selectedPositions.size >= maxStocks;
103+
const displayTicker = formatTickerForDisplay(ticker);
102104
return (
103105
<Badge
104106
key={ticker}
@@ -107,7 +109,7 @@ export function StockSelectionTab({
107109
onClick={() => !isDisabled && onTogglePosition(ticker)}
108110
>
109111
<Eye className="w-3 h-3 mr-1" />
110-
{ticker}
112+
{displayTicker}
111113
{isDisabled && <Lock className="w-3 h-3 ml-1" />}
112114
</Badge>
113115
);
@@ -178,4 +180,4 @@ export function StockSelectionTab({
178180
)}
179181
</TabsContent>
180182
);
181-
}
183+
}

src/components/rebalance/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export interface RebalancePosition {
77
currentValue: number;
88
currentAllocation: number;
99
avgPrice?: number;
10+
assetClass?: string;
11+
assetSymbol?: string;
1012
}
1113

1214
export interface RebalanceConfig {
@@ -37,4 +39,4 @@ export interface PortfolioData {
3739
currentPrice: number;
3840
priceChangeFromAvg: number;
3941
}>;
40-
}
42+
}

0 commit comments

Comments
 (0)