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 var | Default | Description |
|---|---|---|
OHLC_ALERT_EMAIL_THRESHOLD | 0.7 | Minimum 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:
| Metric | Grouping |
|---|---|
| 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 performers | Symbol + 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):
| Column | Type | Notes |
|---|---|---|
id | UUID PK | gen_random_uuid() |
user_id | UUID FK | References users.id (cascade delete) |
symbol | text | e.g. EUR/USD |
timeframe | text | Optional; null = match any TF |
pattern_type | text | Optional; null = match any pattern |
min_signal_score | numeric | Optional; null = no score filter |
created_at | timestamptz | now() 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):
| Method | Path | Description |
|---|---|---|
GET | /api/ohlc/watchlist | Returns the authenticated user’s watchlist entries |
POST | /api/ohlc/watchlist | Adds a new entry (body: symbol, optional timeframe, patternType, minSignalScore) |
DELETE | /api/ohlc/watchlist/:id | Removes 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:
| Component | Content |
|---|---|
| Summary cards | Total open positions count, total unrealised P&L (pips) with colour coding (green/red) |
| Open positions table | One 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)
| Field | Type | Added by |
|---|---|---|
notificationsEnabled | boolean | Desktop notifications |
drawingMode | DrawingMode | Chart drawing tools |
drawings | ChartDrawing[] | Chart drawing tools |
New Env Vars
| Var | Default | Scope |
|---|---|---|
OHLC_ALERT_EMAIL_THRESHOLD | 0.7 | Backend |
All other email transport vars (SMTP_HOST, SMTP_PORT, etc.) are pre-existing — see Environment Variables.
Related Docs
- OHLC Phase 1 — Feature Enhancements — pattern detection, signal score, backtest, indicators, live alerts
- Database Schema —
ohlc_pattern_detection_events,ohlc_watchlisttable and migration workflow - Environment Variables — full env var reference including SMTP transport config