Skip to content

Commit aaa4045

Browse files
committed
Refactor chart data fetching and improve downsampling
Refactored PerformanceChart to fetch and cache portfolio and stock data per period, using new fetchPortfolioDataForPeriod and fetchStockDataForPeriod functions. Downsampling is now consistently set to 80 points for all chart periods. Improved YTD filtering, added more robust error handling, and enhanced X-axis tick formatting. Also removed unused legacy code and deleted the tutorial-todo.md file.
1 parent 5d7092d commit aaa4045

File tree

4 files changed

+394
-982
lines changed

4 files changed

+394
-982
lines changed

src/components/PerformanceChart.tsx

Lines changed: 89 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
99
import { alpacaAPI } from "@/lib/alpaca";
1010
import { useAuth, isSessionValid } from "@/lib/auth";
1111
import { useToast } from "@/hooks/use-toast";
12-
import { fetchPortfolioData, fetchStockData, type PortfolioData, type StockData } from "@/lib/portfolio-data";
12+
import { fetchPortfolioDataForPeriod, fetchStockDataForPeriod, type PortfolioData, type StockData, type PortfolioDataPoint } from "@/lib/portfolio-data";
1313
import { useNavigate } from "react-router-dom";
1414

1515
interface PerformanceChartProps {
@@ -41,8 +41,8 @@ const PerformanceChart = React.memo(({ selectedStock, onClearSelection }: Perfor
4141
const [positionsLoading, setPositionsLoading] = useState(true); // Track positions loading separately
4242
const [error, setError] = useState<string | null>(null);
4343
const [metrics, setMetrics] = useState<any>(null);
44-
const [portfolioData, setPortfolioData] = useState<PortfolioData | null>(null);
45-
const [stockData, setStockData] = useState<StockData>({});
44+
const [portfolioData, setPortfolioData] = useState<{ [period: string]: PortfolioDataPoint[] }>({});
45+
const [stockData, setStockData] = useState<{ [ticker: string]: { [period: string]: PortfolioDataPoint[] } }>({});
4646
const [positions, setPositions] = useState<any[]>([]);
4747
const [hasAlpacaConfig, setHasAlpacaConfig] = useState(true); // Assume configured initially
4848
const { apiSettings, isAuthenticated } = useAuth();
@@ -51,8 +51,9 @@ const PerformanceChart = React.memo(({ selectedStock, onClearSelection }: Perfor
5151
// Track if we've already fetched for current apiSettings and selectedStock
5252
const fetchedRef = useRef<string>('');
5353
const lastFetchTimeRef = useRef<number>(0);
54+
const metricsLoaded = useRef(false);
5455

55-
const fetchData = useCallback(async () => {
56+
const fetchData = useCallback(async (period: string) => {
5657
// Debounce fetches - don't fetch if we just fetched less than 2 seconds ago
5758
const now = Date.now();
5859
if (now - lastFetchTimeRef.current < 2000) {
@@ -64,13 +65,33 @@ const PerformanceChart = React.memo(({ selectedStock, onClearSelection }: Perfor
6465
setError(null);
6566

6667
try {
67-
// First try to fetch portfolio data (doesn't require Alpaca API)
68-
const portfolioHistoryData = await fetchPortfolioData();
69-
setPortfolioData(portfolioHistoryData);
68+
// Fetch data for the specific period
69+
if (selectedStock) {
70+
// Check if we already have this data
71+
if (!stockData[selectedStock]?.[period]) {
72+
const stockHistoryData = await fetchStockDataForPeriod(selectedStock, period);
73+
setStockData(prev => ({
74+
...prev,
75+
[selectedStock]: {
76+
...prev[selectedStock],
77+
[period]: stockHistoryData
78+
}
79+
}));
80+
}
81+
} else {
82+
// Fetch portfolio data for this period if not cached
83+
if (!portfolioData[period]) {
84+
const portfolioHistoryData = await fetchPortfolioDataForPeriod(period);
85+
setPortfolioData(prev => ({
86+
...prev,
87+
[period]: portfolioHistoryData
88+
}));
89+
}
90+
}
7091

71-
// Try to fetch metrics (which now uses batch internally)
72-
// The edge functions will handle checking if Alpaca is configured
73-
const metricsData = await alpacaAPI.calculateMetrics().catch(err => {
92+
// Try to fetch metrics only once (not period-specific)
93+
if (!metricsLoaded.current) {
94+
const metricsData = await alpacaAPI.calculateMetrics().catch(err => {
7495
console.warn("Failed to calculate metrics:", err);
7596
// Check if it's a configuration error
7697
if (err.message?.includes('API settings not found') ||
@@ -107,56 +128,33 @@ const PerformanceChart = React.memo(({ selectedStock, onClearSelection }: Perfor
107128
return null;
108129
});
109130

110-
// Positions are now included in metrics data
111-
const positionsData = metricsData?.positions || [];
131+
// Positions are now included in metrics data
132+
const positionsData = metricsData?.positions || [];
112133

113-
setMetrics(metricsData);
114-
setPositions(positionsData || []);
115-
setPositionsLoading(false); // Mark positions as loaded
134+
setMetrics(metricsData);
135+
setPositions(positionsData || []);
136+
setPositionsLoading(false); // Mark positions as loaded
137+
metricsLoaded.current = true;
138+
}
116139

117-
// If a stock is selected, fetch its data (uses Alpaca API)
118-
if (selectedStock) {
140+
// Fetch daily change for selected stock if needed
141+
if (selectedStock && period === '1D') {
119142
try {
120-
// console.log(`Fetching stock data for ${selectedStock}...`);
121-
const stockHistoryData = await fetchStockData(selectedStock);
122-
// Debug logging removed to prevent console spam
123-
setStockData(prev => {
124-
const newState = { ...prev, [selectedStock]: stockHistoryData };
125-
// console.log(`Updated stockData state for ${selectedStock}`);
126-
return newState;
143+
const batchData = await alpacaAPI.getBatchData([selectedStock], {
144+
includeQuotes: true,
145+
includeBars: true
127146
});
128147

129-
// Fetch daily change using batch method
130-
try {
131-
const batchData = await alpacaAPI.getBatchData([selectedStock], {
132-
includeQuotes: true,
133-
includeBars: true
134-
});
135-
136-
const data = batchData[selectedStock];
137-
if (data?.quote && data?.previousBar) {
138-
const currentPrice = data.quote.ap || data.quote.bp || 0;
139-
const previousClose = data.previousBar.c;
140-
const dayChange = currentPrice - previousClose;
141-
const dayChangePercent = previousClose > 0 ? (dayChange / previousClose) * 100 : 0;
142-
143-
// console.log(`Daily change for ${selectedStock}: $${dayChange.toFixed(2)} (${dayChangePercent.toFixed(2)}%`);
144-
}
145-
} catch (err) {
146-
console.warn(`Could not fetch daily change for ${selectedStock}:`, err);
148+
const data = batchData[selectedStock];
149+
if (data?.quote && data?.previousBar) {
150+
const currentPrice = data.quote.ap || data.quote.bp || 0;
151+
const previousClose = data.previousBar.c;
152+
const dayChange = currentPrice - previousClose;
153+
// const dayChangePercent = previousClose > 0 ? (dayChange / previousClose) * 100 : 0;
154+
console.log(`Daily change for ${selectedStock}: $${dayChange.toFixed(2)}`);
147155
}
148156
} catch (err) {
149-
console.error(`Error fetching data for ${selectedStock}:`, err);
150-
// Check if it's an API configuration error for stock data
151-
if (err instanceof Error &&
152-
(err.message.includes('API settings not found') ||
153-
err.message.includes('not configured') ||
154-
err.message.includes('Edge Function returned a non-2xx status code'))) {
155-
setHasAlpacaConfig(false);
156-
setError(null); // Don't show error for missing API config
157-
} else {
158-
setError(`Failed to fetch data for ${selectedStock}: ${err instanceof Error ? err.message : 'Unknown error'}`);
159-
}
157+
console.warn(`Could not fetch daily change for ${selectedStock}:`, err);
160158
}
161159
}
162160
} catch (err) {
@@ -181,17 +179,17 @@ const PerformanceChart = React.memo(({ selectedStock, onClearSelection }: Perfor
181179
} finally {
182180
setLoading(false);
183181
}
184-
}, [apiSettings, selectedStock, toast]);
182+
}, [selectedStock, portfolioData, stockData, toast]);
185183

186-
// Fetch data on component mount and when selectedStock changes
184+
// Fetch data when period or stock changes
187185
useEffect(() => {
188186
// Don't fetch if not authenticated or session is invalid
189187
if (!isAuthenticated || !isSessionValid()) {
190188
console.log('PerformanceChart: Skipping fetch - session invalid or not authenticated');
191189
return;
192190
}
193191

194-
const fetchKey = `${apiSettings?.apiKey || 'none'}-${selectedStock || 'portfolio'}`;
192+
const fetchKey = `${selectedStock || 'portfolio'}-${selectedPeriod}`;
195193

196194
// Avoid duplicate fetches for the same configuration
197195
if (fetchedRef.current === fetchKey) {
@@ -202,11 +200,11 @@ const PerformanceChart = React.memo(({ selectedStock, onClearSelection }: Perfor
202200

203201
// Add a small delay on initial mount to ensure session is settled
204202
const timeoutId = setTimeout(() => {
205-
fetchData();
203+
fetchData(selectedPeriod);
206204
}, 500);
207205

208206
return () => clearTimeout(timeoutId);
209-
}, [selectedStock, fetchData, isAuthenticated]); // Include fetchData and isAuthenticated in dependencies
207+
}, [selectedStock, selectedPeriod, fetchData, isAuthenticated]); // Include fetchData and isAuthenticated in dependencies
210208

211209
// Get real stock metrics from positions
212210
const getStockMetrics = useCallback((symbol: string) => {
@@ -271,40 +269,42 @@ const PerformanceChart = React.memo(({ selectedStock, onClearSelection }: Perfor
271269

272270
// Get appropriate data based on selection
273271
const getCurrentData = useCallback(() => {
274-
// Debug logging removed to prevent console spam
275-
276-
// Check for real stock data first (even if no portfolio data)
277-
if (selectedStock && stockData[selectedStock]) {
272+
// Check for real stock data first
273+
if (selectedStock && stockData[selectedStock]?.[selectedPeriod]) {
278274
const periodData = stockData[selectedStock][selectedPeriod];
279-
// Debug logging removed to prevent console spam
280-
281275
if (periodData && Array.isArray(periodData) && periodData.length > 0) {
282-
// console.log(`Returning ${periodData.length} real data points for ${selectedStock} ${selectedPeriod}`);
283276
return periodData;
284-
} else {
285-
// console.warn(`No valid data for ${selectedStock} ${selectedPeriod} - periodData:`, periodData);
286277
}
287278
}
288279

289280
// Check for portfolio data
290-
if (!selectedStock && portfolioData && portfolioData[selectedPeriod]) {
281+
if (!selectedStock && portfolioData[selectedPeriod]) {
291282
const data = portfolioData[selectedPeriod];
292-
// console.log(`Returning ${data.length} portfolio data points for ${selectedPeriod}`);
293283
return data;
294284
}
295285

296286
// No data available
297-
// Debug logging removed to prevent console spam
298287
return [];
299288
}, [selectedStock, stockData, selectedPeriod, portfolioData]);
300289

301290
const currentData = useMemo(() => getCurrentData(), [getCurrentData]);
302291

292+
// Custom tick formatter for X-axis based on period
293+
const formatXAxisTick = useCallback((value: string) => {
294+
// For 1M period, show abbreviated format
295+
if (selectedPeriod === '1M' || selectedPeriod === '3M' || selectedPeriod === 'YTD' || selectedPeriod === '1Y') {
296+
// If the value already looks like "Sep 12", keep it
297+
// Otherwise try to format it consistently
298+
return value;
299+
}
300+
return value;
301+
}, [selectedPeriod]);
302+
303303
const latestValue = currentData[currentData.length - 1] || { value: 0, pnl: 0 };
304304
const firstValue = currentData[0] || { value: 0, pnl: 0 };
305305
const totalReturn = latestValue.pnl || (latestValue.value - firstValue.value);
306-
const totalReturnPercent = latestValue.pnlPercent ?
307-
parseFloat(latestValue.pnlPercent).toFixed(2) :
306+
const totalReturnPercent = 'pnlPercent' in latestValue && latestValue.pnlPercent ?
307+
parseFloat(String(latestValue.pnlPercent)).toFixed(2) :
308308
(firstValue.value > 0 ? ((totalReturn / firstValue.value) * 100).toFixed(2) : '0.00');
309309
const isPositive = totalReturn >= 0;
310310

@@ -394,6 +394,20 @@ const PerformanceChart = React.memo(({ selectedStock, onClearSelection }: Perfor
394394
</div>
395395
)}
396396
</div>
397+
<div className="text-xs text-muted-foreground">
398+
Data may be incomplete or delayed.{' '}
399+
<a
400+
href={selectedStock
401+
? `https://app.alpaca.markets/trade/${selectedStock}`
402+
: 'https://app.alpaca.markets/dashboard/overview'
403+
}
404+
target="_blank"
405+
rel="noopener noreferrer"
406+
className="text-primary hover:underline"
407+
>
408+
View on Alpaca →
409+
</a>
410+
</div>
397411
</div>
398412
</CardHeader>
399413
<CardContent>
@@ -439,6 +453,9 @@ const PerformanceChart = React.memo(({ selectedStock, onClearSelection }: Perfor
439453
tick={{ fontSize: 11 }}
440454
tickLine={false}
441455
axisLine={false}
456+
interval="preserveStartEnd"
457+
minTickGap={50}
458+
tickFormatter={formatXAxisTick}
442459
/>
443460
<YAxis
444461
domain={yAxisDomain}

src/components/rebalance-detail/RebalanceWorkflowTab.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -317,20 +317,6 @@ function RebalanceWorkflowSteps({
317317
{step.id === 'analysis' && step.stockAnalyses && (
318318
<div className="space-y-4 pl-14">
319319
{step.stockAnalyses.map((stockAnalysis: any) => {
320-
// Define workflow steps and determine their status based on agent completion
321-
const getWorkflowStepStatus = (agentKeys: string[]) => {
322-
const agentStatuses = agentKeys.map(key => stockAnalysis.agents?.[key] || 'pending');
323-
const hasError = agentStatuses.some(s => s === 'error' || s === 'failed');
324-
const hasCompleted = agentStatuses.some(s => s === 'completed');
325-
const hasRunning = agentStatuses.some(s => s === 'running');
326-
const allCompleted = agentStatuses.every(s => s === 'completed');
327-
328-
if (hasError) return 'error';
329-
if (allCompleted && agentStatuses.length > 0) return 'completed';
330-
if (hasCompleted || hasRunning) return 'running';
331-
return 'pending';
332-
};
333-
334320
// Get research and other steps from full_analysis workflow steps
335321
const fullAnalysis = stockAnalysis.fullAnalysis || {};
336322
const fullWorkflowSteps = fullAnalysis.workflowSteps || [];
@@ -341,10 +327,33 @@ function RebalanceWorkflowSteps({
341327

342328
// Check if all agents in this step are completed
343329
const agents = step.agents || [];
330+
331+
// Debug logging for research phase
332+
if (stepId === 'research' && agents.length > 0) {
333+
console.log(`Research phase full step data:`, step);
334+
console.log(`Research phase agents:`, agents.map((a: any) => ({
335+
name: a.name,
336+
status: a.status,
337+
error: a.error,
338+
errorAt: a.errorAt
339+
})));
340+
}
341+
344342
const anyError = agents.some((a: any) => a.status === 'error' || a.status === 'failed');
345343
const allCompleted = agents.length > 0 && agents.every((a: any) => a.status === 'completed');
346344
const anyRunning = agents.some((a: any) => a.status === 'running');
347345
const anyCompleted = agents.some((a: any) => a.status === 'completed');
346+
347+
// Check if analysis is complete (by checking if later phases have completed agents)
348+
const analysisComplete = fullWorkflowSteps.some((s: any) =>
349+
(s.id === 'risk' || s.id === 'portfolio') &&
350+
s.agents?.some((a: any) => a.status === 'completed')
351+
);
352+
353+
// If analysis is complete but this phase has agents still "running", they actually failed
354+
if (analysisComplete && anyRunning) {
355+
return 'error'; // Agents got stuck/failed
356+
}
348357

349358
if (anyError) return 'error';
350359
if (allCompleted) return 'completed';
@@ -357,7 +366,7 @@ function RebalanceWorkflowSteps({
357366
name: 'Data Analysis',
358367
key: 'dataAnalysis',
359368
icon: ChartBar,
360-
status: getWorkflowStepStatus(['marketAnalyst', 'newsAnalyst', 'socialMediaAnalyst', 'fundamentalsAnalyst'])
369+
status: getStepStatusFromWorkflow('analysis') // Use consistent method like other phases
361370
},
362371
{
363372
name: 'Research',

0 commit comments

Comments
 (0)