Split strategy dashboard sections
All checks were successful
deploy / deploy (push) Successful in 58s

Proof: operator dashboard Strategy panels now render as separate sidebar menu destinations for overview, pair config, competitiveness, asset registry, successful trades, and quote lifecycle; static UI coverage and the dashboard bundle verify the routing.

Assumptions: this is a UI-only operator cleanup inside the active maker timing and competitiveness turn; it does not change strategy decisions, pair config, execution, persistence, live funds, or response policy.

Still fake: venue-native terminal fill ids and fee-complete realized PnL remain unavailable.
This commit is contained in:
philipp 2026-05-19 17:42:10 +02:00
parent 04c59ee93d
commit 686b922342
4 changed files with 143 additions and 74 deletions

View file

@ -31,6 +31,7 @@ function LoadingPanel({ error, onRetry }) {
export default function App() {
const [state, dispatch] = useReducer(dashboardReducer, initialDashboardState);
const currentPage = state.page || state.dashboard?.default_page || 'funds';
const isStrategyPage = currentPage === 'strategy' || currentPage.startsWith('strategy-');
const isReadyForSocket = Boolean(state.session && state.dashboard);
const criticalBanner = null;
@ -178,8 +179,12 @@ export default function App() {
onControl={submitControl}
/>
) : null}
{currentPage === 'strategy' ? (
<StrategyPage onControl={submitControl} strategy={state.dashboard.strategy} />
{isStrategyPage ? (
<StrategyPage
onControl={submitControl}
page={currentPage}
strategy={state.dashboard.strategy}
/>
) : null}
{currentPage === 'system' ? (
<SystemPage onControl={submitControl} system={state.dashboard.system} />

View file

@ -6,8 +6,33 @@ const NAV_ITEMS = [
},
{
page: 'strategy',
title: 'Strategy',
description: 'Trading state, decision flow, and guarded omissions.',
title: 'Strategy overview',
description: 'Quote, response, and settlement counters.',
},
{
page: 'strategy-pairs',
title: 'Pair config',
description: 'Directed pairs, edges, limits, and response policy.',
},
{
page: 'strategy-competitiveness',
title: 'Competitiveness',
description: 'Timing, quote-age, relay result, and outcome summaries.',
},
{
page: 'strategy-assets',
title: 'Asset registry',
description: 'Supported-token import and deposit addresses.',
},
{
page: 'strategy-trades',
title: 'Successful trades',
description: 'Only quotes with proven asset movement.',
},
{
page: 'strategy-lifecycle',
title: 'Quote lifecycle',
description: 'Incoming quotes and what happened next.',
},
{
page: 'system',

View file

@ -1203,12 +1203,11 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
);
}
export default function StrategyPage({ strategy, onControl }) {
function StrategyOverviewSection({ strategy }) {
const funnel = strategy.strategy_state.trade_funnel || {};
const counts = funnel.counts || {};
return (
<>
<section className="panel">
<div className="panel-head">
<div>
@ -1234,16 +1233,11 @@ export default function StrategyPage({ strategy, onControl }) {
<MetricCard label="Executor armed" meta={`Paused ${formatBoolean(strategy.executor_state.paused)}`} value={formatBoolean(strategy.executor_state.armed)} />
</div>
</section>
);
}
<PairConfigSection assetCatalog={strategy.asset_catalog} pairConfig={strategy.pair_config} onControl={onControl} />
<MakerCompetitivenessSection
pairConfig={strategy.pair_config}
summary={strategy.strategy_state.maker_competitiveness}
/>
<AssetCatalogSection assetCatalog={strategy.asset_catalog} onControl={onControl} />
function SuccessfulTradesSection({ funnel, counts }) {
return (
<section className="panel">
<div className="panel-head">
<div>
@ -1261,7 +1255,11 @@ export default function StrategyPage({ strategy, onControl }) {
</div>
<SuccessfulTradesTable items={funnel.successful_trades} />
</section>
);
}
function QuoteLifecycleSection({ items }) {
return (
<section className="panel full-width-evidence-panel">
<div className="panel-head">
<div>
@ -1272,8 +1270,33 @@ export default function StrategyPage({ strategy, onControl }) {
</div>
</div>
</div>
<QuoteLifecycleTable items={strategy.strategy_state.recent_lifecycle_rows} />
<QuoteLifecycleTable items={items} />
</section>
</>
);
}
export default function StrategyPage({ strategy, onControl, page = 'strategy' }) {
const funnel = strategy.strategy_state.trade_funnel || {};
const counts = funnel.counts || {};
switch (page) {
case 'strategy-pairs':
return <PairConfigSection assetCatalog={strategy.asset_catalog} pairConfig={strategy.pair_config} onControl={onControl} />;
case 'strategy-competitiveness':
return (
<MakerCompetitivenessSection
pairConfig={strategy.pair_config}
summary={strategy.strategy_state.maker_competitiveness}
/>
);
case 'strategy-assets':
return <AssetCatalogSection assetCatalog={strategy.asset_catalog} onControl={onControl} />;
case 'strategy-trades':
return <SuccessfulTradesSection counts={counts} funnel={funnel} />;
case 'strategy-lifecycle':
return <QuoteLifecycleSection items={strategy.strategy_state.recent_lifecycle_rows} />;
case 'strategy':
default:
return <StrategyOverviewSection strategy={strategy} />;
}
}

View file

@ -9,6 +9,7 @@ const serviceCardSource = readFileSync(new URL('../src/operator-dashboard/static
const statusBarSource = readFileSync(new URL('../src/operator-dashboard/static/components/StatusBar.jsx', import.meta.url), 'utf8');
const systemSource = readFileSync(new URL('../src/operator-dashboard/static/pages/SystemPage.jsx', import.meta.url), 'utf8');
const appSource = readFileSync(new URL('../src/operator-dashboard/static/App.jsx', import.meta.url), 'utf8');
const navSource = readFileSync(new URL('../src/operator-dashboard/static/components/NavRail.jsx', import.meta.url), 'utf8');
test('strategy page owns consolidated quote lifecycle and successful trade tables', () => {
assert.match(strategySource, /Quote lifecycle/);
@ -143,10 +144,25 @@ test('strategy page distinguishes configured bps from gross quote edge percent',
assert.doesNotMatch(strategySource, /Edge \$\{item\.edge_bps\}%/);
});
test('pair controls are rendered before the long asset catalog table', () => {
assert.ok(
strategySource.indexOf('<PairConfigSection') < strategySource.indexOf('<AssetCatalogSection'),
);
test('strategy section panels are split into separate menu destinations', () => {
assert.match(navSource, /Strategy overview/);
assert.match(navSource, /Pair config/);
assert.match(navSource, /Competitiveness/);
assert.match(navSource, /Asset registry/);
assert.match(navSource, /Successful trades/);
assert.match(navSource, /Quote lifecycle/);
assert.match(navSource, /page: 'strategy-pairs'/);
assert.match(navSource, /page: 'strategy-competitiveness'/);
assert.match(navSource, /page: 'strategy-assets'/);
assert.match(navSource, /page: 'strategy-trades'/);
assert.match(navSource, /page: 'strategy-lifecycle'/);
assert.match(appSource, /currentPage\.startsWith\('strategy-'\)/);
assert.match(appSource, /page=\{currentPage\}/);
assert.match(strategySource, /case 'strategy-pairs'/);
assert.match(strategySource, /case 'strategy-competitiveness'/);
assert.match(strategySource, /case 'strategy-assets'/);
assert.match(strategySource, /case 'strategy-trades'/);
assert.match(strategySource, /case 'strategy-lifecycle'/);
});