Skip to content

UI/UX overhaul: testnet/mainnet split, USD/KRW toggle, /orders/[id], dashboard rebuild #97

@fray-cloud

Description

@fray-cloud

Problem Statement

사용자가 PR3로 LLM 트레이드 흐름을 사용하기 시작했지만 주변 UI가 미완성이라 일상적인 운영이 어려움:

  • 포트폴리오paper/real/all 모드 토글을 보여주지만 PR1에서 paper 모드를 폐기했고 실제로는 ExchangeKey의 network(testnet/mainnet)로 갈라야 함. 현재는 testnet/mainnet 손익이 한 화면에 섞여 보임.
  • 모든 가격이 USD 단독 표시라 한국 사용자가 즉각적인 가치 판단이 어려움. useBaseCurrency / useExchangeRate 훅과 formatKrw 유틸은 이미 있는데 일부 페이지에서만 활용.
  • 활동 피드에서 주문 항목 클릭하면 404 — Order activity item에 link: '/orders'가 박혀 있는데 PR1에서 /orders 라우트 자체를 삭제함.
  • 대시보드는 "Check back soon" placeholder — 무엇을 보여줘야 하는지 미정의.

Solution

네 가지 개선:

  1. 포트폴리오 네트워크 분리 — testnet / mainnet / all 토글 (mode 파라미터 제거하고 network로 대체). 백엔드는 Order 행을 exchangeKey.network로 필터.
  2. USD/KRW 통화 토글 글로벌 노출 — nav-bar에 useBaseCurrency 토글 추가. 모든 가격 표시 컴포넌트가 토글에 반응. localStorage 영속.
  3. /orders/[id] 주문 상세 페이지 신설 — 캔들 차트 + entry/TP/SL 마커, 현재 mark price 기준 미실현 PnL, 수동 "Close Position" 버튼(실거래 모드 한정), 연결된 LlmDecisionLog의 reasoning.
  4. 대시보드 재구축 — 세 섹션:
    • PnL 카드 (오늘 / 이번주, testnet vs mainnet 분리)
    • 활성 포지션 테이블 + 인라인 "수동 닫기" 버튼
    • 최근 5개 LLM 결정 카드 (signal, TP/SL, reasoning, 결과)

User Stories

  1. As a 한국 트레이더, I want to see USD prices alongside KRW equivalents so that I can evaluate values in my local currency at a glance.
  2. As a 트레이더, I want a single toggle in the nav-bar to switch between USD-primary and KRW-primary so that my preference applies everywhere.
  3. As a 트레이더, I want my currency toggle preference to persist across reloads so that I don't have to set it every session.
  4. As a 트레이더, I want to filter portfolio by testnet only / mainnet only / both so that I see real-money status separately from sandbox practice.
  5. As a 트레이더, I want testnet PnL labelled clearly as "모의" so that I never confuse it with real gains/losses.
  6. As a 트레이더, I want to click an order in the activity feed and land on its detail page (not a 404) so that navigation is reliable.
  7. As a 트레이더 on the order detail page, I want to see the entry price, TP and SL marked on the candle chart so that I understand the trade visually.
  8. As a 트레이더 on the order detail page, I want to see current unrealized PnL refreshed every few seconds so that I can monitor live status.
  9. As a 트레이더 with an open mainnet position, I want a "Close Position" button on the order detail page so that I can react to news mid-trade without going to Binance.
  10. As a 트레이더 on the order detail page, I want to see the LLM reasoning that opened this trade so that I can evaluate model quality post-hoc.
  11. As a 트레이더, I want the dashboard to show today's realized PnL split by testnet vs mainnet so that I know my position at a glance.
  12. As a 트레이더, I want the dashboard to show this week's cumulative PnL so that I have weekly context.
  13. As a 트레이더, I want the dashboard to list my open positions with one-click close access so that emergency exits are fast.
  14. As a 트레이더, I want the dashboard to show my last 5 LLM decisions with their reasoning so that I see the model's recent thinking.
  15. As a 트레이더, I want each LLM decision card to show the trade outcome (filled / stopped out / take-profit hit) so that I can correlate signal quality with results.
  16. As a 사용자 in /settings, I want to register both a testnet and mainnet API key for the same exchange (binance) so that I can practice and trade live with one account.
  17. As a 트레이더, I want USD/KRW conversions to use a recently-cached exchange rate so that values aren't stale.

Implementation Decisions

Backend

  • PortfolioService.getSummary(userId, network?: 'testnet' | 'mainnet' | 'all') — replace mode: paper/real/all. Filter Order rows by joined exchangeKey.network. "all" returns both networks, plus a byNetwork breakdown so the UI can show split totals from one call.
  • ActivityService — order activity item links from /orders/orders/${order.id}.
  • New GET /orders/:id — returns: Order row, related LlmDecisionLog (via FK), live mark price (from exchange), computed unrealized PnL, exchangeKey network.
  • New POST /orders/:id/close — manual close. Validates order is real-mode and still open. Publishes a Kafka close event consumed by the worker saga, which calls BinanceRest.closePosition() (MARKET reduceOnly — already known to work post-PR3 fix).
  • New GET /llm-trades/decisions?limit=N — paginated user-scoped list, newest first, joined with Order for outcome status.
  • New GET /dashboard/summary — single aggregate endpoint: { pnl: { today: {testnet, mainnet}, week: {testnet, mainnet} }, openPositions: Order[], recentDecisions: LlmDecisionLog[] }. Avoids three round-trips on dashboard load.

Frontend

  • /orders/[id]/page.tsx — new page. Sections: header (symbol + side + status badge), OrderChart component (CandleChart wrapper with TP/SL overlay markers), PnL panel, Close Position button (gated: real mode + open status), reasoning card.
  • OrderChart component — extends existing CandleChart with horizontal lines at entry/TP/SL and small text labels.
  • /dashboard/page.tsx — full rewrite. Calls GET /dashboard/summary once. Three sections rendered in CSS grid (responsive).
  • /portfolio/page.tsx — replace mode: paper/real/all toggle with network: testnet/mainnet/all toggle. Show split totals when "all" selected.
  • <BaseCurrencyToggle> in nav-bar.tsx — small KRW ⇄ USD pill button. Uses existing useBaseCurrency hook (already localStorage-backed). Visible on every page.
  • Currency display utility — generalise so any price-rendering component takes (price, baseCurrency, krwPerUsd) and emits { main, sub } strings consistently. Apply to: ticker-table, ticker-card-list, candle-chart label, /orders/[id] PnL, /portfolio/page, dashboard PnL cards.
  • Activity page — already cleaned in PR1; just need link to point to new /orders/[id].

Data

  • No schema changes required.
  • ExchangeKey.network (PR2), Order.realizedPnl / closedAt / entryPrice / takeProfitPrice / stopLossPrice / tpOrderId / slOrderId (PR2), LlmDecisionLog.orderId (PR2) — all in place.

API Contracts

  • GET /portfolio/summary?network=testnet|mainnet|all
  • GET /orders/:id{ order, decision?, markPrice, unrealizedPnl }
  • POST /orders/:id/close{ status: 'pending' } then WS order:updated
  • GET /llm-trades/decisions?limit=20&cursor=...
  • GET /dashboard/summary → aggregate

Testing Decisions

External behavior only — never assert on internal call ordering or private fields.

  • PortfolioService unit (Vitest, mocked Prisma): given a mix of testnet and mainnet filled Orders, summary with network=testnet returns only testnet aggregates and zero mainnet totals; network=all returns both plus split.
  • ActivityService unit: order activity items have link: '/orders/${id}'.
  • GET /orders/:id controller test: 200 with hydrated payload for own order, 404 for missing, 403 for other user's order.
  • POST /orders/:id/close controller: rejects on already-closed orders, rejects on testnet-mode-only env if mainnet config disabled, publishes Kafka event with correct shape.
  • Worker close saga unit: calls BinanceRest.closePosition with opposite side + filled quantity + reduceOnly.
  • useBaseCurrency integration: toggle persists via localStorage; rendering helper outputs { main: '$X', sub: '₩Y' } correctly for both modes.
  • /orders/[id] page: mocks API response, asserts entry/TP/SL markers render on chart, Close button hidden when order closed.

Prior art: existing Vitest patterns under apps/api-server/src/**/*.test.ts (mocked Prisma + class-based service tests), apps/web/src/**/*.test.tsx (component tests with mocked queries).

Out of Scope

  • Position watcher (no longer needed — algoOrder migration in PR3 fix made conditional orders work directly on Binance).
  • Manual non-LLM trading (no "place a limit order yourself" form).
  • Multi-symbol watchlist or alerts on dashboard.
  • Performance/quality analytics of LLM signals over time (separate PRD).
  • Mobile-specific layouts (responsive CSS only, no native).
  • Mainnet enablement gate (already env-gated since PR3).
  • Internationalisation beyond ko/en (no JP, CN, etc.).

Further Notes

  • The 404 on activity feed is purely the stale /orders link from PR0 — not a routing bug. The fix is one line in ActivityService plus the new detail page.
  • Currency toggle hook + helpers already exist; this PRD is mostly applying them everywhere rather than building fresh infrastructure.
  • Dashboard is a single aggregate endpoint to avoid waterfall loads on a screen most users will visit first thing every session.
  • Order detail "Close Position" should reuse PR3's BinanceRest.closePosition() MARKET reduceOnly path (verified working post -4120 fix).
  • All new pages must use useBaseCurrency from day one — no hardcoded "$" prefixes — to avoid having to retrofit later.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions