Markets / OHLC Phase 2 — Actionability & Observability

This document covers the five feature areas added in Phase 2 of the markets/OHLC feature track. Phase 1 (harmonic detection, signal scoring, multi-TF confirmation, backtest, equity curve) is documented in OHLC Feature Enhancements. Phase 2 layers actionability (watchlist, paper portfolio, drawing tools) and observability (analytics dashboard, desktop/email notifications) on top of that pipeline — with no new detection logic.


1. Desktop & Email Notifications

Desktop — Permission & Toggle

The browser Notification API requires a user gesture to request permission. A dedicated NotificationToggle.tsx component renders a bell icon button and calls Notification.requestPermission() directly inside its onClick handler — satisfying the gesture requirement without a separate modal.

Permission state is stored in Redux (ohlcSlice.notificationsEnabled: boolean). It is not persisted to localStorage; the browser’s own permission grant persists across page loads, so the Redux flag is only a runtime signal-routing switch.

Desktop — Firing Alerts

useOhlcAlerts.ts (introduced in Phase 1 for in-app snackbars) was extended with a notification branch:

if (notificationsEnabled && Notification.permission === 'granted') {
  new Notification(`${event.patternType} detected`, {
    body: `${event.symbol} ${event.timeframe} — score ${event.signalScore}`,
  });
}

Both conditions must be true — the Redux flag guards against accidental firing if the browser permission was later revoked.

Email — Backend Threshold

An admin-facing fire-and-forget email is sent whenever a new detection event’s signalScore meets or exceeds a configurable threshold:

Env varDefaultDescription
OHLC_ALERT_EMAIL_THRESHOLD0.7Minimum signal_score that triggers an admin email alert

The email is dispatched via Nodemailer inside ohlcPatternService immediately after the detection event is persisted. Delivery is fire-and-forget (void sendAlertEmail(...)) — a Nodemailer transport failure does not affect the WebSocket broadcast or the HTTP response. Only users with the admin role receive these emails.


2. Analytics Dashboard (/markets/analytics)

Service: ohlcAnalyticsService.ts

Pure in-app aggregation over the ohlc_pattern_detection_events table — no new database tables or views. The service returns:

MetricGrouping
Win rate + avg P&L (pips)By pattern type
Win rate + avg P&L (pips)By timeframe
Win rate + avg P&L (pips)By symbol
Win rate + avg P&L (pips)By signal score bucket (0–0.5, 0.5–0.7, 0.7–1)
Top 5 performersSymbol + timeframe + pattern combinations, ranked by win rate then avg P&L

“Win” is defined as a detection event whose outcome field resolves to a non-loss value (i.e. hit TP1, TP2, or TP3 before SL). P&L is read directly from pnl_pips on the detection event row.

Frontend

The /markets/analytics route renders:

  • KPI cards — overall win rate, average P&L (pips), total signals evaluated
  • MUI X BarCharts — one bar chart per grouping dimension (pattern type, timeframe, symbol, score bucket)
  • Top performers table — symbol / TF / pattern columns, win-rate and avg-P&L columns

All data is fetched via a single RTK Query endpoint (useGetOhlcAnalyticsQuery) and cached for 60 seconds.


3. Chart Drawing Tools

Data Model

Two drawing types are supported:

type DrawingMode = 'none' | 'trendline' | 'hline';
 
interface ChartDrawing {
  id: string; // nanoid
  type: DrawingMode;
  time1: number; // Unix timestamp (seconds)
  price1: number;
  time2?: number; // trendlines only
  price2?: number; // trendlines only
}

Drawings are stored in ohlcSlice.drawings: ChartDrawing[] (Redux). They are client-side only and are not persisted to the database or to localStorage — they reset on page reload by design.

Toolbar: DrawingToolbar.tsx

MUI ToggleButtonGroup with three buttons (None, Trendline, H-Line). Selecting a mode dispatches setDrawingMode(mode). A separate “Clear all” IconButton dispatches clearDrawings().

Chart Integration: MarketsChart.tsx

onClick on chart container
  └─ if drawingMode === 'hline'   → dispatch(addDrawing({ type: 'hline', ... }))
  └─ if drawingMode === 'trendline'
       └─ first click  → store pending anchor in local ref
       └─ second click → dispatch(addDrawing({ type: 'trendline', ... }))

A useEffect keyed on [drawings, chart] re-renders all drawings as lightweight-charts LineSeries (colour: amber #FFC107) every time the array changes. Each series is tracked in a drawingSeriesRef map so that stale series are removed before re-adding — preventing duplicate overlays on symbol/timeframe switches.


4. Watchlist (/api/ohlc/watchlist)

Database Table: ohlc_watchlist

Stored in TimescaleDB (standard Postgres table, not a hypertable):

ColumnTypeNotes
idUUID PKgen_random_uuid()
user_idUUID FKReferences users.id (cascade delete)
symboltexte.g. EUR/USD
timeframetextOptional; null = match any TF
pattern_typetextOptional; null = match any pattern
min_signal_scorenumericOptional; null = no score filter
created_attimestamptznow() default

Unique constraint on (user_id, symbol, timeframe, pattern_type) — prevents duplicate subscriptions for the same combination (null values participate in the uniqueness check via a partial index).

REST Endpoints

All three routes are auth-protected (JWT access token required):

MethodPathDescription
GET/api/ohlc/watchlistReturns the authenticated user’s watchlist entries
POST/api/ohlc/watchlistAdds a new entry (body: symbol, optional timeframe, patternType, minSignalScore)
DELETE/api/ohlc/watchlist/:idRemoves a single entry by ID

Frontend: WatchlistPanel.tsx

Renders inside the Markets sidebar. Contains a controlled form (symbol text field, TF select, pattern select, min-score slider) that calls the POST endpoint. The existing entries are listed below with per-row delete buttons. RTK Query handles optimistic cache invalidation via providesTags: ['Watchlist'] / invalidatesTags: ['Watchlist'].

Future Integration

The watchlist table is currently used only for UI display. The planned next step is to filter live WebSocket alert delivery server-side: when ohlc:pattern:detected fires, the backend will query ohlc_watchlist and push the event only to the socket connections belonging to users whose watchlist entries match the event’s symbol, timeframe, pattern type, and minimum score.


5. Paper Portfolio (/markets/portfolio)

Design Approach — No New Table

Open positions are derived at query time from ohlc_pattern_detection_events WHERE outcome = 'open'. There is no separate positions table. This means:

  • Zero schema migration required
  • Positions automatically appear when a pattern is detected and disappear when its outcome resolves (SL/TP hit)
  • The portfolio is a live view of the detection pipeline’s unresolved events

Service: ohlcPaperPortfolioService.ts

1. Fetch all detection events WHERE outcome = 'open'
2. For each unique (symbol, timeframe) pair, fetch the latest candle close
3. Compute unrealised P&L:
     direction = bullish → entryPrice = dPrice, currentPrice = latestClose
     unrealisedPips = (currentPrice - entryPrice) × PIP_MULTIPLIERS[symbol]
     (sign inverted for bearish)
4. Return enriched position objects + aggregate summary

PIP_MULTIPLIERS is the same map used by ohlcTradeSimulator.ts (see Phase 1 doc for values per symbol).

Frontend: /markets/portfolio

Two MUI components:

ComponentContent
Summary cardsTotal open positions count, total unrealised P&L (pips) with colour coding (green/red)
Open positions tableOne row per event: symbol, TF, pattern type, direction, entry price, current price, unrealised P&L pips

Data is fetched via useGetPaperPortfolioQuery() with a 30-second polling interval so P&L updates without a manual refresh.


Cross-Cutting Notes

No New Detection Logic

Phase 2 adds zero changes to ohlcPatternDetector.ts, ohlcTradeSimulator.ts, or the signal scoring formula. The detection pipeline from Phase 1 is consumed as-is.

Redux Additions (ohlcSlice)

FieldTypeAdded by
notificationsEnabledbooleanDesktop notifications
drawingModeDrawingModeChart drawing tools
drawingsChartDrawing[]Chart drawing tools

New Env Vars

VarDefaultScope
OHLC_ALERT_EMAIL_THRESHOLD0.7Backend

All other email transport vars (SMTP_HOST, SMTP_PORT, etc.) are pre-existing — see Environment Variables.