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

11 KiB

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:

export interface MostActiveStock {
  symbol: string;
  name: string;
  price: number;
  changePercent: number;
  volume: number;
}
  • Step 2: Commit
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:

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
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):

route("api/stocks/most-actives", "routes/api/stocks/most-actives.ts"),

The routes array should look like:

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
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:

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
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:

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
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

npm run typecheck

Expected: No type errors. If there are errors, fix them before proceeding.

  • Step 2: Run dev server and verify
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)

git add -A
git commit -m "fix: address typecheck/dev issues"