Skip to content
Back to skills

Frontend Patterns

ASecurity

Fintech-specific frontend patterns — financial data formatting (BRL/USD/USDT/

  • 2 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added September 2, 2026
ai-agentstypescriptgoreactnodegitapifrontendbackendsecurityperformance

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Scanned September 2, 2026

npx -y skills add Canhada-Labs/ceo-orchestration --skill frontend-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Frontend Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Frontend Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/canhada-labs-frontend-patterns/badge)](https://www.skillsdirectory.com/skills/canhada-labs-frontend-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: fintech-frontend-patterns
description: Fintech-specific frontend patterns — financial data formatting (BRL/USD/USDT/
  multi-currency), trading terminal component architecture (order book virtualization,
  trading form), real-time price update patterns, PRO tier gating (Free/Pro/Trader/Quant
  ladder), accessibility rules specific to dense financial data, and fintech anti-patterns.
  Use when working on any trading, market data, orderbook, price display, tier gating, or
  financial-domain UI code in the {{PROJECT_NAME}} frontend. EXTENDS the universal skill at
  `frontend/frontend-patterns/SKILL.md` — read that first, then apply the rules here.
owner: Alex Rivera
# --- smart-loading fields (PLAN-083 Wave 0b sub-agent 0.7b) ---
domain: fintech
priority: 5
risk_class: low
stack: [typescript, react]
context_budget_tokens: 1000
inactive_but_retained: false
repo_profile_binding:
  frontend: {active: true, priority: 4}
  engine: {active: true, priority: 9}
  fintech: {active: true, priority: 4}
  trading-readonly: {active: true, priority: 8}
  generic: {active: true, priority: 7}
activation_triggers:
  - {event: help-me-invoked, regex: "(?i)trading.?terminal|tier.?gating|pro.?tier|trading.?form"}
# --- K1 paths: native file-touch activation (PLAN-135 W3 unit k1a) ---
paths:
  - "**/trading/**"
  - "**/terminal/**"
  - "**/tiers/**"
  - "**/orderbook/**"
---

# Fintech Frontend Patterns

> **This skill extends `frontend/frontend-patterns/SKILL.md`.** Read the
> universal skill first for architecture principles, composition patterns,
> error boundaries, lazy loading, WS/SSE lifecycle, state management,
> virtualization, memoization, responsive design, accessibility basics, and
> performance monitoring. This document layers fintech-specific rules on top.

## Fintech Overview

| Item | Value |
|------|-------|
| Primary users | Brazilian traders (default locale `pt-BR`) |
| Currencies | BRL, USD, USDT, MXN, ARS, EUR (more possible) |
| PRO tiers | Free, Pro, Trader, Quant |
| Real-time | WebSocket (market data) + SSE (book updates, alerts) |
| Dense-data pages | Trading terminal, order book, arbitrage, spreads, depth |

## Fintech-Specific Component Hierarchy

Above and beyond the universal `Pages` / `Shared` split, fintech apps usually
grow these page families and providers:

```
App
  ├── AuthProvider (JWT token, user tier)
  ├── Pages
  │     ├── Market data pages (exchanges, orderbooks, pairs)
  │     ├── Trading pages (terminal, orders, positions)
  │     ├── Analytics pages (arbitrage, spreads, depth)
  │     ├── Intelligence pages (AI, predictions, social)
  │     └── Settings pages (billing, API keys, profile)
  └── Shared
        ├── PRO widgets (gated by tier)
        └── Financial formatters (prices, volumes, timestamps)
```

### Extra Rules

1. **PRO widgets MUST check tier** before rendering content. Show upgrade CTA
   for insufficient tier.
2. **Every component that displays financial data MUST use the formatting
   utilities.** Never format prices/volumes inline.
3. **AuthProvider MUST expose the user tier** so PRO widgets can read it
   synchronously during render.

## Financial Data Formatting

### Price Formatting

```typescript
// CORRECT — locale-aware, precision-preserving
function formatPrice(
  value: string | number,
  options?: {
    currency?: string;        // "BRL", "USD", "USDT"
    significantDigits?: number;
    locale?: string;          // default "pt-BR"
  }
): string {
  const { currency = "USD", significantDigits = 8, locale = "pt-BR" } = options ?? {};

  // Use Intl.NumberFormat for locale-aware formatting
  const num = typeof value === "string" ? parseFloat(value) : value;

  // Dynamic precision based on magnitude
  const decimals = num >= 1000 ? 2
    : num >= 1 ? 4
    : num >= 0.01 ? 6
    : 8;

  return new Intl.NumberFormat(locale, {
    minimumFractionDigits: 2,
    maximumFractionDigits: decimals,
    style: currency === "BRL" || currency === "USD" ? "currency" : "decimal",
    currency: currency === "BRL" ? "BRL" : currency === "USD" ? "USD" : undefined,
  }).format(num);
}

// WRONG — hardcoded formatting
function formatPrice(value: number): string {
  return `$${value.toFixed(2)}`; // Loses precision, wrong locale, wrong currency
}
```

### BRL Locale Specifics

- Decimal separator: `,` (comma)
- Thousands separator: `.` (period)
- Currency symbol: `R$` (prefix with space)
- Example: `R$ 1.234.567,89`
- Default locale for the platform: `pt-BR`

### Volume Formatting

```typescript
// Abbreviate large volumes
function formatVolume(value: number, locale = "pt-BR"): string {
  if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(1)}B`;
  if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
  if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
  return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(value);
}
```

### Timestamp Formatting

```typescript
// Always show relative time for recent, absolute for old
function formatTimestamp(ms: number, locale = "pt-BR"): string {
  const age = Date.now() - ms;
  if (age < 60_000) return `${Math.floor(age / 1000)}s ago`;
  if (age < 3_600_000) return `${Math.floor(age / 60_000)}min ago`;
  return new Intl.DateTimeFormat(locale, {
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
  }).format(ms);
}
```

### Formatting Rules

1. **NEVER use `toFixed()` directly in JSX.** Always use a formatter function.
2. **Locale MUST default to `pt-BR`** (primary user base is Brazilian).
3. **Currency MUST come from the data, not hardcoded.** BRL, USD, USDT, MXN, ARS, EUR all possible.
4. **Negative values MUST be visually distinct** (red color, minus sign).
5. **Zero values MUST be shown as "0", not empty string or "-".**
6. **Loading states MUST show skeleton, not "0" or "N/A".**

## Trading Terminal Component Architecture

### Order Book Component

```
OrderBookWidget
  ├── OrderBookHeader (pair, spread, mid price)
  ├── AskSide (virtualized list, sorted ascending)
  │     └── PriceLevel (price, size, cumulative, depth bar)
  ├── MidPrice (current mid, trend arrow)
  ├── BidSide (virtualized list, sorted descending)
  │     └── PriceLevel (price, size, cumulative, depth bar)
  └── OrderBookFooter (depth summary, exchange source)
```

### Order Book Virtualization

Order books routinely show 500+ price levels per side. That makes virtualization
mandatory — rendering every level kills FPS during WS updates. Use
`react-window` or `@tanstack/react-virtual` exactly as described in the
universal skill, but pin row height and keep depth bars off the main layout
pass:

```typescript
import { useVirtualizer } from "@tanstack/react-virtual";

function OrderBookSide({ levels }: { levels: PriceLevel[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: levels.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 24,  // order-book rows are dense
    overscan: 8,
  });

  return (
    <div ref={parentRef} className="ob-side">
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
        {virtualizer.getVirtualItems().map((vRow) => (
          <PriceLevelRow
            key={levels[vRow.index].price}  // stable key = price string
            level={levels[vRow.index]}
            style={{
              position: "absolute",
              top: 0,
              transform: `translateY(${vRow.start}px)`,
              height: `${vRow.size}px`,
            }}
          />
        ))}
      </div>
    </div>
  );
}
```

### Rules for Order Book UI

1. **Order book MUST be virtualized.** Rendering 500+ price levels kills FPS.
2. **Price levels MUST show depth bars** (visual representation of size at each level).
3. **Spread MUST be prominently displayed** between bid and ask sides.
4. **Color coding:** Green for bids, red for asks. NEVER use color alone (add +/- signs).
5. **Flash animation** on price level changes (brief highlight, 200ms).
6. **Stale indicator:** If book data is >15s old, show warning badge.
7. **Stable keys** from the price string, never array index — keys must survive
   delta updates.

### Trading Form Component

The order-entry form is the highest-stakes UI surface in the app. Treat it as a
single feature area with these contracts:

```
TradingForm
  ├── PairSelector
  ├── SideToggle (Buy / Sell)
  ├── OrderTypeTabs (Market / Limit / Stop)
  ├── PriceInput (disabled for Market)
  ├── QuantityInput (with slider for percent of balance)
  ├── QuoteEstimate (fees, slippage, total)
  └── SubmitButton (confirmation modal on click)
```

### Rules for Trading Form

1. **Optimistic validation runs on every keystroke.** Show inline errors, don't
   wait for submit.
2. **Quote estimate refreshes on input pause (300ms debounce)**, not on every
   keystroke.
3. **Tab order MUST be pair → side → type → price → quantity → submit.**
4. **Submit MUST require a confirmation modal** showing final price, quantity,
   fees, total, and a "this is irreversible" line.
5. **Disable submit while a previous submission is in-flight.** No double orders.
6. **Form state is ephemeral.** Use `useState` or a dedicated `trading-store`
   that is NOT persisted.
7. **Never prefill the quantity field from the previous order.** Each order is
   a fresh decision.

## Real-Time Price Update Patterns

Real-time price updates need more than the generic WS lifecycle from the
universal skill. Layer these fintech rules on top:

1. **rAF batching is mandatory.** Price streams fire faster than the refresh
   rate. Coalesce updates in a `requestAnimationFrame` tick (e.g. 30 books per
   frame max), carry excess to the next frame.
2. **Per-instrument throttle** (50ms per exchange:pair, max 20 updates/sec/book)
   for depth updates.
3. **Delta protocol applies to pending frame**, not committed state, so
   overlapping deltas collapse cleanly.
4. **Flash animation on change**: wrap the price cell with a brief 200ms
   highlight (green on up-tick, red on down-tick). Use CSS `@keyframes`, not JS
   interval.
5. **Stale badge after 15s of no update** on any visible price. Stale prices
   are worse than no prices.
6. **Components MUST NOT subscribe to the raw WS store.** Use typed selector
   hooks (`usePriceForPair`, `useSpread`, `useBookDepth`) that use `useShallow`
   so only the interested component re-renders.

## PRO Widget Gating

### Tier System

| Tier | Widgets | Data Refresh | API Calls |
|------|---------|-------------|-----------|
| Free | Basic (market overview, top pairs) | 30s polling | 100/day |
| Pro | +50 widgets (analytics, spreads) | 5s polling + SSE | 1K/day |
| Trader | +30 widgets (trading terminal) | Real-time WS | 5K/day |
| Quant | All widgets (AI, predictions) | Real-time WS | Unlimited |

### Gating Pattern

```typescript
function ProWidget({
  requiredTier,
  children,
}: {
  requiredTier: "pro" | "trader" | "quant";
  children: React.ReactNode;
}) {
  const { tier } = useAuth();
  const tierRank = { free: 0, pro: 1, trader: 2, quant: 3 };

  if (tierRank[tier] < tierRank[requiredTier]) {
    return (
      <UpgradeCTA
        currentTier={tier}
        requiredTier={requiredTier}
        featureDescription="Access advanced analytics"
      />
    );
  }

  return <>{children}</>;
}

// Usage
<ProWidget requiredTier="trader">
  <TradingTerminal />
</ProWidget>
```

### Rules

1. **Gating check MUST happen client-side AND server-side.** Client for UX,
   server for security.
2. **Upgrade CTA MUST be contextual** ("Upgrade to Trader to access trading
   terminal").
3. **Never hide PRO features entirely.** Show blurred/teaser version with
   upgrade prompt.
4. **Free tier MUST still be useful.** Market overview, top 10 pairs, basic
   charts.
5. **Currently 95% of backend endpoints lack tier gating** (audit finding).
   Frontend gating is the ONLY layer until backend is fixed.

## Responsive Density for Trading UIs

Density rules that override the universal responsive defaults for financial
data:

| Viewport | Order Book | Pair List | Charts |
|----------|-----------|-----------|--------|
| Mobile | Top 5 bids + asks | Card view, 10 visible | Simplified, no overlays |
| Tablet | Top 15 bids + asks | Compact table, 20 visible | Standard with tooltips |
| Desktop | Top 25 bids + asks | Full table, virtualized | Full with overlays |
| Wide | Top 50 + depth chart | Multi-column, virtualized | Multiple chart panels |

### Rules

1. **Desktop is primary** for the trading terminal. Mobile must still be
   functional for monitoring.
2. **Font size MUST NOT go below 12px** for financial data (readability).
3. **Charts MUST be touch-friendly** on tablet (larger touch targets).

## Accessibility for Financial Data

On top of the universal accessibility basics, financial UIs need:

1. **Price levels MUST have aria-labels** with full context:
   `aria-label="Bid: 50,123.45 BRL, size 0.5 BTC, cumulative 2.3 BTC"`.
2. **Status changes MUST use `aria-live="polite"`** for updates:
   book status, connection status, alert notifications.
3. **Charts MUST have text alternatives** (summary table or description).
4. **Order entry form: Tab order MUST follow logical sequence**
   (pair → side → type → price → quantity → submit).
5. **Arrow keys for navigating order book levels.**
6. **Financial data MUST be readable** without visual styling (plain text
   fallback when CSS fails to load).
7. **Provide a high-contrast mode** for the trading terminal.

## Fintech Anti-Patterns

| Anti-Pattern | Why It's Wrong | Correct Approach |
|---|---|---|
| `toFixed(2)` in JSX | Wrong locale, wrong precision | `formatPrice()` utility |
| Hardcoded "R$" / "$" | Breaks for USD, USDT, MXN pairs | `Intl.NumberFormat` with currency |
| Rendering 500 price levels without virtualization | DOM thrashing on every WS tick | Virtualize order book sides |
| Updating the DOM on every WS message | Re-render storm | rAF batching + per-book throttle |
| Color-only bid/ask indicator | Accessibility violation | Color + `+`/`-` + arrow |
| Persisting trading form state | Stale orders on next session | Ephemeral `useState` or non-persisted store |
| Prefilling quantity from last order | Encourages unintended trades | Always start at zero |
| Client-only PRO gating | Bypassable by any user | Gate on server too |
| Showing "0" while price stream warms up | Misleading | Skeleton / "—" with explicit "loading" aria |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…