- )
-}
diff --git a/ui/src/components/uta/EditUTADialog.tsx b/ui/src/components/uta/EditUTADialog.tsx
index 18d9c0f7a..9dbd22dd9 100644
--- a/ui/src/components/uta/EditUTADialog.tsx
+++ b/ui/src/components/uta/EditUTADialog.tsx
@@ -11,15 +11,21 @@ import { SchemaFormFields } from './SchemaFormFields'
/**
* UTA configuration dialog — edits credentials, guards, enabled state.
- * Mounted from both the trading page (legacy entry) and the UTA detail
- * page (new entry, accessed via the Edit button in the page header).
+ * Mounted from Settings → Trading (primary CRUD entry) and from the UTA
+ * detail page in Portfolio (sibling Edit button).
+ *
+ * When opened from Settings → Trading, the parent passes `onViewInPortfolio`
+ * to render a header link that switches the user over to the Portfolio
+ * drill-in for this account. When opened from inside Portfolio's detail
+ * page, that prop is omitted (the user is already in that context).
*/
-export function EditUTADialog({ uta, preset, health, onSave, onDelete, onClose }: {
+export function EditUTADialog({ uta, preset, health, onSave, onDelete, onViewInPortfolio, onClose }: {
uta: UTAConfig
preset?: BrokerPreset
health?: BrokerHealthInfo
onSave: (a: UTAConfig) => Promise
onDelete: () => Promise
+ onViewInPortfolio?: () => void
onClose: () => void
}) {
const [draft, setDraft] = useState(uta)
@@ -74,11 +80,25 @@ export function EditUTADialog({ uta, preset, health, onSave, onDelete, onClose }
{uta.id}
-
+
+ {onViewInPortfolio && (
+
+ )}
+
+
{/* Body */}
diff --git a/ui/src/pages/PortfolioPage.tsx b/ui/src/pages/PortfolioPage.tsx
index e169b1a79..05a42910e 100644
--- a/ui/src/pages/PortfolioPage.tsx
+++ b/ui/src/pages/PortfolioPage.tsx
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'
import { api, type Position, type WalletCommitLog, type EquityCurvePoint, type UTASnapshotSummary } from '../api'
import { useAutoSave } from '../hooks/useAutoSave'
import { useAccountHealth } from '../hooks/useAccountHealth'
+import { useWorkspace } from '../tabs/store'
import { PageHeader } from '../components/PageHeader'
import { EmptyState } from '../components/StateViews'
import { EquityCurve } from '../components/EquityCurve'
@@ -277,7 +278,7 @@ export function PortfolioPage() {
{/* Empty states */}
{data.accounts.length === 0 && !loading && (
-
+
)}
{data.accounts.length > 0 && allPositions.length === 0 && !loading && (
@@ -334,6 +335,31 @@ async function fetchPortfolioData(): Promise {
}
}
+// ==================== Empty: no trading accounts ====================
+
+function NoAccountsEmpty() {
+ const openOrFocus = useWorkspace((s) => s.openOrFocus)
+ const setSidebar = useWorkspace((s) => s.setSidebar)
+ const goToTradingSettings = () => {
+ setSidebar('settings')
+ openOrFocus({ kind: 'settings', params: { category: 'trading' } })
+ }
+ return (
+
+
No trading accounts connected.
+
+ Portfolio shows live equity, positions and PnL across all your brokers. Add a connection to get started.
+
+
+
+ )
+}
+
// ==================== Hero Metrics ====================
function HeroMetrics({ equity, curve }: {
diff --git a/ui/src/pages/TradingPage.tsx b/ui/src/pages/TradingPage.tsx
index ab7da0ac4..e44e53e10 100644
--- a/ui/src/pages/TradingPage.tsx
+++ b/ui/src/pages/TradingPage.tsx
@@ -1,5 +1,4 @@
-import { useState, useEffect, useMemo, useCallback } from 'react'
-import { useNavigate } from 'react-router-dom'
+import { useState, useEffect, useMemo } from 'react'
import { Field, inputClass } from '../components/form'
import { SDKSelector } from '../components/SDKSelector'
import type { SDKOption } from '../components/SDKSelector'
@@ -10,13 +9,18 @@ import { PageHeader } from '../components/PageHeader'
import { Dialog } from '../components/uta/Dialog'
import { HealthBadge } from '../components/uta/HealthBadge'
import { SchemaFormFields } from '../components/uta/SchemaFormFields'
-import { Metric, signFromDelta } from '../components/Metric'
-import { Sparkline } from '../components/Sparkline'
-import { fmt, fmtPnl, fmtPctSigned } from '../lib/format'
+import { EditUTADialog } from '../components/uta/EditUTADialog'
+import { fmt } from '../lib/format'
import { api } from '../api'
-import type { UTAConfig, BrokerPreset, BrokerHealthInfo, TestConnectionResult, Position, AccountInfo, EquityCurvePoint } from '../api/types'
+import { useWorkspace } from '../tabs/store'
+import type { UTAConfig, BrokerPreset, BrokerHealthInfo, TestConnectionResult, Position, AccountInfo } from '../api/types'
// ==================== Live equity (across all UTAs) ====================
+//
+// TradingPage is the CRUD surface for broker connections. The single
+// per-card equity number is here as a liveness signal — "this connection
+// returned real account data" — not as a portfolio view. Aggregate
+// equity, sparklines, 24h deltas, and trade logs live in Portfolio.
interface EquitySummary {
totalEquity: string
@@ -26,105 +30,50 @@ interface EquitySummary {
accounts: Array<{ id: string; label: string; equity: string; cash: string }>
}
-interface PerUtaCurve { values: number[]; firstAtCutoff: number | null; latest: number | null }
-
-interface CurveSummary {
- /** Aggregate (across all UTAs) — feeds the hero banner. */
- total: { values: number[]; firstAtCutoff: number | null; latest: number | null }
- /** Per-UTA curves — feed the per-card sparkline + 24h delta. */
- perUta: Record
-}
-
-const CUTOFF_24H_MS = 24 * 60 * 60 * 1000
-
-/** Build a curve summary from equity-curve points: latest value + the
- * oldest value still within the trailing 24h window (the "baseline"
- * for today PnL). */
-function summarizeCurve(points: EquityCurvePoint[]): CurveSummary {
- const sorted = [...points].sort((a, b) => a.timestamp.localeCompare(b.timestamp))
- const cutoff = Date.now() - CUTOFF_24H_MS
-
- const totalValues: number[] = []
- let totalFirstAtCutoff: number | null = null
- let totalLatest: number | null = null
- const perUtaValues = new Map()
- const perUtaFirstAtCutoff = new Map()
- const perUtaLatest = new Map()
-
- for (const p of sorted) {
- const t = new Date(p.timestamp).getTime()
- const totalN = Number(p.equity)
- if (Number.isFinite(totalN)) {
- totalValues.push(totalN)
- totalLatest = totalN
- if (t >= cutoff && totalFirstAtCutoff == null) totalFirstAtCutoff = totalN
- }
- for (const [id, raw] of Object.entries(p.accounts ?? {})) {
- const n = Number(raw)
- if (!Number.isFinite(n)) continue
- let arr = perUtaValues.get(id)
- if (!arr) { arr = []; perUtaValues.set(id, arr) }
- arr.push(n)
- perUtaLatest.set(id, n)
- if (t >= cutoff && !perUtaFirstAtCutoff.has(id)) perUtaFirstAtCutoff.set(id, n)
- }
- }
-
- const perUta: Record = {}
- for (const [id, values] of perUtaValues) {
- perUta[id] = {
- values,
- firstAtCutoff: perUtaFirstAtCutoff.get(id) ?? null,
- latest: perUtaLatest.get(id) ?? null,
- }
- }
-
- return {
- total: { values: totalValues, firstAtCutoff: totalFirstAtCutoff, latest: totalLatest },
- perUta,
- }
-}
-
// ==================== Page ====================
export function TradingPage() {
const tc = useTradingConfig()
const healthMap = useAccountHealth()
- const navigate = useNavigate()
+ const openOrFocus = useWorkspace((s) => s.openOrFocus)
+ const setSidebar = useWorkspace((s) => s.setSidebar)
const [showAdd, setShowAdd] = useState(false)
+ const [editingId, setEditingId] = useState(null)
const [presets, setPresets] = useState([])
const [equity, setEquity] = useState(null)
- const [curve, setCurve] = useState(null)
const [lastUpdated, setLastUpdated] = useState(null)
useEffect(() => {
api.trading.getBrokerPresets().then(r => setPresets(r.presets)).catch(() => {})
}, [])
- // Live aggregates: pull `equity()` for headline numbers and `equityCurve()`
- // for trend + 24h delta. One fetch each per cycle, shared across the
- // hero banner + every UTA card. Polling cadence (30s) is informational —
- // user can drill into a UTA for the 15s refresh of broker state.
- const refreshAggregates = useCallback(async () => {
- try {
- const [eq, cv] = await Promise.all([
- api.trading.equity().catch(() => null),
- api.trading.equityCurve({ limit: 1500 }).catch(() => ({ points: [] as EquityCurvePoint[] })),
- ])
- if (eq) setEquity(eq)
- setCurve(summarizeCurve(cv.points))
- setLastUpdated(new Date())
- } catch {
- // Don't surface — aggregates are nice-to-have, the page still renders
- // from useTradingConfig if the equity endpoint is down.
+ // Per-card liveness signal — `equity()` lets each card show "this
+ // connection actually returned an account balance" rather than just
+ // "ping went through". 60s cadence is enough; trend/sparkline/aggregate
+ // moved to Portfolio.
+ useEffect(() => {
+ let cancelled = false
+ const refresh = async () => {
+ const eq = await api.trading.equity().catch(() => null)
+ if (cancelled) return
+ if (eq) {
+ setEquity(eq)
+ setLastUpdated(new Date())
+ }
}
+ refresh()
+ const id = setInterval(refresh, 60_000)
+ return () => { cancelled = true; clearInterval(id) }
}, [])
- useEffect(() => {
- refreshAggregates()
- const id = setInterval(refreshAggregates, 30_000)
- return () => clearInterval(id)
- }, [refreshAggregates])
+ const editingUTA = editingId ? tc.utas.find(u => u.id === editingId) : null
+ const editingPreset = editingUTA ? presets.find(p => p.id === editingUTA.presetId) : undefined
+
+ const openInPortfolio = (id: string) => {
+ setEditingId(null)
+ setSidebar('portfolio')
+ openOrFocus({ kind: 'uta-detail', params: { id } })
+ }
if (tc.loading) return
if (tc.error) {
@@ -149,32 +98,27 @@ export function TradingPage() {
{tc.utas.length === 0 ? (
setShowAdd(true)} />
) : (
- <>
- {equity && }
-
-