Files
AITrader/docs/superpowers/plans/2026-05-16-most-active-stocks-table-plan.md

364 lines
11 KiB
Markdown

# Most Active Stocks Table Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the StockViewer on `/stocks` with a table of most active stocks from Alpaca's screener API.
**Architecture:** Server proxy route calls Alpaca screener, React component fetches and auto-refreshes every 30s with clickable rows linking to `/analyze/TICKER`.
**Tech Stack:** React Router 7, React, TailwindCSS, fetch API, Alpaca Markets API
---
### Task 1: Add MostActiveStock type to types.ts
**Files:**
- Modify: `app/types.ts`
- [ ] **Step 1: Add MostActiveStock interface**
Add to `app/types.ts` after the `AlpacaAccount` interface:
```typescript
export interface MostActiveStock {
symbol: string;
name: string;
price: number;
changePercent: number;
volume: number;
}
```
- [ ] **Step 2: Commit**
```bash
git add app/types.ts
git commit -m "types: add MostActiveStock interface"
```
---
### Task 2: Create API route for most active stocks
**Files:**
- Create: `app/routes/api/stocks/most-actives.ts`
- [ ] **Step 1: Create the server proxy route**
Create `app/routes/api/stocks/most-actives.ts`:
```typescript
import type { MostActiveStock } from "../../../types";
const ALPACA_API_KEY = process.env.ALPACA_API_KEY!;
const ALPACA_SECRET_KEY = process.env.ALPACA_SECRET_KEY!;
const ALPACA_DATA_URL = process.env.ALPACA_DATA_URL || "https://data.alpaca.markets";
export async function loader() {
try {
const response = await fetch(`${ALPACA_DATA_URL}/v1beta1/screener/stocks/most-actives`, {
headers: {
"APCA-API-KEY-ID": ALPACA_API_KEY,
"APCA-API-SECRET-KEY": ALPACA_SECRET_KEY,
},
});
if (!response.ok) {
throw new Error(`Alpaca API error: ${response.status}`);
}
const data = await response.json();
const stocks: MostActiveStock[] = (data.most_actives || []).map((item: any) => ({
symbol: item.symbol,
name: item.name || item.symbol,
price: parseFloat(item.price) || 0,
changePercent: parseFloat(item.change_percent) || 0,
volume: parseInt(item.volume) || 0,
}));
return Response.json(stocks);
} catch (error) {
console.error("Most active stocks API error:", error);
const message = error instanceof Error ? error.message : "Unknown error";
return Response.json(
{ error: `Failed to fetch most active stocks: ${message}` },
{ status: 500 }
);
}
}
```
- [ ] **Step 2: Commit**
```bash
git add app/routes/api/stocks/most-actives.ts
git commit -m "feat: add most-actives API proxy route"
```
---
### Task 3: Register the new API route
**Files:**
- Modify: `app/routes.ts`
- [ ] **Step 1: Add route entry**
Add this line to `app/routes.ts` after the existing `api/stocks` route (line 11):
```typescript
route("api/stocks/most-actives", "routes/api/stocks/most-actives.ts"),
```
The routes array should look like:
```typescript
export default [
index("routes/landing.tsx"),
route("api/alpaca/account", "routes/api/alpaca/account.ts"),
route("api/alpaca/quote/:ticker", "routes/api/alpaca/quote.ts"),
route("api/alpaca/orders", "routes/api/alpaca/orders.ts"),
route("api/alpaca/positions", "routes/api/alpaca/positions.ts"),
route("api/indicators", "routes/api/indicators.ts"),
route("api/analyze", "routes/api/analyze.ts"),
route("api/stocks", "routes/api/stocks/index.ts"),
route("api/stocks/most-actives", "routes/api/stocks/most-actives.ts"),
route("stocks", "routes/stocks.tsx"),
route("analyze", "routes/analyze.tsx"),
route("analyze/:ticker", "routes/analyze.ticker.tsx"),
] satisfies RouteConfig;
```
- [ ] **Step 2: Commit**
```bash
git add app/routes.ts
git commit -m "routes: register most-actives API endpoint"
```
---
### Task 4: Create MostActiveStocks component
**Files:**
- Create: `app/components/MostActiveStocks.tsx`
- [ ] **Step 1: Create the component**
Create `app/components/MostActiveStocks.tsx`:
```typescript
import { useState, useEffect, useCallback } from "react";
import { Link } from "react-router";
import type { MostActiveStock } from "../types";
function formatVolume(vol: number): string {
if (vol >= 1_000_000_000) return `${(vol / 1_000_000_000).toFixed(1)}B`;
if (vol >= 1_000_000) return `${(vol / 1_000_000).toFixed(1)}M`;
if (vol >= 1_000) return `${(vol / 1_000).toFixed(1)}K`;
return vol.toString();
}
function formatPrice(price: number): string {
return `$${price.toFixed(2)}`;
}
function formatChangePercent(pct: number): string {
return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
}
export default function MostActiveStocks() {
const [stocks, setStocks] = useState<MostActiveStock[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setError(null);
const res = await fetch("/api/stocks/most-actives");
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Failed to fetch data");
}
const data = await res.json();
setStocks(data);
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to fetch most active stocks.";
setError(message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, [fetchData]);
if (loading) {
return (
<div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
<div className="space-y-3">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="animate-pulse flex gap-4 py-3 border-b border-gray-100 last:border-0">
<div className="h-5 bg-gray-200 rounded w-16" />
<div className="h-5 bg-gray-200 rounded w-32" />
<div className="h-5 bg-gray-200 rounded w-20" />
<div className="h-5 bg-gray-200 rounded w-20" />
<div className="h-5 bg-gray-200 rounded w-24" />
</div>
))}
</div>
</div>
);
}
if (error && stocks.length === 0) {
return (
<div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-600 text-sm mb-3">{error}</p>
<button
onClick={fetchData}
className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors"
>
Retry
</button>
</div>
</div>
);
}
return (
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
{error && (
<div className="bg-red-50 border-b border-red-200 px-6 py-3">
<p className="text-red-600 text-sm">{error}</p>
</div>
)}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="text-left px-6 py-4 text-sm font-semibold text-gray-600">Symbol</th>
<th className="text-left px-6 py-4 text-sm font-semibold text-gray-600">Name</th>
<th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Price</th>
<th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Change %</th>
<th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Volume</th>
</tr>
</thead>
<tbody>
{stocks.map((stock) => (
<tr key={stock.symbol} className="border-b border-gray-100 hover:bg-gray-50 transition-colors">
<td className="px-6 py-4">
<Link
to={`/analyze/${stock.symbol}`}
className="text-blue-600 font-semibold hover:text-blue-700 hover:underline"
>
{stock.symbol}
</Link>
</td>
<td className="px-6 py-4 text-gray-600">{stock.name}</td>
<td className="px-6 py-4 text-right font-mono text-gray-900">{formatPrice(stock.price)}</td>
<td className={`px-6 py-4 text-right font-mono font-medium ${stock.changePercent >= 0 ? "text-green-600" : "text-red-600"}`}>
{formatChangePercent(stock.changePercent)}
</td>
<td className="px-6 py-4 text-right text-gray-600">{formatVolume(stock.volume)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
```
- [ ] **Step 2: Commit**
```bash
git add app/components/MostActiveStocks.tsx
git commit -m "feat: add MostActiveStocks table component with auto-refresh"
```
---
### Task 5: Update stocks.tsx page
**Files:**
- Modify: `app/routes/stocks.tsx`
- [ ] **Step 1: Replace StockViewer with MostActiveStocks**
Replace the entire contents of `app/routes/stocks.tsx`:
```typescript
import MostActiveStocks from "../components/MostActiveStocks";
import Navbar from "../components/Navbar";
export default function Stocks() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50">
<Navbar />
<div className="py-20">
<div className="mx-auto max-w-7xl px-6 sm:px-8 lg:px-8">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Most Active Stocks
</h1>
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
Real-time view of the most actively traded stocks, auto-refreshing every 30 seconds.
</p>
</div>
<MostActiveStocks />
</div>
</div>
</div>
);
}
```
- [ ] **Step 2: Commit**
```bash
git add app/routes/stocks.tsx
git commit -m "feat: replace StockViewer with MostActiveStocks on stocks page"
```
---
### Task 6: Verify and test
**Files:**
- All modified files
- [ ] **Step 1: Run typecheck**
```bash
npm run typecheck
```
Expected: No type errors. If there are errors, fix them before proceeding.
- [ ] **Step 2: Run dev server and verify**
```bash
npm run dev
```
Navigate to `http://localhost:5173/stocks` and verify:
- Table loads with most active stocks
- Symbol links navigate to `/analyze/TICKER`
- Change % is color-coded (green for positive, red for negative)
- Data auto-refreshes every 30 seconds
- Loading skeleton shows on initial load
- Error state shows with retry button if API fails
- [ ] **Step 3: Final commit (if any fixes needed)**
```bash
git add -A
git commit -m "fix: address typecheck/dev issues"
```