feat: client-side simulation layer for terminal dashboard panels

Adds a local synthetic data engine that can drive all dashboard panels
(candles, order book, liq ladder, stats) without a backend connection,
plus de-synced beam animation phases in the orchestration topology.
This commit is contained in:
tinkle-community
2026-07-05 19:13:44 +09:00
parent ea74f91de2
commit 49613830ce
7 changed files with 789 additions and 45 deletions

View File

@@ -17,6 +17,7 @@ import { KlineChart } from './KlineChart'
import { ExecutionLog } from './ExecutionLog'
import { SignalMatrix } from './SignalMatrix'
import { RiskRadar } from './RiskRadar'
import { useDemoEngine } from '../../lib/demo/useDemoEngine'
// crypto majors trade on the Hyperliquid main dex (no hip3 cost/liq heatmap);
// everything else in the universe is an xyz-dex synthetic market that does.
@@ -88,50 +89,77 @@ export function TerminalDashboard({
traders,
selectedTraderId,
onTraderSelect,
status,
account,
positions,
decisions,
status: propStatus,
account: propAccount,
positions: propPositions,
decisions: propDecisions,
}: TerminalDashboardProps) {
const traderId = selectedTrader?.trader_id || selectedTraderId
useTick(1000)
const clock = new Date().toLocaleTimeString('en-GB', { hour12: false })
const { data: fullStats } = useSWR(
const { data: realFullStats } = useSWR(
traderId ? ['full-stats', traderId] : null,
() => api.getFullStats(traderId!, true),
{ refreshInterval: 30000, shouldRetryOnError: false }
)
const { data: history } = useSWR(
const { data: realHistory } = useSWR(
traderId ? ['pos-history', traderId] : null,
() => api.getPositionHistory(traderId!, 50, true),
{ refreshInterval: 60000, shouldRetryOnError: false }
)
const { data: config } = useSWR(
const { data: realConfig } = useSWR(
traderId ? ['trader-config', traderId] : null,
() => api.getTraderConfig(traderId!, true),
{ refreshInterval: 120000, shouldRetryOnError: false }
)
const latest = decisions && decisions.length > 0 ? decisions[0] : undefined
const candidateCoins = latest?.candidate_coins ?? []
const { data: flow } = useSWR(
const { data: realFlow } = useSWR(
traderId ? ['flow-markets', traderId] : null,
() => api.getFlowMarkets(selectedTrader?.ai_model, 'mainnet', '1h', 50, true),
// paid x402 endpoint — poll slowly (5m) to conserve claw402 funds; the
// topology beam animation is client-side and stays fast regardless
{ refreshInterval: 300000, shouldRetryOnError: false }
)
const flowItems = flow?.data?.inflow ?? []
const { data: signalRank } = useSWR(
const { data: realSignalRank } = useSWR(
traderId ? ['signal-rank', traderId] : null,
() => api.getSignalRanking(selectedTrader?.ai_model, 'mainnet', 'all', 30, true),
// paid x402 endpoint — poll slowly (5m) to conserve claw402 funds
{ refreshInterval: 300000, shouldRetryOnError: false }
)
// Demo / showcase mode for product walkthroughs. Toggle with Shift+D (or the
// discreet corner dot). Generates a fast, profitable-looking US-equity dataset
// entirely client-side — it never touches the backend or any real account.
// When off, real data flows through unchanged.
const [demo, setDemo] = useState(false)
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.shiftKey && (e.key === 'D' || e.key === 'd')) {
const el = document.activeElement
if (el && /^(input|textarea|select)$/i.test(el.tagName)) return
setDemo((v) => !v)
}
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [])
const sim = useDemoEngine(demo)
const on = demo && !!sim
const status = on ? sim!.status : propStatus
const account = on ? sim!.account : propAccount
const positions = on ? sim!.positions : propPositions
const decisions = on ? sim!.decisions : propDecisions
const fullStats = on ? sim!.fullStats : realFullStats
const history = on ? sim!.history : realHistory
const config = on ? (sim!.config as unknown as typeof realConfig) : realConfig
const flow = on ? sim!.flow : realFlow
const signalRank = on ? sim!.signalRank : realSignalRank
const latest = decisions && decisions.length > 0 ? decisions[0] : undefined
const candidateCoins = latest?.candidate_coins ?? []
const flowItems = flow?.data?.inflow ?? []
// Both the cost/liq map and the order book follow this symbol so they stay in
// sync. The heatmap only covers hip3_perp synthetic markets, so we pick a
// synthetic (non-crypto) the bot trades — preferring the BUSIEST one (most
@@ -236,6 +264,26 @@ export function TerminalDashboard({
return (
<div className="nofx-terminal" style={{ minHeight: '100vh', padding: 0 }}>
{/* discreet, unlabelled showcase toggle (Shift+D also works) */}
<button
type="button"
onClick={() => setDemo((v) => !v)}
aria-label="toggle presentation mode"
style={{
position: 'fixed',
right: 10,
bottom: 10,
zIndex: 9999,
width: 12,
height: 12,
padding: 0,
borderRadius: '50%',
border: 'none',
cursor: 'pointer',
background: on ? 'var(--tm-up)' : 'rgba(26,24,19,0.2)',
opacity: on ? 0.55 : 0.18,
}}
/>
{/* centered, capped content column — no border (keeps it from feeling
embedded) but bounded so the aspect-ratio SVGs don't balloon on wide screens */}
{navSlot &&
@@ -315,19 +363,20 @@ export function TerminalDashboard({
back to the other one if the guess is wrong */}
<LiquidationMap
symbol={activeSym}
demo={on}
marketType={CRYPTO_MAJORS.has(activeSym) ? 'perp' : 'hip3_perp'}
height={ROW1_H - 130}
/>
</div>
<div style={{ ...sc, borderRight: cellBorder, height: ROW1_H, overflow: 'hidden' }}>
<OrderBook symbol={activeSym} markPrice={positions?.find((p) => baseLabel(p.symbol) === activeSym)?.entry_price} />
<OrderBook symbol={activeSym} demo={on} markPrice={positions?.find((p) => baseLabel(p.symbol) === activeSym)?.entry_price} />
</div>
<div style={{ ...sc, height: ROW1_H, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<SignalMatrix items={signalRank?.items} active={activeSym} onSelect={setSelectedSym} />
<SignalMatrix items={signalRank?.items} max={18} active={activeSym} onSelect={setSelectedSym} />
{/* the live K-line always sits under the selector and flexes to fill */}
<div className="tm-rule" style={{ margin: '10px 0 8px' }} />
<div style={{ flex: 1, minHeight: 0 }}>
<KlineChart symbol={activeSym} fill />
<KlineChart symbol={activeSym} fill demo={on} />
</div>
</div>
</div>