mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 09:54:35 +08:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user