Skip to content

Commit 562e105

Browse files
committed
Refactor rebalance config UI and improve Alpaca order sync
Simplifies rebalance and scheduled rebalance configuration UIs by removing 'use default settings' and direct editing of position size and allocation fields, making these values always use user settings. Adds a stock selection limit display to modals. Improves Alpaca order status synchronization in RecentTrades with periodic batch updates. Cleans up types and removes deprecated fields. Refactors portfolio data helpers for more consistent period handling and downsampling.
1 parent 24b93dc commit 562e105

19 files changed

+751
-899
lines changed

src/components/PortfolioPositions.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -651,9 +651,7 @@ export default function PortfolioPositions({ onSelectStock, selectedStock }: Por
651651
// The constraints object below includes targetCashAllocation
652652
// which tells the Portfolio Manager the stock/cash split
653653

654-
// Build constraints object
655-
// Note: When useDefaultSettings is true, config already has the values from apiSettings
656-
// (loaded in RebalanceModal's loadData function)
654+
// Build constraints object with user-configured values
657655
const constraints = {
658656
maxPositionSize: config.maxPosition,
659657
minPositionSize: config.minPosition,
@@ -701,8 +699,7 @@ export default function PortfolioPositions({ onSelectStock, selectedStock }: Por
701699
constraints,
702700
portfolioData,
703701
skipOpportunityAgent: config.skipOpportunityAgent,
704-
rebalanceThreshold: config.rebalanceThreshold,
705-
useDefaultSettings: config.useDefaultSettings
702+
rebalanceThreshold: config.rebalanceThreshold
706703
}
707704
});
708705

src/components/RecentTrades.tsx

Lines changed: 184 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ArrowUpRight, ArrowDownRight, Clock, CheckCircle, XCircle, TrendingUp,
66
import { alpacaAPI } from "@/lib/alpaca";
77
import { useAuth, isSessionValid } from "@/lib/auth";
88
import { supabase } from "@/lib/supabase";
9+
import { getCachedSession } from "@/lib/cachedAuth";
910
import { useToast } from "@/hooks/use-toast";
1011
import AnalysisDetailModal from "@/components/AnalysisDetailModal";
1112
import RebalanceDetailModal from "@/components/RebalanceDetailModal";
@@ -104,6 +105,153 @@ function RecentTrades() {
104105
}
105106
}, [user?.id, isAuthenticated, toast]); // isSessionValid is a pure function, doesn't need to be in deps
106107

108+
// Function to update Alpaca order status for approved orders using batch API
109+
const updateAlpacaOrderStatus = async () => {
110+
if (!user?.id || !apiSettings) return;
111+
112+
try {
113+
// Get recent trading actions (last 48 hours) - same timeframe as displayed trades
114+
const twoDaysAgo = new Date();
115+
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
116+
117+
// Get approved and executed orders with Alpaca IDs in metadata from last 48 hours
118+
// Include executed orders in case they need status updates (partial fills, etc.)
119+
const { data: approvedOrders, error } = await supabase
120+
.from('trading_actions')
121+
.select('id, metadata, status, created_at')
122+
.eq('user_id', user.id)
123+
.in('status', ['approved', 'executed'])
124+
.gte('created_at', twoDaysAgo.toISOString());
125+
126+
if (error || !approvedOrders || approvedOrders.length === 0) {
127+
console.log('No approved orders found to update');
128+
return;
129+
}
130+
131+
// Filter orders that have Alpaca order IDs
132+
const ordersWithAlpacaIds = approvedOrders.filter(o => o.metadata?.alpaca_order?.id);
133+
if (ordersWithAlpacaIds.length === 0) {
134+
console.log('No orders with Alpaca IDs found');
135+
return;
136+
}
137+
138+
// Extract all Alpaca order IDs
139+
const alpacaOrderIds = ordersWithAlpacaIds.map(o => o.metadata.alpaca_order.id);
140+
console.log(`Fetching status for ${alpacaOrderIds.length} Alpaca orders:`, alpacaOrderIds);
141+
142+
// Fetch all orders from Alpaca using batch API
143+
const session = await getCachedSession();
144+
const response = await fetch(`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/alpaca-batch`, {
145+
method: 'POST',
146+
headers: {
147+
'Authorization': `Bearer ${session?.access_token}`,
148+
'Content-Type': 'application/json',
149+
},
150+
body: JSON.stringify({
151+
orderIds: alpacaOrderIds,
152+
includeOrders: true
153+
})
154+
});
155+
156+
if (!response.ok) {
157+
console.error('Failed to fetch orders from Alpaca batch API');
158+
return;
159+
}
160+
161+
const responseData = await response.json();
162+
console.log('Full response from alpaca-batch:', responseData);
163+
const alpacaOrders = responseData?.data?.orders || [];
164+
console.log(`Received ${alpacaOrders.length} orders from Alpaca:`, alpacaOrders);
165+
166+
// Update status for each order
167+
let hasUpdates = false;
168+
for (const order of ordersWithAlpacaIds) {
169+
const alpacaOrderId = order.metadata.alpaca_order.id;
170+
const alpacaOrder = alpacaOrders.find((o: any) => o.id === alpacaOrderId);
171+
172+
if (alpacaOrder) {
173+
console.log(`Found Alpaca order ${alpacaOrderId} with status: ${alpacaOrder.status}`);
174+
175+
// Check if status has changed or if there's new fill information
176+
const currentAlpacaStatus = order.metadata?.alpaca_order?.status;
177+
const currentFilledQty = order.metadata?.alpaca_order?.filled_qty;
178+
const hasStatusChanged = currentAlpacaStatus !== alpacaOrder.status;
179+
const hasNewFillData = alpacaOrder.filled_qty && alpacaOrder.filled_qty !== currentFilledQty;
180+
181+
// Always update if we don't have a status yet, or if something changed
182+
if (!currentAlpacaStatus || hasStatusChanged || hasNewFillData) {
183+
console.log(`Order ${alpacaOrderId} updating: current status "${currentAlpacaStatus}" -> new status "${alpacaOrder.status}"`);
184+
hasUpdates = true;
185+
186+
// Build the alpaca_order object, only including defined values
187+
const alpacaOrderUpdate: any = {
188+
...(order.metadata?.alpaca_order || {}),
189+
status: alpacaOrder.status,
190+
updated_at: new Date().toISOString()
191+
};
192+
193+
// Only add filled_qty and filled_avg_price if they exist
194+
if (alpacaOrder.filled_qty) {
195+
alpacaOrderUpdate.filled_qty = parseFloat(alpacaOrder.filled_qty);
196+
}
197+
if (alpacaOrder.filled_avg_price) {
198+
alpacaOrderUpdate.filled_avg_price = parseFloat(alpacaOrder.filled_avg_price);
199+
}
200+
201+
// Update metadata with latest Alpaca order info
202+
const updatedMetadata = {
203+
...(order.metadata || {}),
204+
alpaca_order: alpacaOrderUpdate
205+
};
206+
207+
const updates: any = {
208+
metadata: updatedMetadata
209+
};
210+
211+
// If order is filled, update execution timestamp in metadata
212+
if (alpacaOrder.status === 'filled') {
213+
// Store execution details in metadata, not in main status field
214+
updates.executed_at = alpacaOrder.filled_at || new Date().toISOString();
215+
console.log(`Order ${alpacaOrderId} is filled, updating execution timestamp`);
216+
} else if (['canceled', 'cancelled', 'rejected', 'expired'].includes(alpacaOrder.status) && order.status === 'approved') {
217+
// Only update to rejected if it was approved before
218+
updates.status = 'rejected';
219+
console.log(`Marking order ${alpacaOrderId} as rejected due to Alpaca status: ${alpacaOrder.status}`);
220+
}
221+
222+
console.log(`Updating order ${order.id} with:`, updates);
223+
const { data: updateData, error: updateError } = await supabase
224+
.from('trading_actions')
225+
.update(updates)
226+
.eq('id', order.id)
227+
.select();
228+
229+
if (updateError) {
230+
console.error(`Failed to update order ${order.id}:`, updateError);
231+
console.error('Update payload was:', updates);
232+
} else {
233+
console.log(`Successfully updated order ${order.id}`, updateData);
234+
}
235+
} else {
236+
console.log(`Order ${alpacaOrderId} unchanged at status: ${currentAlpacaStatus}`);
237+
}
238+
} else {
239+
console.log(`No matching Alpaca order found for ${alpacaOrderId}`);
240+
}
241+
}
242+
243+
// Refresh the trades after a short delay if we made updates
244+
if (hasUpdates) {
245+
console.log('Updates were made, refreshing trades...');
246+
setTimeout(() => {
247+
fetchAllTrades();
248+
}, 500);
249+
}
250+
} catch (err) {
251+
console.error('Error updating Alpaca order status:', err);
252+
}
253+
};
254+
107255
// Track if we've already fetched for current user
108256
const fetchedRef = useRef<string>('');
109257
const lastFetchTimeRef = useRef<number>(0);
@@ -132,10 +280,31 @@ function RecentTrades() {
132280
// Add a small delay on initial mount to ensure session is settled
133281
const timeoutId = setTimeout(() => {
134282
fetchAllTrades();
283+
284+
// Also update Alpaca order status if credentials exist
285+
const hasCredentials = apiSettings?.alpaca_paper_api_key || apiSettings?.alpaca_live_api_key;
286+
if (hasCredentials) {
287+
console.log('Alpaca credentials detected, updating order status...');
288+
updateAlpacaOrderStatus();
289+
}
135290
}, 500);
136291

137292
return () => clearTimeout(timeoutId);
138-
}, [user?.id, isAuthenticated, fetchAllTrades]); // Include isAuthenticated in dependencies
293+
}, [user?.id, isAuthenticated, fetchAllTrades, apiSettings]); // Include isAuthenticated and apiSettings in dependencies
294+
295+
// Periodically update Alpaca order status
296+
useEffect(() => {
297+
const hasCredentials = apiSettings?.alpaca_paper_api_key || apiSettings?.alpaca_live_api_key;
298+
299+
if (!hasCredentials) return;
300+
301+
const interval = setInterval(() => {
302+
console.log('Periodic order status update...');
303+
updateAlpacaOrderStatus();
304+
}, 30000); // Check every 30 seconds
305+
306+
return () => clearInterval(interval);
307+
}, [apiSettings, user]);
139308

140309
const formatTimestamp = (timestamp: string) => {
141310
const date = new Date(timestamp);
@@ -232,7 +401,7 @@ function RecentTrades() {
232401

233402
const renderTradeCard = (decision: TradeDecision) => {
234403
const isPending = decision.status === 'pending';
235-
const isExecuted = decision.status === 'executed';
404+
const isExecuted = decision.alpacaOrderStatus === 'filled' || decision.alpacaOrderStatus === 'partially_filled';
236405
const isApproved = decision.status === 'approved';
237406
const isRejected = decision.status === 'rejected';
238407

@@ -373,30 +542,25 @@ function RecentTrades() {
373542
const status = decision.alpacaOrderStatus.toLowerCase();
374543
let variant: any = "outline";
375544
let icon = null;
376-
let displayText = decision.alpacaOrderStatus;
377545
let customClasses = "";
378546

547+
// Display the actual Alpaca status directly
379548
if (status === 'filled') {
380549
variant = "success";
381550
icon = <CheckCircle className="h-3 w-3 mr-1" />;
382-
displayText = "filled";
383551
} else if (status === 'partially_filled') {
384552
variant = "default";
385553
icon = <Clock className="h-3 w-3 mr-1" />;
386-
displayText = "partial filled";
387554
customClasses = "bg-blue-500 text-white border-blue-500";
388-
} else if (['new', 'pending_new', 'accepted'].includes(status)) {
555+
} else if (['new', 'pending_new', 'accepted', 'pending_replace', 'pending_cancel'].includes(status)) {
389556
variant = "warning";
390557
icon = <Clock className="h-3 w-3 mr-1" />;
391-
displayText = "placed";
392-
} else if (['canceled', 'cancelled'].includes(status)) {
558+
} else if (['canceled', 'cancelled', 'expired', 'replaced'].includes(status)) {
393559
variant = "destructive";
394560
icon = <XCircle className="h-3 w-3 mr-1" />;
395-
displayText = "failed";
396561
} else if (status === 'rejected') {
397562
variant = "destructive";
398563
icon = <XCircle className="h-3 w-3 mr-1" />;
399-
displayText = "rejected";
400564
}
401565

402566
return (
@@ -405,10 +569,10 @@ function RecentTrades() {
405569
className={`text-xs ${customClasses}`}
406570
>
407571
{icon}
408-
{displayText}
409-
{decision.alpacaFilledQty && status === 'partially_filled' ? (
572+
{decision.alpacaOrderStatus}
573+
{decision.alpacaFilledQty > 0 && status === 'partially_filled' && (
410574
<span className="ml-1">({decision.alpacaFilledQty}/{decision.quantity})</span>
411-
) : null}
575+
)}
412576
</Badge>
413577
);
414578
})()}
@@ -500,7 +664,13 @@ function RecentTrades() {
500664
variant="ghost"
501665
size="icon"
502666
className="h-7 w-7"
503-
onClick={() => fetchAllTrades()}
667+
onClick={() => {
668+
fetchAllTrades();
669+
const hasCredentials = apiSettings?.alpaca_paper_api_key || apiSettings?.alpaca_live_api_key;
670+
if (hasCredentials) {
671+
updateAlpacaOrderStatus();
672+
}
673+
}}
504674
disabled={loading}
505675
>
506676
{loading ? (

src/components/rebalance/RebalanceModal.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
DialogTitle,
1111
} from "@/components/ui/dialog";
1212
import { Button } from "@/components/ui/button";
13+
import { Badge } from "@/components/ui/badge";
1314
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
1415
import { Alert, AlertDescription } from "@/components/ui/alert";
1516
import { RefreshCw, Settings, List, AlertCircle } from "lucide-react";
@@ -156,6 +157,18 @@ export default function RebalanceModal({ isOpen, onClose, onApprove }: Rebalance
156157
Stock Selection
157158
</TabsTrigger>
158159
</TabsList>
160+
161+
{/* Stock Selection Limit Display */}
162+
{maxStocks > 0 && (
163+
<div className="mt-3 flex items-center justify-between text-sm">
164+
<span className="text-muted-foreground">
165+
Stock selection limit: {selectedPositions.size} / {maxStocks} stocks selected
166+
</span>
167+
{selectedPositions.size >= maxStocks && (
168+
<Badge variant="destructive" className="text-xs">Limit Reached</Badge>
169+
)}
170+
</div>
171+
)}
159172
</div>
160173

161174
<ConfigurationTab

src/components/rebalance/components/WorkflowExplanation.tsx

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,6 @@ interface WorkflowExplanationProps {
66
}
77

88
export function WorkflowExplanation({ rebalanceThreshold }: WorkflowExplanationProps) {
9-
return (
10-
<div className="pt-4 border-t">
11-
<h4 className="text-sm font-semibold mb-2">How Scheduled Rebalancing Works</h4>
12-
<div className="space-y-2 text-xs text-muted-foreground">
13-
<div className="flex items-start gap-2">
14-
<span className="font-medium">1.</span>
15-
<span>Schedule triggers at configured time intervals</span>
16-
</div>
17-
<div className="flex items-start gap-2">
18-
<span className="font-medium">2.</span>
19-
<span>Calculate allocation drift for all selected stocks</span>
20-
</div>
21-
<div className="flex items-start gap-2">
22-
<span className="font-medium">3.</span>
23-
<span>
24-
If max drift &lt; {rebalanceThreshold}%: Opportunity Agent evaluates market signals to identify high-priority stocks
25-
</span>
26-
</div>
27-
<div className="flex items-start gap-2">
28-
<span className="font-medium">4.</span>
29-
<span>
30-
If max drift &ge; {rebalanceThreshold}%: Analyze all selected stocks immediately
31-
</span>
32-
</div>
33-
<div className="flex items-start gap-2">
34-
<span className="font-medium">5.</span>
35-
<span>Run full multi-agent analysis on selected stocks</span>
36-
</div>
37-
<div className="flex items-start gap-2">
38-
<span className="font-medium">6.</span>
39-
<span>Portfolio Manager creates optimal rebalance trades</span>
40-
</div>
41-
</div>
42-
</div>
43-
);
9+
// Component removed - no longer showing workflow explanation
10+
return null;
4411
}

0 commit comments

Comments
 (0)