Files
AITrader/docs/superpowers/plans/2026-05-12-stocks-route-plan.md
T
henry 8429db504a feat: add stock indicators route and Alpaca account info
- New /stocks route with StockViewer component
- New /api/indicators endpoint with SMA, EMA, RSI, MACD
- New /api/alpaca/account endpoint
- AlpacaAccountInfo component on home page
- Indicator calculation utilities
- Tests for utilities and components
- Vite proxy config for /api
2026-05-12 21:07:18 +02:00

16 KiB

Stock Indicators & Alpaca Account Info 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: Add a new /stocks route that displays stock indicators and show Alpaca account information on the home page.

Architecture:

  • Backend: New API endpoint /api/indicators that fetches historic prices from Alpaca and calculates indicators (SMA, EMA, RSI, etc.).
  • Frontend: New route /stocks with a component that allows users to input a stock symbol, fetch indicators, and display them in a table.
  • Home page: Add a component showing Alpaca account balance, buying power, and positions.

Tech Stack: React Router, TypeScript, TailwindCSS, Alpaca API, Node.js.


Task 1: Create backend API endpoint for stock indicators

Files:

  • Create: app/api/indicators.ts

  • Modify: app/api/+types.ts (add response type)

  • Test: app/api/__tests__/indicators.test.ts

  • Step 1: Define response type

// app/api/+types.ts
export interface IndicatorData {
  symbol: string;
  indicators: {
    sma: number;
    ema: number;
    rsi: number;
    macd: number;
    // add more as needed
  };
}
  • Step 2: Implement the endpoint
// app/api/indicators.ts
import { NextResponse } from "@react-router/server";
import { type RequestHandler } from "./+types";
import { type IndicatorData } from "./+types";

export const GET: RequestHandler = async ({ request, params }) => {
  const url = new URL(request.url);
  const symbol = url.searchParams.get("symbol");

  if (!symbol) {
    return NextResponse.json(
      { error: "Symbol is required" },
      { status: 400 }
    );
  }

  try {
    // Call Alpaca historic prices
    // Calculate indicators
    // Return JSON
    const data: IndicatorData = {
      symbol,
      indicators: {
        sma: 0,
        ema: 0,
        rsi: 0,
        macd: 0,
      },
    };
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to fetch indicators" },
      { status: 500 }
    );
  }
};
  • Step 3: Write test for endpoint
// app/api/__tests__/indicators.test.ts
import { GET } from "../indicators";

describe("GET /api/indicators", () => {
  it("returns indicators for a valid symbol", async () => {
    const req = new Request("http://localhost:5173/api/indicators?symbol=AAPL");
    const res = await GET({ request: req, params: {} } as any);
    expect(res).toBeInstanceOf(Response);
    const json = await res.json();
    expect(json).toHaveProperty("symbol");
    expect(json).toHaveProperty("indicators");
  });

  it("returns error for missing symbol", async () => {
    const req = new Request("http://localhost:5173/api/indicators");
    const res = await GET({ request: req, params: {} } as any);
    expect(res.status).toBe(400);
  });
});
  • Step 4: Run tests
npm run typecheck && npm test
  • Step 5: Commit
git add app/api/indicators.ts app/api/+types.ts app/api/__tests__/indicators.test.ts
git commit -m "feat: add indicators API endpoint"

Task 2: Create StockViewer component for the /stocks route

Files:

  • Create: app/components/StockViewer.tsx

  • Create: app/routes/stocks.tsx

  • Modify: app/routes.ts (add new route)

  • Test: app/components/__tests__/StockViewer.test.tsx

  • Step 1: Implement StockViewer component

// app/components/StockViewer.tsx
import { useState } from "react";

export default function StockViewer() {
  const [symbol, setSymbol] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [indicators, setIndicators] = useState<any>(null);

  const fetchIndicators = async (symbol: string) => {
    setLoading(true);
    setError(null);
    try {
      const res = await fetch(`/api/indicators?symbol=${symbol}`);
      if (!res.ok) throw new Error("Failed to fetch");
      const data = await res.json();
      setIndicators(data.indicators);
    } catch (err) {
      setError("Failed to fetch indicators");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">Stock Indicators</h1>
      <div className="mb-4">
        <input
          type="text"
          value={symbol}
          onChange={(e) => setSymbol(e.target.value)}
          placeholder="Enter stock symbol"
          className="border p-2 mr-2"
        />
        <button
          onClick={() => fetchIndicators(symbol)}
          disabled={loading}
          className="bg-blue-500 text-white px-4 py-2"
        >
          {loading ? "Loading..." : "Get Indicators"}
        </button>
      </div>
      {error && <p className="text-red-500">{error}</p>}
      {indicators && (
        <div className="mt-4">
          <h2 className="text-xl">Results for {symbol}</h2>
          <table className="w-full border-collapse">
            <thead>
              <tr className="border-b">
                <th className="text-left p-2">Indicator</th>
                <th className="text-left p-2">Value</th>
              </tr>
            </thead>
            <tbody>
              {Object.entries(indicators).map(([key, value]) => (
                <tr key={key} className="border-b">
                  <td className="p-2">{key.toUpperCase()}</td>
                  <td className="p-2">{value.toFixed(2)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}
  • Step 2: Create route component
// app/routes/stocks.tsx
import StockViewer from "../components/StockViewer";

export default function Stocks() {
  return <StockViewer />;
}
  • Step 3: Add route to routes.ts
// app/routes.ts
import { type RouteConfig, index } from "@react-router/dev/routes";
import Home from "./routes/home.tsx";
import Stocks from "./routes/stocks.tsx";

export default [
  index("routes/home.tsx", Home),
  index("routes/stocks.tsx", Stocks),
] satisfies RouteConfig;
  • Step 4: Write test for StockViewer
// app/components/__tests__/StockViewer.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import StockViewer from "../StockViewer";

jest.mock("react", () => ({
  ...jest.requireActual("react"),
  useState: jest.fn(),
}));

describe("StockViewer", () => {
  it("displays indicators after fetching", async () => {
    // Mock fetch
    const mockData = { indicators: { sma: 100, ema: 120, rsi: 50, macd: 0.5 } };
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve(mockData),
      })
    ) as any;

    render(<StockViewer />);
    const input = screen.getByPlaceholderText("Enter stock symbol");
    const button = screen.getByText("Get Indicators");

    await userEvent.type(input, "AAPL");
    await userEvent.click(button);

    await waitFor(() => {
      expect(screen.getByText("Results for AAPL")).toBeInTheDocument();
    });
  });
});
  • Step 5: Run tests
npm run typecheck && npm test
  • Step 6: Commit
git add app/components/StockViewer.tsx app/routes/stocks.tsx app/routes.ts app/components/__tests__/StockViewer.test.tsx
git commit -m "feat: add stocks route with indicator viewer"

Task 3: Add Alpaca account info component to home page

Files:

  • Create: app/components/AlpacaAccountInfo.tsx

  • Modify: app/routes/home.tsx

  • Test: app/components/__tests__/AlpacaAccountInfo.test.tsx

  • Step 1: Implement AlpacaAccountInfo component

// app/components/AlpacaAccountInfo.tsx
import { useState, useEffect } from "react";

export default function AlpacaAccountInfo() {
  const [account, setAccount] = useState<any>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchAccount = async () => {
      try {
        const res = await fetch("/api/alpaca/account");
        if (!res.ok) throw new Error("Failed to fetch");
        const data = await res.json();
        setAccount(data);
      } catch (err) {
        setError("Failed to fetch account info");
      }
    };
    fetchAccount();
  }, []);

  if (error) return <p className="text-red-500">{error}</p>;
  if (!account) return <p>Loading account...</p>;

  return (
    <div className="bg-gray-100 p-4 rounded-lg">
      <h2 className="text-lg font-bold mb-2">Alpaca Account</h2>
      <div className="space-y-2">
        <p>Balance: ${account.cash}</p>
        <p>Buying Power: ${account.buying_power}</p>
        <p>Portfolio Value: ${account.portfolio_value}</p>
      </div>
    </div>
  );
}
  • Step 2: Add component to home page
// app/routes/home.tsx
import { Welcome } from "../welcome/welcome";
import AlpacaAccountInfo from "../components/AlpacaAccountInfo";

export default function Home() {
  return (
    <>
      <Welcome />
      <div className="container mx-auto p-4">
        <AlpacaAccountInfo />
      </div>
    </>
  );
}
  • Step 3: Create backend endpoint for Alpaca account
// app/api/alpaca/account.ts
import { NextResponse } from "@react-router/server";
import { type RequestHandler } from "./+types";

export const GET: RequestHandler = async () => {
  // Call Alpaca API to get account info
  // Return JSON with cash, buying_power, portfolio_value
  const account = {
    cash: 10000,
    buying_power: 20000,
    portfolio_value: 30000,
  };
  return NextResponse.json(account);
};
  • Step 4: Add type for Alpaca account
// app/api/+types.ts
export interface AlpacaAccount {
  cash: number;
  buying_power: number;
  portfolio_value: number;
}
  • Step 5: Write test for AlpacaAccountInfo
// app/components/__tests__/AlpacaAccountInfo.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import AlpacaAccountInfo from "../AlpacaAccountInfo";

jest.mock("react", () => ({
  ...jest.requireActual("react"),
  useState: jest.fn(),
  useEffect: jest.fn(),
}));

describe("AlpacaAccountInfo", () => {
  it("displays account info", async () => {
    // Mock fetch
    global.fetch = jest.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve({ cash: 10000, buying_power: 20000, portfolio_value: 30000 }),
      })
    ) as any;

    render(<AlpacaAccountInfo />);
    await waitFor(() => {
      expect(screen.getByText("Alpaca Account")).toBeInTheDocument();
    });
  });
});
  • Step 6: Run tests
npm run typecheck && npm test
  • Step 7: Commit
git add app/components/AlpacaAccountInfo.tsx app/api/alpaca/account.ts app/api/+types.ts app/routes/home.tsx app/components/__tests__/AlpacaAccountInfo.test.tsx
git commit -m "feat: add Alpaca account info to home page"

Task 4: Update backend to calculate indicators (add indicator calculation logic)

Files:

  • Create: app/utils/indicators.ts

  • Modify: app/api/indicators.ts

  • Test: app/utils/__tests__/indicators.test.ts

  • Step 1: Implement indicator calculation functions

// app/utils/indicators.ts
export function calculateSMA(prices: number[], period: number = 20): number {
  if (prices.length < period) return 0;
  const sum = prices.slice(0, period).reduce((a, b) => a + b, 0);
  return sum / period;
}

export function calculateEMA(prices: number[], period: number = 20): number {
  if (prices.length < period) return 0;
  const multiplier = 2 / (period + 1);
  let ema = prices[period - 1];
  for (let i = period; i < prices.length; i++) {
    ema = prices[i] * multiplier + ema * (1 - multiplier);
  }
  return ema;
}

export function calculateRSI(prices: number[], period: number = 14): number {
  if (prices.length < period + 1) return 0;
  let gains = 0;
  let losses = 0;
  for (let i = 1; i <= period; i++) {
    const diff = prices[i] - prices[i - 1];
    if (diff > 0) gains += diff;
    else losses -= diff;
  }
  const avgGain = gains / period;
  const avgLoss = losses / period;
  const rs = avgGain / avgLoss;
  return 100 - (100 / (1 + rs));
}

export function calculateMACD(prices: number[], fastPeriod: number = 12, slowPeriod: number = 26, signalPeriod: number = 9): number {
  if (prices.length < slowPeriod) return 0;
  const emaFast = calculateEMA(prices, fastPeriod);
  const emaSlow = calculateEMA(prices, slowPeriod);
  const macdLine = emaFast - emaSlow;
  // Signal line (EMA of MACD line)
  const signal = calculateEMA([macdLine], signalPeriod);
  return macdLine - signal;
}
  • Step 2: Use these functions in indicators endpoint
// app/api/indicators.ts
import { calculateSMA, calculateEMA, calculateRSI, calculateMACD } from "../utils/indicators";

export const GET: RequestHandler = async ({ request, params }) => {
  const url = new URL(request.url);
  const symbol = url.searchParams.get("symbol");

  if (!symbol) {
    return NextResponse.json(
      { error: "Symbol is required" },
      { status: 400 }
    );
  }

  try {
    // Fetch historic prices from Alpaca
    const prices = await fetchHistoricPrices(symbol);
    // Calculate indicators
    const sma = calculateSMA(prices);
    const ema = calculateEMA(prices);
    const rsi = calculateRSI(prices);
    const macd = calculateMACD(prices);

    const data = {
      symbol,
      indicators: { sma, ema, rsi, macd },
    };
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to fetch indicators" },
      { status: 500 }
    );
  }
};

async function fetchHistoricPrices(symbol: string): Promise<number[]> {
  // Implement Alpaca API call
  // Return array of closing prices
  return [100, 102, 101, 105, 107, 110, 108, 115, 120, 119];
}
  • Step 3: Write tests for indicator calculations
// app/utils/__tests__/indicators.test.ts
import { calculateSMA, calculateEMA, calculateRSI, calculateMACD } from "../indicators";

describe("Indicator calculations", () => {
  it("calculates SMA correctly", () => {
    const prices = [1, 2, 3, 4, 5];
    expect(calculateSMA(prices, 3)).toBe(3);
  });

  it("calculates EMA correctly", () => {
    const prices = [1, 2, 3, 4, 5];
    expect(calculateEMA(prices, 3)).toBeCloseTo(3.5);
  });

  it("calculates RSI correctly", () => {
    const prices = [100, 102, 101, 105, 107, 110, 108, 115, 120, 119];
    expect(calculateRSI(prices, 5)).toBeCloseTo(66.7);
  });

  it("calculates MACD correctly", () => {
    const prices = [100, 102, 101, 105, 107, 110, 108, 115, 120, 119];
    expect(calculateMACD(prices)).toBeCloseTo(2.5);
  });
});
  • Step 4: Run tests
npm run typecheck && npm test
  • Step 5: Commit
git add app/utils/indicators.ts app/api/indicators.ts app/utils/__tests__/indicators.test.ts
git commit -m "feat: add indicator calculation utilities"

Task 5: Final integration and testing

Files:

  • Modify: package.json (add test script if needed)

  • Modify: vite.config.ts (optional proxy)

  • Step 1: Ensure Vite proxy is set up for API calls

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:5173",
        changeOrigin: true,
      },
    },
  },
});
  • Step 2: Run the full development server
npm run dev
  • Step 3: Verify the new route works

  • Navigate to http://localhost:5173/stocks

  • Enter a symbol (e.g., AAPL) and confirm indicators appear

  • Step 4: Verify Alpaca account info on home page

  • Visit http://localhost:5173/ and confirm account info displays

  • Step 5: Run all tests

npm test
  • Step 6: Run typecheck
npm run typecheck
  • Step 7: Commit final changes
git add vite.config.ts
git commit -m "chore: configure Vite proxy for API calls"

Plan complete and saved to docs/superpowers/plans/2026-05-12-stocks-route-plan.md. Two execution options:

1. Subagent-Driven (recommended) - I dispatch a fresh subagent per task, review between tasks, fast iteration

2. Inline Execution - Execute tasks in this session using executing-plans, batch execution with checkpoints

Which approach?