# PR #70 — WebSocket coverage audit + TradingView-style blink-effect research

**Author:** tele
**Date:** 2026-04-19
**Source branch:** `feature/nan-20-live-price-websocket` (PR #70 HEAD `89751769`)

---

## Part 1 — WebSocket integration coverage

The single hook `useLiveTickerPrice(ticker)` is the canonical consumer. It both subscribes (ref-counted via `MarketWebSocketProvider`) and reads via `useSyncExternalStore` from `LivePriceStore` (300 ms batched flush). Any component below that calls it shows live prices.

### Components currently wired to live WS prices (15)

| Feature | File | Surface |
|---|---|---|
| home | `home/presentation/components/WatchlistCard.tsx` | Home → watchlist preview rows |
| watchlist | `watchlist/presentation/components/WatchlistRow.tsx` | Watchlist detail rows |
| watchlist | `watchlist/presentation/components/AddStocksSheet.tsx` | "Add stocks" picker prices |
| market | `market/presentation/components/MarketMoverRow.tsx` | Market movers compact row |
| market | `market/presentation/components/MarketMoverStockItem.tsx` | Market movers detail item |
| market | `market/presentation/components/SectorStockRow.tsx` | Sector detail rows |
| stock-ideas | `stock-ideas/presentation/components/IdeaStockRow.tsx` | Stock ideas list |
| simtrade | `simtrade/presentation/components/HoldingsList.tsx` | SimTrade holdings rows |
| simtrade | `simtrade/presentation/components/SimilarStocksCarousel.tsx` | SimTrade similar stocks |
| screener | `screener/presentation/components/ScreenerResultRow.tsx` | Screener result rows |
| stock-detail | `stock-detail/presentation/components/ExpandedHeader.tsx` | Stock detail header (full) |
| stock-detail | `stock-detail/presentation/components/CollapsedHeader.tsx` | Stock detail collapsed nav |
| stock-detail | `stock-detail/presentation/components/SimilarStocksCard.tsx` | Stock detail "similar" rail |
| market | `market/presentation/hooks/useLiveSparkline.ts` | Drives all sparkline last-bar updates |
| stock-detail | `stock-detail/presentation/hooks/useChartData.ts` | Last-bar live merge for the ticker chart |

### What this means in practice
- **Home** — watchlist preview ✅, market movers section ✅
- **Watchlist detail** — rows + add-stocks picker ✅
- **Market** — movers (all tabs) ✅, sector detail ✅
- **Stock detail** — both headers ✅, similar stocks card ✅, the chart's last bar ✅, sparklines ✅
- **Stock ideas** — row prices ✅
- **Screener** — result rows ✅
- **SimTrade** — holdings ✅, similar stocks ✅

### Likely NOT wired yet (gaps)
Quick scan vs the PR body's claim. Worth checking if user cares:
- **Search results** (recently searched + result rows) — PR body claims wired; couldn't find direct `useLiveTickerPrice` import in search components. May be transitively via a shared row component, or may be a real gap.
- **News ticker tags** — PR body claims wired; couldn't find import in news/ feature. Same caveat — may be transitive or real gap.
- **Earnings spotlight** — PR body claims wired; not found in `earnings/` feature components.
- **SubmitOrder / order flow** — not wired (intentional per scope; trading needs a stable snapshot, not live ticks).
- **Portfolio summary cards** (account value, day change) — wired in stock detail headers only. Account-level aggregate values (top of portfolio screen) appear to use REST `useSnapshotQuery`, not live ticks.

**Recommendation:** flag the 3 above (search recents, news tags, earnings spotlight) to the 70-wagmi-charts agent for verification — they were in the original PR scope but the import map suggests they may have slipped during refactor.

---

## Part 2 — TradingView-style price-change blink (text-color only, partial-digit highlight)

### What user wants
- Old price 251.04 → new price 252.03
- Only the **changed suffix** ("2.03") changes color briefly (~600–1000 ms)
- **Text color animation only** — no background-color flash, no chip, no card pulse
- Up = green, down = red (TradingView convention; agent confirms)

### Codebase already has a precedent — but wrong shape
`MarketMoverStockItem.tsx:21` defines `usePriceBlink(formattedPrice)` which:
- Detects formatted-string change between renders
- Animates an `Animated.Value` from 1 → 0 over 400 ms
- Drives the **`backgroundColor`** of an `Animated.View` overlaid behind the change text

So the project already proves the React Native primitives work. What needs changing for the TradingView style:
1. Animate **text color** instead of background color
2. Target **only the diff suffix** (the digits that actually changed), not the whole change row
3. Optionally tune duration to ~600–800 ms (current 400 ms is already close)

### React Native feasibility — yes, fully

Two viable approaches:

**A. `Animated.Text` with color interpolation** (matches existing `usePriceBlink` pattern)
```tsx
import { Animated } from 'react-native';

const colorAnim = useRef(new Animated.Value(0)).current;
const animatedColor = colorAnim.interpolate({
  inputRange: [0, 1],
  outputRange: [baseColor, highlightColor],
});

useEffect(() => {
  if (changed) {
    colorAnim.setValue(1);
    Animated.timing(colorAnim, { toValue: 0, duration: 700, useNativeDriver: false }).start();
  }
}, [price]);

return (
  <Text style={styles.price}>
    {prefix}
    <Animated.Text style={{ color: animatedColor }}>{suffix}</Animated.Text>
  </Text>
);
```
- **Pros:** matches the existing in-house pattern, no new deps
- **Cons:** `useNativeDriver: false` for color (color isn't a native-driver-supported prop on RN); runs on JS thread, but a single color tween is cheap

**B. Reanimated `useSharedValue` + `useAnimatedStyle` + `interpolateColor`**
```tsx
import Animated, { useSharedValue, useAnimatedStyle, withTiming, interpolateColor } from 'react-native-reanimated';

const phase = useSharedValue(0);

useEffect(() => {
  if (changed) {
    phase.value = withSequence(withTiming(1, { duration: 0 }), withTiming(0, { duration: 700 }));
  }
}, [price]);

const animatedStyle = useAnimatedStyle(() => ({
  color: interpolateColor(phase.value, [0, 1], [baseColor, highlightColor]),
}));

return (
  <Text style={styles.price}>
    {prefix}
    <Animated.Text style={[styles.priceSuffix, animatedStyle]}>{suffix}</Animated.Text>
  </Text>
);
```
- **Pros:** runs on UI thread, smoother under GC, Reanimated already in the tree
- **Cons:** slightly more boilerplate; `Animated.Text` from Reanimated nests inside RN `<Text>` cleanly on iOS + Android

**Recommendation:** **Approach B (Reanimated)** for two reasons:
1. The codebase is moving toward Reanimated 3 patterns (used in InteractiveLineChart, wagmi-charts uses it under the hood)
2. UI-thread color animation will scale better when 10+ rows blink simultaneously (e.g. watchlist with all live)

### The diff logic (which digits to highlight)

Common-prefix diff on the formatted string:
```ts
function diffFormatted(prev: string, next: string): { prefix: string; suffix: string } {
  let i = 0;
  while (i < prev.length && i < next.length && prev[i] === next[i]) i++;
  return { prefix: next.slice(0, i), suffix: next.slice(i) };
}
```
- For `251.04 → 252.03` → prefix = `"25"`, suffix = `"2.03"` ✅
- For `99.99 → 100.05` → prefix = `""`, suffix = `"100.05"` (whole number flashes — correct, the cents place matters but so does the digit count)
- For `1234.56 → 1234.57` → prefix = `"1234.5"`, suffix = `"7"` (only the last cent)
- For `$251.04 → $252.03` (with currency symbol) → prefix = `"$25"`, suffix = `"2.03"` ✅
- Edge case: same value (no change) → suffix = `""` → no animation triggered

### Direction (color choice)

```ts
const direction = price > prevPrice ? 'up' : price < prevPrice ? 'down' : 'flat';
const highlightColor = direction === 'up' ? colors.success : colors.danger;
```
TradingView convention: green for up, red for down. Skip animation entirely on `flat` (price equal — common case when sub-second ticks repeat).

### Where to apply (rollout order)

1. **Phase 1 — primitive component** — build `<LivePriceText price={n} format={fn} style={...} />` in `shared/ui/` or `features/market/presentation/components/`
2. **Phase 2 — high-impact screens first** — Stock Detail (Expanded + Collapsed headers), WatchlistRow, MarketMoverRow / MarketMoverStockItem
3. **Phase 3 — broader rollout** — SectorStockRow, IdeaStockRow, ScreenerResultRow, SimTrade rows
4. **Phase 4 — replace existing `usePriceBlink`** in MarketMoverStockItem with the primitive (drops the background-flash version)

### Risks / gotchas

- `<Animated.Text>` nested in `<Text>` works but the inner can't have its own font-family override that differs from the outer, otherwise inheritance breaks line-height alignment
- If 30+ rows blink simultaneously (watchlist with full live), the JS-thread Animated approach (A) may stutter — Reanimated (B) avoids this
- Diff via formatted string assumes consistent locale formatting; pass through the same `Intl.NumberFormat` for both prev + next
- For the chart's last-bar updates (`useChartData`), do NOT apply blink — that's a chart node, not a text node
- Don't blink when hydrating from REST (initial mount): only animate from second render onward (track `prevPriceRef`)

### Tunable params
- Duration: 700 ms recommended (TradingView ~600–800 ms feels right)
- Highlight intensity: full-color highlight (no opacity gradient) — TradingView is binary, not faded
- Same-value debounce: skip animation if `Math.abs(price - prev) < 0.01` (sub-cent noise)

---

## Open questions for user

1. **Search recents / news ticker tags / earnings spotlight** — were these supposed to be wired per the PR body? Want me to ask the 70 agent to verify and patch any real gaps?
2. **Direction colors** — green-for-up + red-for-down (TradingView convention), or always green for any change?
3. **Rollout cadence** — start with high-impact screens (Stock Detail headers + WatchlistRow + MarketMoverRow) and expand from there, or apply everywhere at once?
4. **Replace existing background-flash** — confirm we deprecate the current `usePriceBlink` (background flash in MarketMoverStockItem) in favor of the new text-color primitive across the board.

---

**Next step:** user decides on the open questions. If GO, dispatch a `<LivePriceText>` primitive build + phased rollout to the 70-wagmi-charts agent (or a new agent if 70 is closing out).
