Markets / OHLC Feature Enhancements

This document covers the architectural additions introduced in the feat/markets-enhancements branch: three new harmonic pattern types, a composite signal score, multi-timeframe confirmation, backtest improvements, technical indicator overlays, live pattern alerts, and two new crypto symbols.

Pattern Detection Architecture Recap

The detection pipeline runs whenever new candles arrive and on admin-triggered redetection:

OhlcCandle[] (DB)
      │
      ▼
findSwingPoints()          ← pivot-lookback = 5 candles
      │
      ▼
detectPatterns(swings)     ← check each 5-swing window against all pattern checkers
      │
      ▼
ohlcPatternService         ← enriches with prevSwingPrice + signalScore, persists to DB
      │
      ▼
broadcast(ohlc:pattern:detected)  ← WebSocket push to all connected clients

The core detection code lives in backend/services/ohlcPatternDetector.ts; enrichment and persistence live in backend/services/ohlcPatternService.ts.


New Pattern Types

Three pattern types were added to the harmonic_pattern_type Postgres enum and to the HarmonicPatternType TypeScript union:

PatternKey Fibonacci RatiosNotes
cypherAB: 0.382–0.618 of XA; BC ext: 1.272–1.414; XD: 0.786 of XCUses XC range (not XA) for the D-point retracement — distinguishing feature vs Gartley/Bat
three_drivesEach retracement ≈ 0.618 of preceding drive; drive legs ≈ equal (±10%)5-point window maps to 3 drives + 2 retracements; retracement tolerance = ±0.08
abcdBC ≈ 0.618 retracement of AB (±0.08); CD ≈ AB in length (±10%)X point is used only for direction validation; AB/CD equality is the primary signal

Global tolerance for all “near” ratio checks is 5% (FIB_TOLERANCE = 0.05). Confidence is 1 − mean(ratio errors), clamped to [0, 1].

DB Schema

// backend/db/timescale/schemas/ohlcPatterns.ts
export const harmonicPatternTypeEnum = pgEnum('harmonic_pattern_type', [
  'gartley',
  'bat',
  'butterfly',
  'crab',
  'shark',
  'head_and_shoulders',
  'cypher', // ← new
  'three_drives', // ← new
  'abcd', // ← new
]);

Migration: backend/db/timescale/migrations/0005_common_katie_power.sql

SL/TP Levels (ohlcTradeSimulator.ts)

PatternSLTP1TP2TP3
cypherBeyond X0.382 of XC0.618 of XC0.786 of XC
three_drivesBeyond X1st drive high2nd drive highD-point
abcdBeyond C extendedB (start of CD)A (start of AB)— (mirrors TP1)

Composite Signal Score

Every detected pattern is scored [0, 1] and stored as signal_score on ohlc_patterns.

Formula

signalScore = harmonicConfidence × 0.4
            + rsiDivergence       × 0.3
            + srConfluence        × 0.3
ComponentWeightHow it is computed
Harmonic confidence40%Raw confidence from the pattern checker (ratio accuracy)
RSI divergence30%RSI-14 at the D-point candle: bullish if RSI < 30, bearish if > 70. Score = (30 − rsi) / 30 (bullish) or (rsi − 70) / 30 (bearish). Zero if RSI is neutral.
S/R confluence30%1 − dist / 0.002 when D-price is within 0.2% of prevSwingPrice; otherwise 0

prevSwingPrice is the prior S/R wick level found by scanning backward from X up to 120 candles (PREV_SR_LOOKBACK) for the nearest contiguous wick cluster that breaks X’s price level.

Schema Column

signalScore: numeric('signal_score', { precision: 5, scale: 4 }),

The value is stored as a 4-decimal numeric string (e.g. "0.7340"). The service writes it with .toFixed(4).


Multi-Timeframe Confirmation

Endpoint

GET /api/ohlc/patterns/multi-tf?symbol=EUR%2FUSD

No auth required (same as all other read endpoints).

Logic (ohlcPatternService.getMultiTimeframeConfirmed)

  1. Fetches the 50 most recent patterns for each of the 5 tracked timeframes (5m, 15m, 1h, 4h, 1d) in parallel.
  2. Builds a Map<"patternType:direction", Set<Timeframe>>.
  3. Returns only entries where the set size is ≥ 2.
export interface MultiTfConfirmedPattern {
  patternType: string;
  direction: string;
  timeframes: Timeframe[];
}

Frontend Component

MultiTfConfirmationBadge is rendered in MarketsChart above the chart. It calls the RTK Query useGetMultiTfPatternsQuery(symbol) hook and displays a chip per confirmed combination. The badge is always visible for the current symbol regardless of the selected timeframe.


Backtest Enhancements

Date Range Filtering

The POST /api/ohlc/backtest request body accepts optional ISO 8601 date strings:

// backend/features/validation/schemas/ohlc.ts
from: z.string().datetime({ offset: true }).optional(),
to:   z.string().datetime({ offset: true }).optional(),

When omitted the service defaults to the last 7 days. The controller converts the strings via dayjs(from).toDate() before passing them to the service.

Partial-Close Simulation

The backtest now runs two parallel simulations per pattern:

SimulationBehaviourStored as
StandardSingle exit at SL, TP1, TP2, or TP3 (first hit wins)pnlPips
Partial-close1/3 at TP1 (then SL moves to breakeven), 1/3 at TP2, 1/3 at TP3partialClosePnlPips

The partial-close simulator (simulatePartialClose) tracks a currentSl that is reassigned to dPrice (breakeven) as soon as TP1 is reached. If SL fires after TP1, only the remaining open third is closed at the updated stop.

Max Drawdown

Computed server-side from the PnL series of all completed trades:

// Peak-to-trough drawdown on cumulative PnL curve
maxDrawdownPips: number; // absolute pips
maxDrawdownPct: number; // fraction of peak equity (4 decimal places)

A client-side fallback with identical logic lives in frontend/features/ohlc/logic/calculateMaxDrawdown.ts for scenarios where the server result is not yet available.

Equity Curve Chart

EquityCurveChart.tsx receives the backtest trades array and renders a lightweight-charts LineSeries of cumulative PnL over time. It is rendered inside BacktestPanel below the summary stats table.


Technical Indicators

All indicators are computed client-side from the same candle array already loaded for the chart.

IndicatorLocationChart pane
EMA 20calculateEma(candles, 20)Pane 0 (main)
EMA 50calculateEma(candles, 50)Pane 0 (main)
EMA 200calculateEma(candles, 200)Pane 0 (main)
Bollinger BandscalculateBollingerBands(candles)Pane 0 (main) — 3 dashed LineSeries
Volume histogramHistogramSeries of candle.volumePane 3 (new)

Pane layout:

Pane 0  main candles + EMA + Bollinger Bands + harmonic overlays
Pane 1  RSI (14)
Pane 2  MACD
Pane 3  Volume histogram (height = 80px, collapsible)

IndicatorControls

IndicatorControls.tsx renders MUI Chip toggles for each indicator group. Toggle state is stored in Redux (ohlcSlice.indicators) and the MarketsChart component reads it to show/hide series and resize pane heights via chart.panes()[n]?.setHeight(h).

EMA Implementation

Standard exponential moving average seeded with the SMA of the first period candles:

const multiplier = 2 / (period + 1);
// seed
let ema = sum(candles[(0).period - 1].close) / period;
// iterate
ema = (close - ema) * multiplier + ema;

Bollinger Bands Implementation

20-period SMA ± 2 standard deviations (population σ, not sample):

const sma = window.reduce((s, c) => s + c.close, 0) / period;
const variance = window.reduce((s, c) => s + (c.close - sma) ** 2, 0) / period;
const stdDev = Math.sqrt(variance);
upper = sma + 2 * stdDev;
middle = sma;
lower = sma - 2 * stdDev;

Live Pattern Alerts

Hook: useOhlcAlerts

// frontend/features/ohlc/hooks/useOhlcAlerts.ts
export const useOhlcAlerts = (symbol: string, timeframe: string) => {
  const dispatch = useDispatch<AppDispatch>();
  useWebSocket('ohlc:pattern:detected', (payload: unknown) => {
    const event = payload as OhlcPatternDetectionEvent;
    if (event.symbol === symbol && event.timeframe === timeframe) {
      dispatch(addLiveAlert(event));
    }
  });
};

The hook is called unconditionally in MarketsChart. It filters events by the currently selected symbol and timeframe so alerts from other instruments do not surface.

Redux Integration

Alerts accumulate in ohlcSlice.liveAlerts (addLiveAlert reducer). MarketsChart reads selectOhlcLiveAlerts and renders up to 3 simultaneous MUI Snackbar dismissible notifications.

WebSocket Event

type: 'ohlc:pattern:detected'
payload: OhlcPatternDetectionEvent  (same shape returned by GET /api/ohlc/detection-events)

The event is broadcast by ohlcPatternService immediately after a new pattern is persisted and a detection event is created.


New Symbols

Two crypto symbols were added to TRACKED_SYMBOLS and YAHOO_FINANCE_SYMBOLS:

SymbolData sourcePip multiplierChart price format
BTC/USDYahoo Finance (BTC-USD)10 decimal places, minMove = 1
ETH/USDYahoo Finance (ETH-USD)102 decimal places, minMove = 0.01

For reference, existing symbols:

SymbolData sourcePip multiplierChart price format
EUR/USDTwelve Data10 0005 decimal places
XAU/USDYahoo Finance1002 decimal places
WTI/USDYahoo Finance1002 decimal places

Pip multiplier (getPipMultiplier in ohlcTradeSimulator.ts) converts raw price differences to pip-equivalent units for PnL reporting. BTC uses a multiplier of 1 because a $1 price move represents one “unit” of significance at that price scale.

Price format (getPriceFormat in MarketsChart.tsx) is passed to lightweight-charts’ series priceFormat option to control axis label and tooltip precision.

Both new symbols follow the Yahoo Finance fetcher path (yahooFinanceFetcher.ts) using ticker strings BTC-USD and ETH-USD.