Skip to content

About

Non-repainting swing structure and BOS/CHoCH detection engine for OHLCV candles

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

Repository files navigation

market-structure-engine

Non-repainting swing-structure detection for OHLCV candles: Williams-style fractal swings labelled HH / HL / LH / LL, a bullish/bearish trend state machine, and confirmed BOS (Break of Structure) / CHoCH (Change of Character) events. Zero runtime dependencies, batch and streaming APIs with provably identical output. Part of the Visor chart intelligence stack — its overlay adapter emits plain data shapes ready for lightweight-charts style rendering.

Method

Swing detection (fractals)

A swing high at bar i is a bar whose high is strictly greater than the highs of the k bars on each side (default k = 2); mirror rule for swing lows. Strict inequality means exact-tie plateaus do not form swings — detection stays deterministic and side-agnostic.

Confirmation lag — the honest part. The window around bar i is only complete when bar i + k closes, so a swing is emitted exactly k bars after its pivot and carries both indices:

SwingPoint { index, time, price, kind, confirmedAtIndex, label }
                                       ^ always index + k

Once emitted, a swing never changes. The test suite proves the prefix property: running the engine on the first n candles produces exactly the outputs of the full run whose confirmation bar is < n — nothing mutates or disappears when future bars are appended.

Swing labels

Each confirmed swing is compared with the previous confirmed swing of the same kind:

kind condition label
high price > previous high HH
high price ≤ previous high LH
low price < previous low LL
low price ≥ previous low HL

The first swing of each kind is unlabelled (label: null). Ties resolve to the "weaker" label (LH / HL).

Trend state machine and BOS / CHoCH

The engine tracks the most recent unbroken confirmed swing high and swing low as reference levels, plus a trend state ∈ {bullish, bearish, undetermined}. On every bar close (confirmed-close rule — intrabar wicks do not trigger by default):

  • close above the reference swing high →
    • state was bearish → CHoCH (bullish), state flips to bullish
    • otherwise → BOS (bullish), state becomes/stays bullish
  • close below the reference swing low → mirror rules.

A reference level can be broken at most once; the next event in that direction needs a newer confirmed swing. The first break out of undetermined is reported as a BOS that establishes the trend. With wickMode: true, the bar's high/low replaces the close as the trigger price (documented in the event's price field either way).

Event lag: zero bars past the triggering close — but note the level itself appears only k bars after its pivot, so a break that happens inside a swing's own confirmation window cannot be detected against that swing.

Resampling

resample(candles, intervalMs) / Resampler aggregate a lower timeframe into a higher one (e.g. 15m → 1h) with buckets aligned to floor(time / interval). A bucket is emitted only when the next bucket starts, so emitted candles are final — the same non-repainting discipline. The test suite cross-checks resampled 15m BTCUSDT candles against Binance's own 1h klines, exactly.

Install

npm install market-structure-engine

Node 20+. Zero runtime dependencies. Ships ESM + CJS + type declarations.

Quickstart

import { StructureEngine } from 'market-structure-engine';

const engine = new StructureEngine({ fractalK: 2 });

// batch over history…
const { swings, events, state } = engine.process(candles);

// …then stream live closed bars; update returns only what the bar added
const step = engine.update(nextClosedCandle);
for (const event of step.events) {
  console.log(event.type, event.direction, 'broke', event.brokenSwing.price);
}

Batch and incremental modes produce identical output for the same candles (tested).

API

new StructureEngine(options?)

option type default meaning
fractalK number 2 fractal half-width; confirmation lag in bars
wickMode boolean false trigger breaks on wick extremes instead of close
  • process(candles): StructureResult — reset + run over the whole array.
  • update(candle): UpdateResult — feed one closed candle; returns the swings confirmed and the events triggered by that bar, plus the current state.
  • getters: swings, events, state, candlesProcessed; reset().

FractalTracker

The standalone incremental fractal detector (update(candle): SwingPoint[]), if you only need pivots.

resample(candles, intervalMs, { includePartial? }) / new Resampler(intervalMs)

Batch and incremental timeframe aggregation. The trailing partial bucket is dropped by default; Resampler.update returns a bucket only once it is complete, and flush() hands back the in-progress partial.

Overlay adapter (toSwingMarkers, toEventMarkers, toBrokenLevelSegments)

Pure data mappers for chart overlays — no dependency on any charting package:

  • toSwingMarkers(swings) — one marker per swing at the pivot bar, labelled HH/HL/LH/LL, with a styleKey for theming.
  • toEventMarkers(events) — BOS/CHoCH arrows at the triggering bar.
  • toBrokenLevelSegments(events) — a horizontal segment per broken level, from pivot bar to break bar at the swing price.

Demo

npm install
npm run demo   # prints swings/events tables from 1000 real BTCUSDT 1h candles

Tests

npm test

Covers synthetic patterns (uptrend BOS chain, CHoCH reversal, range), wick-vs-close trigger rules, the non-repaint prefix property on real and synthetic data, batch ≡ incremental equivalence, resampling cross-checked against real Binance 1h klines, and a real-fixture snapshot.

Screenshots

Chart overlays coming soon.


Educational and analytical tooling. Not investment advice, not a trading signal service, and not a financial promotion.

About

Non-repainting swing structure and BOS/CHoCH detection engine for OHLCV candles

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages