/* TRADINGGRAPH related file */ // Server-only imports are loaded dynamically inside the action to avoid client bundling issues const JOB_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes export async function action({ request }: { request: Request }) { const body = await request.json(); const ticker = body.ticker?.toUpperCase(); const date = body.date || new Date().toISOString().split("T")[0]; if (!ticker) { return Response.json({ error: "ticker is required" }, { status: 400 }); } const { db } = await import("../../lib/db.server"); const { fetchAccount, fetchRecentCloses, fetchBars } = await import("../../lib/alpacaClient"); const { getJob, listRecentJobs, cancelJob } = await import("../../lib/queue"); // Clean up old unfinished jobs for this ticker (older than timeout) try { const recentJobs = await listRecentJobs(ticker, 50); for (const j of recentJobs) { if (j.state === "waiting" || j.state === "active" || j.state === "delayed") { // Check if the job is too old const jobCreatedAt = j.data?.timestamp; if (jobCreatedAt && Date.now() - jobCreatedAt > JOB_TIMEOUT_MS) { await cancelJob(j.id); } } } } catch (e) { /* ignore cleanup errors */ } // Check if there's a recent unfinished job that can be reused try { const recentJobs = await listRecentJobs(ticker, 10); const activeJob = recentJobs.find((j: any) => j.state === "waiting" || j.state === "active"); if (activeJob) { // Return existing job ID instead of creating a new one const jobId = activeJob.id; // Update the stock record with this job ID await db.stock.upsert({ where: { ticker }, update: { lastJobId: jobId }, create: { ticker, lastJobId: jobId }, }); return Response.json({ status: "queued", jobId }, { status: 202 }); } } catch (e) { /* ignore */ } // Fetch latest Alpaca account and recent prices let account: any = undefined; let prices: number[] = []; let recentBars: any[] = []; try { account = await fetchAccount(); prices = await fetchRecentCloses(ticker); try { recentBars = await fetchBars(ticker, '1Min', { limit: 200 }); if (recentBars && recentBars.length) { prices = recentBars.map((b: any) => (typeof b.ClosePrice === 'number' ? b.ClosePrice : (typeof b.c === 'number' ? b.c : 0))).filter((p: number) => p > 0); } } catch (barErr) { console.warn('[analyze] Failed to fetch recent bars:', barErr); } } catch (e) { console.error("[analyze] Failed to fetch Alpaca data:", e); return Response.json({ error: "Failed to fetch Alpaca data: " + String(e) }, { status: 502 }); } const input = { financialData: `Financial data for ${ticker} as of ${date}`, technicalData: { prices, bars: recentBars, sma: 0, ema: 0, rsi: 0, macd: 0, }, sentimentData: { headlines: [`${ticker} showing positive momentum`], source: "news" as const, }, account, timestamp: Date.now(), }; // Always enqueue as background job try { const { enqueueAnalyze } = await import("../../lib/queue"); const jobId = await enqueueAnalyze(ticker, input); // Save jobId to DB stock record await db.stock.upsert({ where: { ticker }, update: { lastJobId: jobId }, create: { ticker, lastJobId: jobId }, }); return Response.json({ status: "queued", jobId }, { status: 202 }); } catch (enqueueErr) { console.error("[analyze] enqueue error:", enqueueErr); return Response.json({ error: "failed to enqueue" }, { status: 500 }); } }