fix(audit-A): repair the 5 critical broken integrations
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 <noreply@anthropic.com>
This commit is contained in:
parent
4a09d4ba26
commit
ddbffb6cd1
@ -15,11 +15,11 @@ Status: ⬜ open · 🟦 in PR · ✅ fixed (commit hash on the right).
|
|||||||
|
|
||||||
| # | Issue | Severity | Status | Fix commit |
|
| # | Issue | Severity | Status | Fix commit |
|
||||||
| --- | --------------------------------------------------------------------------------------------------------------------------------------- | :------: | :----: | ---------- |
|
| --- | --------------------------------------------------------------------------------------------------------------------------------------- | :------: | :----: | ---------- |
|
||||||
| A1 | `CodeStrategyEditor` POSTs to `/api/backtest`. Real endpoint is `/api/backtest/run`. Result: every "Run Backtest" returns 404. | 🔴 | ⬜ | |
|
| 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}`. | 🔴 | ⬜ | |
|
| 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. | 🔴 | ⬜ | |
|
| 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. | 🔴 | ⬜ | |
|
| 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. | 🟠 | ⬜ | |
|
| 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 `/`.) | 🟠 | ⬜ | |
|
| 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)
|
## B. Functional gaps (feature exists in plan but not implemented)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ArrowRight } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
import { useAppContext } from '../../context/AppContext';
|
import { useAppContext } from '../../context/AppContext';
|
||||||
|
import { fetchNews, type NewsArticle as MarketNewsArticle } from '../../lib/marketApi';
|
||||||
|
|
||||||
// ─── Portfolio Summary ────────────────────────────────────────────────────────
|
// ─── Portfolio Summary ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -91,14 +92,7 @@ function PortfolioSummary() {
|
|||||||
|
|
||||||
// ─── News Feed ────────────────────────────────────────────────────────────────
|
// ─── News Feed ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface NewsArticle {
|
type NewsArticle = MarketNewsArticle;
|
||||||
id?: string;
|
|
||||||
url: string;
|
|
||||||
headline: string;
|
|
||||||
source: string;
|
|
||||||
created_at: string;
|
|
||||||
images?: Array<{ url: string; size?: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function timeAgo(dateStr: string): string {
|
function timeAgo(dateStr: string): string {
|
||||||
const diff = Date.now() - new Date(dateStr).getTime();
|
const diff = Date.now() - new Date(dateStr).getTime();
|
||||||
@ -157,18 +151,22 @@ function NewsFeed() {
|
|||||||
const { activeSymbol } = useAppContext();
|
const { activeSymbol } = useAppContext();
|
||||||
const [news, setNews] = useState<NewsArticle[]>([]);
|
const [news, setNews] = useState<NewsArticle[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const apiBase = (import.meta.env.VITE_TRADING_API_URL as string) || 'http://localhost:4018';
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeSymbol) { setNews([]); return; }
|
if (!activeSymbol) { setNews([]); return; }
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetch(`${apiBase}/api/news?symbols=${activeSymbol}&limit=8`)
|
setError(null);
|
||||||
.then(r => r.ok ? r.json() : Promise.reject())
|
// Use the authenticated marketApi helper so the request carries the
|
||||||
.then(d => { if (!cancelled) { setNews(d.news ?? d ?? []); setLoading(false); } })
|
// platform Bearer token; the raw `fetch()` we used before was hitting
|
||||||
.catch(() => { if (!cancelled) setLoading(false); });
|
// 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; };
|
return () => { cancelled = true; };
|
||||||
}, [activeSymbol, apiBase]);
|
}, [activeSymbol]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -186,7 +184,11 @@ function NewsFeed() {
|
|||||||
<div style={{ fontSize: 12, color: '#9CA3AF', padding: '12px 16px' }}>Loading news…</div>
|
<div style={{ fontSize: 12, color: '#9CA3AF', padding: '12px 16px' }}>Loading news…</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && news.length === 0 && (
|
{!loading && error && (
|
||||||
|
<div style={{ fontSize: 12, color: '#DC2626', padding: '12px 16px' }}>{error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && news.length === 0 && (
|
||||||
<div style={{ fontSize: 12, color: '#9CA3AF', padding: '16px', textAlign: 'center' }}>
|
<div style={{ fontSize: 12, color: '#9CA3AF', padding: '16px', textAlign: 'center' }}>
|
||||||
{activeSymbol ? `No news found for ${activeSymbol}` : 'Search a ticker to see news'}
|
{activeSymbol ? `No news found for ${activeSymbol}` : 'Search a ticker to see news'}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -66,7 +66,10 @@ export function CodeStrategyEditor({ symbol }: Props) {
|
|||||||
setResult(null);
|
setResult(null);
|
||||||
try {
|
try {
|
||||||
const token = await getPlatformAccessToken();
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -74,14 +77,20 @@ export function CodeStrategyEditor({ symbol }: Props) {
|
|||||||
'x-request-id': createRequestId('web-backtest'),
|
'x-request-id': createRequestId('web-backtest'),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
symbol,
|
symbols: [symbol],
|
||||||
strategyCode: code,
|
strategyConfig: {
|
||||||
mode: 'code',
|
type: 'code',
|
||||||
|
language: 'javascript',
|
||||||
|
code,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json().catch(() => ({})) as any;
|
const data = await res.json().catch(() => ({})) as any;
|
||||||
if (!res.ok) throw new Error(data?.error ?? `Backtest failed (${res.status})`);
|
if (!res.ok || data?.success === false) {
|
||||||
setResult(data);
|
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) {
|
} catch (err: any) {
|
||||||
setError(err?.message ?? 'Backtest failed');
|
setError(err?.message ?? 'Backtest failed');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -5,9 +5,7 @@ import { BacktestTab } from '../tabs/BacktestTab';
|
|||||||
import { MyStrategiesTab } from '../tabs/MyStrategiesTab';
|
import { MyStrategiesTab } from '../tabs/MyStrategiesTab';
|
||||||
import { VisualRuleBuilder, type VisualRule } from '../components/strategy/VisualRuleBuilder';
|
import { VisualRuleBuilder, type VisualRule } from '../components/strategy/VisualRuleBuilder';
|
||||||
import { CodeStrategyEditor } from '../components/strategy/CodeStrategyEditor';
|
import { CodeStrategyEditor } from '../components/strategy/CodeStrategyEditor';
|
||||||
import { getPlatformAccessToken } from '../lib/authSession';
|
import { createTradeProfile } from '../lib/profileApi';
|
||||||
import { tradingRuntime } from '../lib/runtime';
|
|
||||||
import { createRequestId } from '../../../shared/request-id.js';
|
|
||||||
|
|
||||||
type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting';
|
type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting';
|
||||||
|
|
||||||
@ -49,33 +47,37 @@ export function ResearchView() {
|
|||||||
...(showBacktestTab ? ['Backtesting' as ResearchTab] : []),
|
...(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 handleSaveVisualStrategy = async (name: string, rules: VisualRule[]) => {
|
||||||
const token = await getPlatformAccessToken();
|
|
||||||
const fallbackSymbol = Object.keys(botState.symbols)[0] ?? 'SPY';
|
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,
|
name,
|
||||||
symbol: activeSymbol || fallbackSymbol,
|
symbols: symbol,
|
||||||
strategyType: 'visual',
|
allocated_capital: totalCapital,
|
||||||
visualRules: rules,
|
risk_per_trade_percent: riskPct,
|
||||||
// Convert to a human-readable description for the existing strategy engine
|
is_active: false, // user activates explicitly from MyStrategiesTab
|
||||||
description: rules.map((r, i) =>
|
strategy_config: {
|
||||||
`Rule ${i + 1}: IF ${r.indicator} ${r.condition} ${r.value} → ${r.action} ${r.quantity} ${r.quantityType}`
|
type: 'visual',
|
||||||
).join('; '),
|
version: 1,
|
||||||
};
|
rules: rules.map(r => ({
|
||||||
const res = await fetch(`${tradingRuntime.tradingApiUrl}/api/profiles`, {
|
indicator: r.indicator,
|
||||||
method: 'POST',
|
condition: r.condition,
|
||||||
headers: {
|
value: r.value,
|
||||||
'Content-Type': 'application/json',
|
action: r.action,
|
||||||
Authorization: `Bearer ${token}`,
|
quantity: r.quantity,
|
||||||
'x-request-id': createRequestId('web-strategy'),
|
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 (
|
return (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user