From 2e22fd5635cc01a314c0de5b6b6cb411323761b9 Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Thu, 14 May 2026 11:00:35 +0200 Subject: [PATCH] feat: add stock detail page with chart, position, and orders - Add /api/alpaca/orders endpoint for order history - Add TradingView chart component for candlestick visualization - Add /analyze/:ticker route with position and orders display - Make ticker cells in analyze page clickable for navigation --- .env.example | 3 +- app/components/TradingViewChart.tsx | 35 +++ .../__tests__/TradingViewChart.test.tsx | 36 +++ app/routes.ts | 2 + app/routes/analyze.ticker.tsx | 52 ++++ app/routes/analyze.tsx | 9 +- app/routes/api/alpaca/orders.ts | 18 ++ .../plans/2026-05-14-stock-detail-page.md | 289 ++++++++++++++++++ .../2026-05-14-stock-detail-page-design.md | 74 +++++ package-lock.json | 16 + package.json | 1 + playwright-report/index.html | 2 +- prisma/dev.db | Bin 24576 -> 24576 bytes tests/orders.test.ts | 8 + 14 files changed, 541 insertions(+), 4 deletions(-) create mode 100644 app/components/TradingViewChart.tsx create mode 100644 app/components/__tests__/TradingViewChart.test.tsx create mode 100644 app/routes/analyze.ticker.tsx create mode 100644 app/routes/api/alpaca/orders.ts create mode 100644 docs/superpowers/plans/2026-05-14-stock-detail-page.md create mode 100644 docs/superpowers/specs/2026-05-14-stock-detail-page-design.md create mode 100644 tests/orders.test.ts diff --git a/.env.example b/.env.example index 700bed3..62dac73 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ ALPACA_API_KEY=your_alpaca_api_key_here ALPACA_SECRET_KEY=your_alpaca_secret_key_here ALPACA_BASE_URL=https://paper-api.alpaca.markets -OPENROUTER_API_KEY=your_openrouter_api_key_here \ No newline at end of file +OPENROUTER_API_KEY=your_openrouter_api_key_here +BASE_URL=http://localhost:5173 \ No newline at end of file diff --git a/app/components/TradingViewChart.tsx b/app/components/TradingViewChart.tsx new file mode 100644 index 0000000..ed5e317 --- /dev/null +++ b/app/components/TradingViewChart.tsx @@ -0,0 +1,35 @@ +import { useEffect, useRef } from "react"; +import * as LightweightCharts from "lightweight-charts"; + +interface TradingViewChartProps { + ticker: string; + data?: Array<{ time: string; open: number; high: number; low: number; close: number }>; +} + +export default function TradingViewChart({ ticker, data }: TradingViewChartProps) { + const containerRef = useRef(null); + + useEffect(() => { + if (!containerRef.current) return; + + const chart = LightweightCharts.createChart(containerRef.current, { + width: containerRef.current.clientWidth, + height: 400, + }); + + const candlestickSeries = chart.addSeries(LightweightCharts.CandlestickSeries); + + if (data && data.length > 0) { + candlestickSeries.setData(data); + } + + return () => chart.remove(); + }, [data]); + + return ( +
+

{ticker} Price Chart

+
+
+ ); +} \ No newline at end of file diff --git a/app/components/__tests__/TradingViewChart.test.tsx b/app/components/__tests__/TradingViewChart.test.tsx new file mode 100644 index 0000000..447a776 --- /dev/null +++ b/app/components/__tests__/TradingViewChart.test.tsx @@ -0,0 +1,36 @@ +/// +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import TradingViewChart from "../TradingViewChart"; + +// Mock lightweight-charts +vi.mock("lightweight-charts", () => ({ + createChart: vi.fn(() => ({ + addSeries: vi.fn(() => ({ + setData: vi.fn(), + })), + remove: vi.fn(), + })), + CandlestickSeries: {}, +})); + +describe("TradingViewChart", () => { + it("renders the ticker symbol as heading", () => { + render(); + expect(screen.getByText("AAPL Price Chart")).toBeInTheDocument(); + }); + + it("renders without data prop", () => { + render(); + expect(screen.getByText("MSFT Price Chart")).toBeInTheDocument(); + }); + + it("renders with data prop", () => { + const data = [ + { time: "2024-01-01", open: 100, high: 110, low: 95, close: 105 }, + { time: "2024-01-02", open: 105, high: 115, low: 100, close: 110 }, + ]; + render(); + expect(screen.getByText("GOOGL Price Chart")).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/app/routes.ts b/app/routes.ts index 3b5ad8b..ccf5559 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -4,10 +4,12 @@ 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("stocks", "routes/stocks.tsx"), route("analyze", "routes/analyze.tsx"), + route("analyze/:ticker", "routes/analyze.ticker.tsx"), ] satisfies RouteConfig; \ No newline at end of file diff --git a/app/routes/analyze.ticker.tsx b/app/routes/analyze.ticker.tsx new file mode 100644 index 0000000..ef1b7a1 --- /dev/null +++ b/app/routes/analyze.ticker.tsx @@ -0,0 +1,52 @@ +import { useLoaderData } from "react-router"; +import TradingViewChart from "../components/TradingViewChart"; +import Navbar from "../components/Navbar"; + +export const meta = () => [{ title: "Stock Detail - AITrader" }]; + +interface LoaderData { + ticker: string; + position: number | null; + orders: any[]; +} + +export async function loader({ params }: { params: { ticker: string } }) { + const ticker = params.ticker?.toUpperCase() || ""; + + // Fetch position + const posRes = await fetch(`${process.env.BASE_URL}/api/alpaca/positions`); + const positions = posRes.ok ? await posRes.json() : []; + const position = positions.find((p: any) => p.ticker === ticker)?.qty ?? null; + + // Fetch orders + const ordRes = await fetch(`${process.env.BASE_URL}/api/alpaca/orders`); + const ordersData = ordRes.ok ? await ordRes.json() : { orders: [] }; + const orders = ordersData.orders?.filter((o: any) => o.symbol === ticker) || []; + + return Response.json({ ticker, position, orders }); +} + +export default function StockDetail() { + const { ticker, position, orders } = useLoaderData() as LoaderData; + + return ( +
+ +
+

{ticker} Detail

+ + + +
+

Position

+

{position ? `Qty: ${position}` : "No position"}

+
+ +
+

Recent Orders

+ {orders.length === 0 ?

No orders

:
{JSON.stringify(orders, null, 2)}
} +
+
+
+ ); +} \ No newline at end of file diff --git a/app/routes/analyze.tsx b/app/routes/analyze.tsx index 8c8fa53..c242bf8 100644 --- a/app/routes/analyze.tsx +++ b/app/routes/analyze.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import { Link } from "react-router"; import Navbar from "../components/Navbar"; import type { TradingDecision } from "../types/agents"; @@ -321,11 +322,15 @@ export default function Analyze() { {stocks.map((stock) => ( - {stock.ticker} + + + {stock.ticker} + + {stock.currentPrice ? `$${stock.currentPrice.toFixed(2)}` : "-"} - + {stock.position} diff --git a/app/routes/api/alpaca/orders.ts b/app/routes/api/alpaca/orders.ts new file mode 100644 index 0000000..b18acdc --- /dev/null +++ b/app/routes/api/alpaca/orders.ts @@ -0,0 +1,18 @@ +import Alpaca from "@alpacahq/alpaca-trade-api"; + +const alpaca = new Alpaca({ + keyId: process.env.ALPACA_API_KEY!, + secretKey: process.env.ALPACA_SECRET_KEY!, + baseUrl: process.env.ALPACA_BASE_URL || "https://paper-api.alpaca.markets", + retryOnError: false, +}); + +export async function loader() { + try { + const orders = await alpaca.getOrders(); + return Response.json({ orders }); + } catch (error) { + console.error("Alpaca orders error:", error); + return Response.json({ orders: [] }, { status: 500 }); + } +} \ No newline at end of file diff --git a/docs/superpowers/plans/2026-05-14-stock-detail-page.md b/docs/superpowers/plans/2026-05-14-stock-detail-page.md new file mode 100644 index 0000000..13ac676 --- /dev/null +++ b/docs/superpowers/plans/2026-05-14-stock-detail-page.md @@ -0,0 +1,289 @@ +# Stock Detail Page 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:** Create stock detail page at `/analyze/:ticker` with TradingView chart, position, orders, and trading graph results. + +**Architecture:** Dynamic route `analyze.ticker.tsx` that fetches ticker data, runs TradingGraph analysis, and displays chart, position, orders, and results. + +**Tech Stack:** React Router 7, TradingView lightweight charts, Alpaca API, Prisma + +--- + +## Prerequisite Check + +- [ ] Verify Alpaca API key is configured in `.env` +- [ ] Verify `@tradingview/lightweight-charts` can be installed + +--- + +### Task 1: Add Alpaca Orders API Endpoint + +**Files:** +- Create: `app/routes/api/alpaca/orders.ts` + +- [ ] **Step 1: Write the failing test** +```typescript +// tests/orders.test.ts +import { test, expect } from "@playwright/test"; + +test("GET /api/alpaca/orders returns orders list", async ({ page }) => { + const res = await page.request.get("/api/alpaca/orders"); + expect(res.ok()).toBeTruthy(); + const data = await res.json(); + expect(data.orders).toBeDefined(); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** +Run: `npm run test:e2e` +Expected: 404 Not Found + +- [ ] **Step 3: Write minimal implementation** +```typescript +import Alpaca from "@alpacahq/alpaca-trade-api"; + +const alpaca = new Alpaca({ + keyId: process.env.ALPACA_API_KEY!, + secretKey: process.env.ALPACA_SECRET_KEY!, + baseUrl: process.env.ALPACA_BASE_URL || "https://paper-api.alpaca.markets", + retryOnError: false, +}); + +export async function loader() { + try { + const orders = await alpaca.getOrders(); + return Response.json({ orders }); + } catch (error) { + console.error("Alpaca orders error:", error); + return Response.json({ orders: [] }, { status: 500 }); + } +} +``` + +- [ ] **Step 4: Register route in routes.ts** +```typescript +route("api/alpaca/orders", "routes/api/alpaca/orders.ts"), +``` + +- [ ] **Step 5: Run test to verify it passes** +Run: `npm run test:e2e -- tests/orders.test.ts` +Expected: PASS + +- [ ] **Step 6: Commit** +```bash +git add app/routes/api/alpaca/orders.ts tests/orders.test.ts +git commit -m "feat: add alpaca orders API endpoint" +``` + +--- + +### Task 2: Install TradingView Lightweight Charts + +**Files:** +- None (npm install) + +- [ ] **Step 1: Install dependency** +```bash +npm install @tradingview/lightweight-charts +``` + +- [ ] **Step 2: Run typecheck to verify installation** +Run: `npx tsc --noEmit` +Expected: No errors + +- [ ] **Step 3: Commit** +```bash +git add package.json package-lock.json +git commit -m "feat: install tradingview lightweight charts" +``` + +--- + +### Task 3: Create TradingView Chart Component + +**Files:** +- Create: `app/components/TradingViewChart.tsx` + +- [ ] **Step 1: Write the component** +```typescript +import { useEffect, useRef } from "react"; +import * as LightweightCharts from "@tradingview/lightweight-charts"; + +interface TradingViewChartProps { + ticker: string; + data?: Array<{ time: string; open: number; high: number; low: number; close: number }>; +} + +export default function TradingViewChart({ ticker, data }: TradingViewChartProps) { + const containerRef = useRef(null); + + useEffect(() => { + if (!containerRef.current) return; + + const chart = LightweightCharts.createChart(containerRef.current, { + width: containerRef.current.clientWidth, + height: 400, + }); + + const candlestickSeries = chart.addCandlestickSeries(); + + if (data && data.length > 0) { + candlestickSeries.setData(data); + } + + return () => chart.remove(); + }, [data]); + + return ( +
+

{ticker} Price Chart

+
+
+ ); +} +``` + +- [ ] **Step 2: Run typecheck** +Run: `npm run typecheck` +Expected: PASS + +- [ ] **Step 3: Commit** +```bash +git add app/components/TradingViewChart.tsx +git commit -m "feat: add tradingview chart component" +``` + +--- + +### Task 4: Create Stock Detail Route + +**Files:** +- Create: `app/routes/analyze.ticker.tsx` +- Modify: `app/routes.ts` + +- [ ] **Step 1: Create the route file** +```typescript +import { db } from "../lib/db.server"; +import TradingViewChart from "../components/TradingViewChart"; +import type { TradingDecision } from "../types/agents"; + +export const meta = () => [{ title: "Stock Detail - AITrader" }]; + +interface LoaderData { + ticker: string; + position: number | null; + orders: any[]; + analysis: TradingDecision | null; +} + +export async function loader({ params }: { params: { ticker: string } }) { + const ticker = params.ticker?.toUpperCase() || ""; + + // Fetch position + const posRes = await fetch(`${process.env.BASE_URL}/api/alpaca/positions`); + const positions = posRes.ok ? await posRes.json() : []; + const position = positions.find((p: any) => p.ticker === ticker)?.qty ?? null; + + // Fetch orders + const ordRes = await fetch(`${process.env.BASE_URL}/api/alpaca/orders`); + const ordersData = ordRes.ok ? await ordRes.json() : { orders: [] }; + const orders = ordersData.orders?.filter((o: any) => o.symbol === ticker) || []; + + return Response.json({ ticker, position, orders }); +} + +export default function StockDetail() { + const { ticker, position, orders } = useLoaderData() as LoaderData; + + return ( +
+ +
+

{ticker} Detail

+ + + +
+

Position

+

{position ? `Qty: ${position}` : "No position"}

+
+ +
+

Recent Orders

+ {orders.length === 0 ?

No orders

:
{JSON.stringify(orders, null, 2)}
} +
+
+
+ ); +} +``` + +- [ ] **Step 2: Register route in routes.ts** +```typescript +route("analyze/:ticker", "routes/analyze.ticker.tsx"), +``` + +- [ ] **Step 3: Run typecheck and test** +Run: `npm run typecheck && npm run test:e2e` +Expected: All pass + +- [ ] **Step 4: Commit** +```bash +git add app/routes/analyze.ticker.tsx app/routes.ts +git commit -m "feat: add stock detail route" +``` + +--- + +### Task 5: Add Navigation from Analyze Page + +**Files:** +- Modify: `app/routes/analyze.tsx` + +- [ ] **Step 1: Make ticker clickable** +```typescript +// Change from: +{stock.ticker} + +// To: + + + {stock.ticker} + + +``` + +- [ ] **Step 2: Add Link import** +```typescript +import { Link } from "react-router"; +``` + +- [ ] **Step 3: Run tests** +Run: `npm run test:e2e` +Expected: All pass + +- [ ] **Step 4: Commit** +```bash +git add app/routes/analyze.tsx +git commit -m "feat: add navigation to stock detail page" +``` + +--- + +### Task 6: Final Verification + +**Files:** +- Check: `package.json`, `tsconfig.json` + +- [ ] **Step 1: Run complete test suite** +```bash +npm run typecheck +npm run test:e2e -- --reporter=line +``` +Expected: All 8+ tests pass + +- [ ] **Step 2: Commit** +```bash +git commit -am "chore: verify stock detail implementation" +``` \ No newline at end of file diff --git a/docs/superpowers/specs/2026-05-14-stock-detail-page-design.md b/docs/superpowers/specs/2026-05-14-stock-detail-page-design.md new file mode 100644 index 0000000..abd9b1e --- /dev/null +++ b/docs/superpowers/specs/2026-05-14-stock-detail-page-design.md @@ -0,0 +1,74 @@ +# Stock Detail Page Implementation Design + +**Goal:** Create a stock detail page at `/analyze/:ticker` showing TradingView chart, position, orders, and trading graph results. + +**Architecture:** Dynamic route approach - separate route from analyze page with ticker parameter. + +## Files to Create/Modify + +1. `app/routes/analyze.ticker.tsx` - Stock detail page component +2. `app/components/TradingViewChart.tsx` - TradingView lightweight charts wrapper +3. `app/routes/api/alpaca/orders.ts` - Orders API endpoint +4. `app/routes.ts` - Add new route + +## Page Structure + +``` +/analyze/:ticker +├── Navbar +├── Stock Header (ticker, current price) +├── TradingView Chart (full width) +├── Position Card (quantity, avg cost, current value) +├── Orders Table (recent orders with status) +├── Trading Graph Results (expandable sections) +│ ├── Analyst Reports (fundamentals, technical, sentiment) +│ ├── Debate Summary +│ └── Final Decision +``` + +## Data Sources + +- **Chart**: TradingView widget with Alpaca data +- **Position**: `/api/alpaca/positions` filtered by ticker +- **Orders**: New `/api/alpaca/orders` endpoint +- **Analysis**: `/api/analyze` + TradingGraph results + +## API Changes + +### GET /api/alpaca/orders +Returns list of orders from Alpaca, optionally filtered by ticker. + +```typescript +// Response format +{ + orders: Array<{ + id: string; + ticker: string; + qty: number; + side: "buy" | "sell"; + status: "new" | "filled" | "canceled"; + filled_at: string | null; + filled_avg_price: string; + }> +} +``` + +## Component Details + +### TradingViewChart.tsx +- Uses TradingView lightweight charts library +- Props: `ticker`, `data` (price data array) +- Fetches historical bars from Alpaca API +- Renders candlestick chart + +### analyze.ticker.tsx +- Loader function fetches position, orders, and runs analysis +- Uses `useLoaderData` for server-fetched data +- Client-side rerun of analysis via form action + +## User Flow + +1. User clicks ticker in analyze page table +2. Navigates to `/analyze/:ticker` +3. Page shows chart at top, position/orders/analysis below +4. "Rerun Analysis" button triggers TradingGraph \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index db565d1..1c0151c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@react-router/node": "7.15.0", "@react-router/serve": "7.15.0", "isbot": "^5.1.36", + "lightweight-charts": "^5.2.0", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router": "7.15.0", @@ -4585,6 +4586,12 @@ "integrity": "sha512-AgFD4VU+lVLP6vjnlNfF7OeInLTyeyckCNPEsuxz1vi786UuK/nk6ynPuhn/h+Ju9++TQyr5EpLRI14fc1QtTQ==", "license": "MIT" }, + "node_modules/fancy-canvas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz", + "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5624,6 +5631,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightweight-charts": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-5.2.0.tgz", + "integrity": "sha512-ey3Vas8UhV06ni+LT9TA1nEe4y8So4Mi6CL/oarNHFMyTktz/xy8e8+oh04Q//eO3t6etvFXgayz2fClyFQb5w==", + "license": "Apache-2.0", + "dependencies": { + "fancy-canvas": "2.1.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", diff --git a/package.json b/package.json index 7d70776..175d527 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@react-router/node": "7.15.0", "@react-router/serve": "7.15.0", "isbot": "^5.1.36", + "lightweight-charts": "^5.2.0", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router": "7.15.0", diff --git a/playwright-report/index.html b/playwright-report/index.html index 5186d5b..f38e7ac 100644 --- a/playwright-report/index.html +++ b/playwright-report/index.html @@ -87,4 +87,4 @@ Error generating stack: `+l.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/prisma/dev.db b/prisma/dev.db index 292d18674931e06d0866db13541f310a3e81ec01..bd77a3810e4972fec8e29c22a894a875badd18ea 100644 GIT binary patch delta 583 zcmZoTz}Rqrae_3X?nD`9M%|4G3-xvRSQwaiFEj97=0C*W$~zSVc_h#w_bu)@Kp4NV z(U5y`y!9^@J{Crs$+0#&feeQF$x^njSQr=>_f6)otL6YQYE2nfStkejOHN*57Y7tz z_y-o~1qvwJ$JGM`>aDri>~(>vl5-18EtB$c%nS?+46~9m(o&2o%?v8?idA+Et8aNXvHhGrmr zrj8I&kBeWBfr;-k1K(%u1%wehd3n6H9#BMtS(gz#%(+FG^%*h3k_!N%w~}T6 delta 583 zcmZoTz}Rqrae_3XXe;&0@4=4awN!q?2_%E!WcnRhC0 z2(K8=Ri5cQkvtOIx47qU$8T&jn`o(Uq5oLgXOURao%XkcJqm|kH}l2cKV zpH!Nb9^e`j>=_(lXl`y{ZenI?U~FM*!Z=U0chfBhX~W27uMe~WVnBgO63_qxkO9W! zX(<)vsg@zGNV*LTEe%2X)~tq*dR!ns^FC$Zf6RZBe>s0Ae>T4_zdpYp-)Fw7eB1eE z@>TQ2^4aq#@-g#1BqzuC*+MvSwvv;#`9(uGJa&?k z_59@_oIrmI2&32EJc}8m6Q^&PWSH$aae4*pSiW2aejENv{EPWZ_+9yV`R?(}A} { + const res = await page.request.get("/api/alpaca/orders"); + expect(res.ok()).toBeTruthy(); + const data = await res.json(); + expect(data.orders).toBeDefined(); +}); \ No newline at end of file