Split strategy dashboard sections
All checks were successful
deploy / deploy (push) Successful in 58s
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:
parent
04c59ee93d
commit
686b922342
4 changed files with 143 additions and 74 deletions
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1203,77 +1203,100 @@ 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>
|
||||
<div className="eyebrow">Trading evidence</div>
|
||||
<h2>Quotes, responses, and proven trades</h2>
|
||||
<div className="panel-subtitle">
|
||||
One place for quote truth: every row starts at the incoming quote, then shows whether we responded, why not, and whether any asset movement was proven.
|
||||
</div>
|
||||
<section className="panel">
|
||||
<div className="panel-head">
|
||||
<div>
|
||||
<div className="eyebrow">Trading evidence</div>
|
||||
<h2>Quotes, responses, and proven trades</h2>
|
||||
<div className="panel-subtitle">
|
||||
One place for quote truth: every row starts at the incoming quote, then shows whether we responded, why not, and whether any asset movement was proven.
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric-grid">
|
||||
<MetricCard label="Successful trades" meta="Requires linked terminal outcome and settlement" value={String(funnel.successful_trade_count || 0)} />
|
||||
<MetricCard
|
||||
label="Gross edge est."
|
||||
meta={`${funnel.successful_trade_gross_edge_estimate_count || 0} proven trades, before fees`}
|
||||
signedValue={funnel.successful_trade_gross_edge_estimate_eure}
|
||||
value={formatEur(funnel.successful_trade_gross_edge_estimate_eure)}
|
||||
/>
|
||||
<MetricCard label="Not filled" meta="Submitted but no settled inventory delta" value={String(counts.not_filled || 0)} />
|
||||
<MetricCard label="Awaiting outcome" meta="Submitted, no durable terminal result yet" value={String(funnel.unresolved_submission_count || 0)} />
|
||||
<MetricCard label="Rejected / blocked" meta="Strategy rejection or executor block" value={String((counts.rejected || 0) + (counts.blocked || 0))} />
|
||||
<MetricCard label="Strategy armed" meta={`Paused ${formatBoolean(strategy.strategy_state.paused)}`} value={formatBoolean(strategy.strategy_state.armed)} />
|
||||
<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} />
|
||||
|
||||
<section className="panel">
|
||||
<div className="panel-head">
|
||||
<div>
|
||||
<div className="eyebrow">Successful trades only</div>
|
||||
<h3>Trades with proven asset movement</h3>
|
||||
<div className="panel-subtitle">
|
||||
This table excludes submitted-only quote responses. Realized PnL remains unavailable until fees and venue-native terminal fills are stored.
|
||||
</div>
|
||||
</div>
|
||||
<div className="pills">
|
||||
<Pill label={`${counts.completed || 0} completed`} stateLabel={(counts.completed || 0) > 0 ? 'healthy' : 'unknown'} />
|
||||
<Pill label={`${formatEur(funnel.successful_trade_gross_edge_estimate_eure)} gross edge est.`} stateLabel={funnel.successful_trade_gross_edge_estimate_eure ? 'healthy' : 'unknown'} />
|
||||
<Pill label={`${counts.not_filled || 0} not filled`} stateLabel={(counts.not_filled || 0) > 0 ? 'warning' : 'unknown'} />
|
||||
</div>
|
||||
</div>
|
||||
<SuccessfulTradesTable items={funnel.successful_trades} />
|
||||
</section>
|
||||
|
||||
<section className="panel full-width-evidence-panel">
|
||||
<div className="panel-head">
|
||||
<div>
|
||||
<div className="eyebrow">Quote lifecycle</div>
|
||||
<h3>Incoming quotes and what happened next</h3>
|
||||
<div className="panel-subtitle">
|
||||
Full-width quote table: incoming quote, response decision, result, decisive reason, and expandable lifecycle stages.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<QuoteLifecycleTable items={strategy.strategy_state.recent_lifecycle_rows} />
|
||||
</section>
|
||||
</>
|
||||
</div>
|
||||
<div className="metric-grid">
|
||||
<MetricCard label="Successful trades" meta="Requires linked terminal outcome and settlement" value={String(funnel.successful_trade_count || 0)} />
|
||||
<MetricCard
|
||||
label="Gross edge est."
|
||||
meta={`${funnel.successful_trade_gross_edge_estimate_count || 0} proven trades, before fees`}
|
||||
signedValue={funnel.successful_trade_gross_edge_estimate_eure}
|
||||
value={formatEur(funnel.successful_trade_gross_edge_estimate_eure)}
|
||||
/>
|
||||
<MetricCard label="Not filled" meta="Submitted but no settled inventory delta" value={String(counts.not_filled || 0)} />
|
||||
<MetricCard label="Awaiting outcome" meta="Submitted, no durable terminal result yet" value={String(funnel.unresolved_submission_count || 0)} />
|
||||
<MetricCard label="Rejected / blocked" meta="Strategy rejection or executor block" value={String((counts.rejected || 0) + (counts.blocked || 0))} />
|
||||
<MetricCard label="Strategy armed" meta={`Paused ${formatBoolean(strategy.strategy_state.paused)}`} value={formatBoolean(strategy.strategy_state.armed)} />
|
||||
<MetricCard label="Executor armed" meta={`Paused ${formatBoolean(strategy.executor_state.paused)}`} value={formatBoolean(strategy.executor_state.armed)} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function SuccessfulTradesSection({ funnel, counts }) {
|
||||
return (
|
||||
<section className="panel">
|
||||
<div className="panel-head">
|
||||
<div>
|
||||
<div className="eyebrow">Successful trades only</div>
|
||||
<h3>Trades with proven asset movement</h3>
|
||||
<div className="panel-subtitle">
|
||||
This table excludes submitted-only quote responses. Realized PnL remains unavailable until fees and venue-native terminal fills are stored.
|
||||
</div>
|
||||
</div>
|
||||
<div className="pills">
|
||||
<Pill label={`${counts.completed || 0} completed`} stateLabel={(counts.completed || 0) > 0 ? 'healthy' : 'unknown'} />
|
||||
<Pill label={`${formatEur(funnel.successful_trade_gross_edge_estimate_eure)} gross edge est.`} stateLabel={funnel.successful_trade_gross_edge_estimate_eure ? 'healthy' : 'unknown'} />
|
||||
<Pill label={`${counts.not_filled || 0} not filled`} stateLabel={(counts.not_filled || 0) > 0 ? 'warning' : 'unknown'} />
|
||||
</div>
|
||||
</div>
|
||||
<SuccessfulTradesTable items={funnel.successful_trades} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function QuoteLifecycleSection({ items }) {
|
||||
return (
|
||||
<section className="panel full-width-evidence-panel">
|
||||
<div className="panel-head">
|
||||
<div>
|
||||
<div className="eyebrow">Quote lifecycle</div>
|
||||
<h3>Incoming quotes and what happened next</h3>
|
||||
<div className="panel-subtitle">
|
||||
Full-width quote table: incoming quote, response decision, result, decisive reason, and expandable lifecycle stages.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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} />;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'/);
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue