@@ -6,6 +6,7 @@ import { ArrowUpRight, ArrowDownRight, Clock, CheckCircle, XCircle, TrendingUp,
66import { alpacaAPI } from "@/lib/alpaca" ;
77import { useAuth , isSessionValid } from "@/lib/auth" ;
88import { supabase } from "@/lib/supabase" ;
9+ import { getCachedSession } from "@/lib/cachedAuth" ;
910import { useToast } from "@/hooks/use-toast" ;
1011import AnalysisDetailModal from "@/components/AnalysisDetailModal" ;
1112import 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 ? (
0 commit comments