From ddbffb6cd1ccd45c785924a15294da4ca60813b9 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Mon, 4 May 2026 06:23:52 -0700 Subject: [PATCH] fix(audit-A): repair the 5 critical broken integrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A1+A2 — CodeStrategyEditor backtest call Was: POST /api/backtest with { symbol, strategyCode, mode: 'code' } Now: POST /api/backtest/run with { symbols: [s], strategyConfig: { type: 'code', language: 'javascript', code } } The backend route is /api/backtest/run (not /api/backtest), and /api/backtest/run validates `symbols[]` and `strategyConfig`, not the ad-hoc fields we were sending. Also unwraps the { success, results } envelope the engine returns and surfaces success:false errors. A3 — VisualRuleBuilder save shape Was: hand-rolled fetch to /api/profiles with { name, symbol, strategyType, visualRules, description } — backend's saveTradeProfileForUser ignored all of that and either 400'd or persisted a half-empty row. Now: uses the canonical createTradeProfile() helper from lib/profileApi with the documented TradeProfilePayload shape. Visual rules go inside strategy_config.{type:'visual', version:1, rules:[...]} so the engine can fan out to a visual interpreter without conflicting with the existing rule-based engine. Allocated capital + risk pct pulled from botState.settings so the profile inherits the user's current sizing. is_active defaults false so the user activates explicitly. A4+A5 — RightPanel.NewsFeed auth + runtime Was: raw fetch() to import.meta.env.VITE_TRADING_API_URL with no Authorization header → 401 on every render in any environment that requires auth, and prod-broken where the runtime resolver is the only source of truth for the API base URL. Now: uses fetchNews() from lib/marketApi which already carries the platform Bearer token and routes through tradingRuntime.tradingApiUrl. Adds an error state in the UI for visibility instead of silently leaving the panel blank. Verified: web/ tsc --noEmit passes. No behavioural change to non-affected code paths (RightPanel portfolio summary, ResearchView other tabs, etc.). Refs: docs/AUDIT_REDESIGN.md items A1–A5. Co-Authored-By: Claude Sonnet 4.6 --- docs/AUDIT_REDESIGN.md | 10 ++-- web/src/components/layout/RightPanel.tsx | 32 ++++++------ .../strategy/CodeStrategyEditor.tsx | 21 +++++--- web/src/views/ResearchView.tsx | 52 ++++++++++--------- 4 files changed, 64 insertions(+), 51 deletions(-) diff --git a/docs/AUDIT_REDESIGN.md b/docs/AUDIT_REDESIGN.md index b426541..5c2c405 100644 --- a/docs/AUDIT_REDESIGN.md +++ b/docs/AUDIT_REDESIGN.md @@ -15,11 +15,11 @@ Status: ⬜ open · 🟦 in PR · ✅ fixed (commit hash on the right). | # | Issue | Severity | Status | Fix commit | | --- | --------------------------------------------------------------------------------------------------------------------------------------- | :------: | :----: | ---------- | -| A1 | `CodeStrategyEditor` POSTs to `/api/backtest`. Real endpoint is `/api/backtest/run`. Result: every "Run Backtest" returns 404. | 🔴 | ⬜ | | -| A2 | `CodeStrategyEditor` payload sends `{symbol, strategyCode, mode}`. Backend `/api/backtest/run` requires `{symbols[], strategyConfig}`. | 🔴 | ⬜ | | -| A3 | `VisualRuleBuilder` save → `/api/profiles` body uses `{strategyType, visualRules, description}`. `saveTradeProfileForUser` expects `strategy_config` shape. Result: 400 or silently-discarded fields. | 🔴 | ⬜ | | -| A4 | `RightPanel.NewsFeed` calls `fetch()` with no `Authorization` header. `/api/news` is `requireAuth`. Result: 401 every render. | 🔴 | ⬜ | | -| A5 | `RightPanel.NewsFeed` reads `import.meta.env.VITE_TRADING_API_URL` directly instead of `tradingRuntime.tradingApiUrl`. Breaks in prod where the runtime resolver is the source of truth. | 🟠 | ⬜ | | +| A1 | `CodeStrategyEditor` POSTs to `/api/backtest`. Real endpoint is `/api/backtest/run`. Result: every "Run Backtest" returns 404. | 🔴 | ✅ | bucket A | +| A2 | `CodeStrategyEditor` payload sends `{symbol, strategyCode, mode}`. Backend `/api/backtest/run` requires `{symbols[], strategyConfig}`. | 🔴 | ✅ | bucket A | +| A3 | `VisualRuleBuilder` save → `/api/profiles` body uses `{strategyType, visualRules, description}`. `saveTradeProfileForUser` expects `strategy_config` shape. Result: 400 or silently-discarded fields. | 🔴 | ✅ | bucket A | +| A4 | `RightPanel.NewsFeed` calls `fetch()` with no `Authorization` header. `/api/news` is `requireAuth`. Result: 401 every render. | 🔴 | ✅ | bucket A | +| A5 | `RightPanel.NewsFeed` reads `import.meta.env.VITE_TRADING_API_URL` directly instead of `tradingRuntime.tradingApiUrl`. Breaks in prod where the runtime resolver is the source of truth. | 🟠 | ✅ | bucket A | | A6 | Backend `/api/chart/bars` previously crashed on crypto symbols (`BTC/USD`) because `/v2/stocks` rejects them. (Already partially fixed in 938ed86 — verify the encode path doesn't double-encode `/`.) | 🟠 | ⬜ | | ## B. Functional gaps (feature exists in plan but not implemented) diff --git a/web/src/components/layout/RightPanel.tsx b/web/src/components/layout/RightPanel.tsx index 9a3c53a..cb838d8 100644 --- a/web/src/components/layout/RightPanel.tsx +++ b/web/src/components/layout/RightPanel.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { ArrowRight } from 'lucide-react'; import { useAppContext } from '../../context/AppContext'; +import { fetchNews, type NewsArticle as MarketNewsArticle } from '../../lib/marketApi'; // ─── Portfolio Summary ──────────────────────────────────────────────────────── @@ -91,14 +92,7 @@ function PortfolioSummary() { // ─── News Feed ──────────────────────────────────────────────────────────────── -interface NewsArticle { - id?: string; - url: string; - headline: string; - source: string; - created_at: string; - images?: Array<{ url: string; size?: string }>; -} +type NewsArticle = MarketNewsArticle; function timeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); @@ -157,18 +151,22 @@ function NewsFeed() { const { activeSymbol } = useAppContext(); const [news, setNews] = useState([]); const [loading, setLoading] = useState(false); - const apiBase = (import.meta.env.VITE_TRADING_API_URL as string) || 'http://localhost:4018'; + const [error, setError] = useState(null); useEffect(() => { if (!activeSymbol) { setNews([]); return; } let cancelled = false; setLoading(true); - fetch(`${apiBase}/api/news?symbols=${activeSymbol}&limit=8`) - .then(r => r.ok ? r.json() : Promise.reject()) - .then(d => { if (!cancelled) { setNews(d.news ?? d ?? []); setLoading(false); } }) - .catch(() => { if (!cancelled) setLoading(false); }); + setError(null); + // Use the authenticated marketApi helper so the request carries the + // platform Bearer token; the raw `fetch()` we used before was hitting + // requireAuth and 401-ing on every render. + fetchNews(activeSymbol, 8) + .then(list => { if (!cancelled) setNews(list); }) + .catch(err => { if (!cancelled) setError(err?.message ?? 'Failed to load news'); }) + .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; - }, [activeSymbol, apiBase]); + }, [activeSymbol]); return (
@@ -186,7 +184,11 @@ function NewsFeed() {
Loading news…
)} - {!loading && news.length === 0 && ( + {!loading && error && ( +
{error}
+ )} + + {!loading && !error && news.length === 0 && (
{activeSymbol ? `No news found for ${activeSymbol}` : 'Search a ticker to see news'}
diff --git a/web/src/components/strategy/CodeStrategyEditor.tsx b/web/src/components/strategy/CodeStrategyEditor.tsx index a27c087..a2041ab 100644 --- a/web/src/components/strategy/CodeStrategyEditor.tsx +++ b/web/src/components/strategy/CodeStrategyEditor.tsx @@ -66,7 +66,10 @@ export function CodeStrategyEditor({ symbol }: Props) { setResult(null); try { const token = await getPlatformAccessToken(); - const res = await fetch(`${tradingRuntime.tradingApiUrl}/api/backtest`, { + // Backend `/api/backtest/run` expects `{ symbols: string[], strategyConfig: object }`. + // We wrap the user's JS in a strategyConfig of type `code` so the engine + // can route to a sandboxed evaluator (see backend custom-strategy handler). + const res = await fetch(`${tradingRuntime.tradingApiUrl}/api/backtest/run`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -74,14 +77,20 @@ export function CodeStrategyEditor({ symbol }: Props) { 'x-request-id': createRequestId('web-backtest'), }, body: JSON.stringify({ - symbol, - strategyCode: code, - mode: 'code', + symbols: [symbol], + strategyConfig: { + type: 'code', + language: 'javascript', + code, + }, }), }); const data = await res.json().catch(() => ({})) as any; - if (!res.ok) throw new Error(data?.error ?? `Backtest failed (${res.status})`); - setResult(data); + if (!res.ok || data?.success === false) { + throw new Error(data?.error ?? `Backtest failed (${res.status})`); + } + // Backend may wrap results in { success, results } or return them flat. + setResult(data?.results ?? data); } catch (err: any) { setError(err?.message ?? 'Backtest failed'); } finally { diff --git a/web/src/views/ResearchView.tsx b/web/src/views/ResearchView.tsx index 7d93eea..2995078 100644 --- a/web/src/views/ResearchView.tsx +++ b/web/src/views/ResearchView.tsx @@ -5,9 +5,7 @@ import { BacktestTab } from '../tabs/BacktestTab'; import { MyStrategiesTab } from '../tabs/MyStrategiesTab'; import { VisualRuleBuilder, type VisualRule } from '../components/strategy/VisualRuleBuilder'; import { CodeStrategyEditor } from '../components/strategy/CodeStrategyEditor'; -import { getPlatformAccessToken } from '../lib/authSession'; -import { tradingRuntime } from '../lib/runtime'; -import { createRequestId } from '../../../shared/request-id.js'; +import { createTradeProfile } from '../lib/profileApi'; type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting'; @@ -49,33 +47,37 @@ export function ResearchView() { ...(showBacktestTab ? ['Backtesting' as ResearchTab] : []), ]; - // Save a visual-builder strategy by converting rules to a profile config + // Save a visual-builder strategy via the canonical createTradeProfile helper. + // Backend `saveTradeProfileForUser` expects the TradeProfilePayload shape: + // { name, symbols, allocated_capital, risk_per_trade_percent, is_active, + // strategy_config: { ... } } + // Visual rules go inside strategy_config.rules so the strategy engine can + // route to the visual interpreter (alongside the existing rule-based engine). const handleSaveVisualStrategy = async (name: string, rules: VisualRule[]) => { - const token = await getPlatformAccessToken(); const fallbackSymbol = Object.keys(botState.symbols)[0] ?? 'SPY'; - const body = { + const symbol = activeSymbol || fallbackSymbol; + const totalCapital = botState.settings?.totalCapital ?? 1000; + const riskPct = botState.settings?.riskPerTrade ?? 1; + + await createTradeProfile({ name, - symbol: activeSymbol || fallbackSymbol, - strategyType: 'visual', - visualRules: rules, - // Convert to a human-readable description for the existing strategy engine - description: rules.map((r, i) => - `Rule ${i + 1}: IF ${r.indicator} ${r.condition} ${r.value} → ${r.action} ${r.quantity} ${r.quantityType}` - ).join('; '), - }; - const res = await fetch(`${tradingRuntime.tradingApiUrl}/api/profiles`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - 'x-request-id': createRequestId('web-strategy'), + symbols: symbol, + allocated_capital: totalCapital, + risk_per_trade_percent: riskPct, + is_active: false, // user activates explicitly from MyStrategiesTab + strategy_config: { + type: 'visual', + version: 1, + rules: rules.map(r => ({ + indicator: r.indicator, + condition: r.condition, + value: r.value, + action: r.action, + quantity: r.quantity, + quantityType: r.quantityType, + })), }, - body: JSON.stringify(body), }); - if (!res.ok) { - const data = await res.json().catch(() => ({})) as any; - throw new Error(data?.error ?? `Save failed (${res.status})`); - } }; return (