UNPKG

@cmike444/supply-and-demand-zones

Version:

A library for identifying supply and demand zones in candlestick data.

619 lines (415 loc) 22 kB
# Supply and Demand Zones A TypeScript library for identifying supply and demand zones in candlestick data, designed for financial and trading applications. ## Table of Contents - [Features](#features) - [Installation](#installation) - [Quick Start](#quick-start) - [Confidence Model](#confidence-model) - [API Reference](#api-reference) - [Zone Detection](#zone-detection) - [Candle Utilities](#candle-utilities) - [Advanced / Internal](#advanced--internal) - [Types](#types) - [Enums](#enums) - [Constants](#constants) - [Developer Tools](#developer-tools) - [Gathering Datasets](#gathering-datasets) - [Visualizing Zones](#visualizing-zones) - [Contributing](#contributing) - [License](#license) ## Features - Detect **supply zones** (Rally-Base-Drop, Drop-Base-Drop patterns) - Detect **demand zones** (Drop-Base-Rally, Rally-Base-Rally patterns) - **Confidence scoring** (0–1) on every zone, derived from departure strength, volume, position, freshness, and timeframe - **Freshness filtering** via `filterFreshZones` — removes stale zones whose price levels conflict with newer zones - Candle utility functions for body size, range, bullish/bearish, decisive, indecisive, and explosive detection - Full TypeScript types, enums, and constants ## Installation ```sh npm install supply-and-demand-zones ``` ## Quick Start ### Importing ```typescript import { identifyZones, filterFreshZones, candleBody, isBullishCandle, ZONE_TYPE, ZONE_DIRECTION, DEFAULT_DECISIVE_THRESHOLD, } from 'supply-and-demand-zones'; ``` ### Identify Supply and Demand Zones ```typescript import { identifyZones } from 'supply-and-demand-zones'; const candles = [ { timestamp: 1, open: 100, high: 110, low: 95, close: 105 }, { timestamp: 2, open: 105, high: 115, low: 100, close: 110 }, { timestamp: 3, open: 110, high: 120, low: 105, close: 115 }, ]; const { supplyZones, demandZones } = identifyZones(candles); console.log('Supply Zones:', supplyZones); console.log('Demand Zones:', demandZones); ``` ### Filter Fresh Zones Remove stale zones whose price levels conflict with newer ones: ```typescript import { identifyZones, filterFreshZones } from 'supply-and-demand-zones'; const { supplyZones, demandZones } = identifyZones(candles); const fresh = filterFreshZones(supplyZones, demandZones); console.log('Fresh Supply Zones:', fresh.supplyZones); console.log('Fresh Demand Zones:', fresh.demandZones); ``` Filter by confidence score: ```typescript const strongZones = fresh.demandZones.filter(z => z.confidence >= 0.7); ``` ### Utility Functions **Candle body size:** ```typescript import { candleBody } from 'supply-and-demand-zones'; const candle = { timestamp: 1, open: 100, high: 110, low: 95, close: 105 }; console.log(candleBody(candle)); // 5 ``` **Bullish / bearish check:** ```typescript import { isBullishCandle } from 'supply-and-demand-zones'; console.log(isBullishCandle(candle)); // true ``` **Decisive check:** ```typescript import { isDecisiveCandle } from 'supply-and-demand-zones'; console.log(isDecisiveCandle(candle)); // true if body/range >= 0.5 ``` **Explosive (Extended Range Candle) check:** ```typescript import { isExplosiveCandle } from 'supply-and-demand-zones'; const candle = { timestamp: 1, open: 100, high: 150, low: 95, close: 145 }; // Basic — body/range ratio >= 0.70 console.log(isExplosiveCandle(candle)); // true // With ATR magnitude check — range must also be >= 1.5 × ATR (e.g. 30) console.log(isExplosiveCandle(candle, 0.70, 30)); // true if range >= 30 ``` **Enums:** ```typescript import { ZONE_DIRECTION, DEFAULT_DECISIVE_THRESHOLD } from 'supply-and-demand-zones'; console.log(ZONE_DIRECTION.SUPPLY); // 0 console.log(ZONE_DIRECTION.DEMAND); // 1 console.log(DEFAULT_DECISIVE_THRESHOLD); // 0.5 ``` ## Confidence Model Every zone returned by `identifyZones` carries a `confidence` score in [0, 1]. ### Departure Sub-score (factors 1–4) Evaluates the strength of the leg that left the zone: | Factor | Description | |---|---| | **countFactor** | Proportion of departure candles that are decisive or explosive in the departure direction. | | **rangeFactor** | Average departure candle range normalised by local ATR, clamped to [0, 1]. Falls back to `0.5` when ATR is zero. | | **volumeFactor** | Departure vs. base volume ratio mapped through `ratio / (ratio + 1)`. Falls back to `0.5` when volume is absent. | | **timeFactor** | "Time at Level": 1–3 base candles → `1.0` (tight imbalance); 4–6 candles → `0.5`; more → `0`. | ``` departureScore = (countFactor + rangeFactor + volumeFactor + timeFactor) / 4 ``` ### Full Zone Score (factors 5–7) Three additional context factors are blended in when scoring through `identifyZones`: | Factor | Description | |---|---| | **positionFactor** | Price position within the chart's full range; higher for supply zones at elevated prices and demand zones at depressed prices. | | **freshnessFactor** | `1.0` if price never re-entered the zone; `0.5` if price touched the proximal line but was repelled before breaching the distal line. | | **timeframeFactor** | Log-normalised median candle interval from 1 minute (`0.0`) to 1 week (`1.0`); higher timeframe zones carry more institutional weight. | ``` confidence = (departureScore × 4 + positionFactor + freshnessFactor + timeframeFactor) / 7 ``` ## API Reference ### Zone Detection --- #### `identifyZones(candles: Candle[]): { supplyZones: SupplyZone[], demandZones: DemandZone[] }` Scans an array of candlestick data and returns all detected supply and demand zones, each with a `confidence` score computed using the full 7-factor model. See [Confidence Model](#confidence-model) for details. | Parameter | Type | Description | |---|---|---| | `candles` | `Candle[]` | Array of candlestick data to scan. | **Returns:** `{ supplyZones: SupplyZone[], demandZones: DemandZone[] }` --- #### `filterFreshZones(supplyZones: SupplyZone[], demandZones: DemandZone[]): { supplyZones: SupplyZone[], demandZones: DemandZone[] }` Filters out stale zones whose price levels conflict with newer zones. A demand zone's proximal line (upper edge) must never be above a supply zone's proximal line (lower edge). When a conflict is found, the zone with the older `endTimestamp` is removed. On a tie, the demand zone is removed. | Parameter | Type | Description | |---|---|---| | `supplyZones` | `SupplyZone[]` | Supply zones to filter. | | `demandZones` | `DemandZone[]` | Demand zones to filter. | **Returns:** `{ supplyZones: SupplyZone[], demandZones: DemandZone[] }` — only non-conflicting zones. --- #### `rallyBaseDrop(candles: Candle[], localATR?: number): SupplyZone | null` Detects a Rally-Base-Drop supply zone pattern. | Parameter | Type | Default | Description | |---|---|---|---| | `candles` | `Candle[]` | — | Array starting at the first candle of the potential pattern. | | `localATR` | `number` | `0` | ATR for base validation. | **Returns:** A `SupplyZone` if the pattern is detected, `null` otherwise. --- #### `dropBaseDrop(candles: Candle[], localATR?: number): SupplyZone | null` Detects a Drop-Base-Drop supply zone pattern. | Parameter | Type | Default | Description | |---|---|---|---| | `candles` | `Candle[]` | — | Array starting at the first candle of the potential pattern. | | `localATR` | `number` | `0` | ATR for base validation. | **Returns:** A `SupplyZone` if the pattern is detected, `null` otherwise. --- #### `dropBaseRally(candles: Candle[], localATR?: number): DemandZone | null` Detects a Drop-Base-Rally demand zone pattern. | Parameter | Type | Default | Description | |---|---|---|---| | `candles` | `Candle[]` | — | Array starting at the first candle of the potential pattern. | | `localATR` | `number` | `0` | ATR for base validation. | **Returns:** A `DemandZone` if the pattern is detected, `null` otherwise. --- #### `rallyBaseRally(candles: Candle[], localATR?: number): DemandZone | null` Detects a Rally-Base-Rally demand zone pattern. | Parameter | Type | Default | Description | |---|---|---|---| | `candles` | `Candle[]` | — | Array starting at the first candle of the potential pattern. | | `localATR` | `number` | `0` | ATR for base validation. | **Returns:** A `DemandZone` if the pattern is detected, `null` otherwise. --- ### Candle Utilities --- #### `candleBody(candle: Candle): number` | Parameter | Type | Description | |---|---|---| | `candle` | `Candle` | The candlestick to measure. | **Returns:** `|close - open|` --- #### `candleRange(candle: Candle): number` | Parameter | Type | Description | |---|---|---| | `candle` | `Candle` | The candlestick to measure. | **Returns:** `high - low` --- #### `isBullishCandle(candle: Candle): boolean` **Returns:** `true` if `close > open`. --- #### `isBearishCandle(candle: Candle): boolean` **Returns:** `true` if `close < open`. --- #### `isDecisiveCandle(candle: Candle, threshold?: number): boolean` Determines if a candle has a large body relative to its range. | Parameter | Type | Default | Description | |---|---|---|---| | `candle` | `Candle` | — | The candlestick to evaluate. | | `threshold` | `number` | `0.5` | Minimum body-to-range ratio. | **Returns:** `true` if body-to-range ratio ≥ `threshold`. --- #### `isIndecisiveCandle(candle: Candle, threshold?: number): boolean` Determines if a candle has a small body relative to its range. | Parameter | Type | Default | Description | |---|---|---|---| | `candle` | `Candle` | — | The candlestick to evaluate. | | `threshold` | `number` | `0.5` | Maximum body-to-range ratio. | **Returns:** `true` if body-to-range ratio is strictly below `threshold`. --- #### `isExplosiveCandle(candle: Candle, threshold?: number, minATR?: number): boolean` Determines if a candle is an Extended Range Candle (ERC) — the "leg-out" that signals an institutional imbalance. | Parameter | Type | Default | Description | |---|---|---|---| | `candle` | `Candle` | — | The candlestick to evaluate. | | `threshold` | `number` | `0.70` | Minimum body-to-range ratio. | | `minATR` | `number` | `0` | Minimum required total range. When > 0, candles whose `high − low` falls below this value are rejected even if their body ratio qualifies. Typically passed as `MIN_EXPLOSIVE_ATR_MULTIPLIER × localATR` (1.5× ATR). | **Returns:** `true` if body-to-range ratio ≥ `threshold` **and** `high − low``minATR`. --- #### `isBullishDecisiveCandle(candle: Candle, threshold?: number): boolean` **Returns:** `true` if the candle is bullish and body-to-range ratio ≥ `threshold` (default `0.5`). --- #### `isBearishDecisiveCandle(candle: Candle, threshold?: number): boolean` **Returns:** `true` if the candle is bearish and body-to-range ratio ≥ `threshold` (default `0.5`). --- #### `isBullishExplosiveCandle(candle: Candle, threshold?: number): boolean` **Returns:** `true` if the candle is bullish and body-to-range ratio ≥ `threshold` (default `0.70`). --- #### `isBearishExplosiveCandle(candle: Candle, threshold?: number): boolean` **Returns:** `true` if the candle is bearish and body-to-range ratio ≥ `threshold` (default `0.70`). --- #### `atr(candles: Candle[], period?: number): number` Computes the Average True Range using a simple mean over the last `period` true-range values. True Range is `max(high − low, |high − prevClose|, |low − prevClose|)`. For the first candle (no previous close), TR = `high − low`. Returns `0` for an empty array. | Parameter | Type | Default | Description | |---|---|---|---| | `candles` | `Candle[]` | — | Array of candles representing the context window. | | `period` | `number` | `14` | Look-back period. | **Returns:** Mean TR over the last `period` values, or `0` if the array is empty. --- ### Advanced / Internal These functions are exported for advanced use cases. They are called internally by the pattern detectors. --- #### `calculateConfidence(departureCandles: Candle[], baseCandles: Candle[], localATR: number, isUpwardDeparture: boolean): number` Calculates the **departure sub-score** (factors 1–4 of the full confidence model). When called through `identifyZones`, three additional context factors are blended in automatically. See [Confidence Model](#confidence-model). | Parameter | Type | Description | |---|---|---| | `departureCandles` | `Candle[]` | Candles forming the explosive leg away from the zone. | | `baseCandles` | `Candle[]` | Candles forming the indecisive base of the zone. | | `localATR` | `number` | ATR for the context window around the zone. | | `isUpwardDeparture` | `boolean` | `true` for a bullish departure (demand zone), `false` for bearish (supply zone). | **Returns:** Departure sub-score in [0, 1]. --- #### `isValidBase(baseCandles: Candle[], precedingCandle: Candle, localATR: number): boolean` Validates that a base sequence is well-formed relative to local volatility. Applies two checks: 1. **Gap check** — rejects if any consecutive open-to-prior-close gap within `[precedingCandle, ...baseCandles]` exceeds `MAX_BASE_GAP_ATR_MULTIPLIER × localATR`. 2. **Height check** — rejects if the base's full high-to-low range exceeds `MAX_ZONE_ATR_MULTIPLIER × localATR`. Returns `true` unconditionally when `localATR ≤ 0`. | Parameter | Type | Description | |---|---|---| | `baseCandles` | `Candle[]` | The indecisive candles forming the base. | | `precedingCandle` | `Candle` | The candle immediately before the base. | | `localATR` | `number` | ATR for the context window around the base. | **Returns:** `true` if the base passes both checks, `false` otherwise. --- #### `findPatternEnd(candles: Candle[], startIndex: number, condition: (candle: Candle) => boolean, maxCount?: number): number` Advances through `candles` from `startIndex` while `condition` holds, returning the index of the first candle that does not satisfy the condition (or the end of the array). | Parameter | Type | Default | Description | |---|---|---|---| | `candles` | `Candle[]` | — | Array of candles. | | `startIndex` | `number` | — | Index to start scanning from. | | `condition` | `(c: Candle) => boolean` | — | Predicate applied to each candle. | | `maxCount` | `number` | — | Maximum number of candles to advance. | **Returns:** Index of the first non-matching candle, or `candles.length` if all checked candles match. --- ## Types ### `Candle` Represents a single candlestick. | Property | Type | Required | Description | |---|---|---|---| | `timestamp` | `number` | Yes | Time of the candlestick. | | `open` | `number` | Yes | Opening price. | | `high` | `number` | Yes | Highest price. | | `low` | `number` | Yes | Lowest price. | | `close` | `number` | Yes | Closing price. | | `volume` | `number` | No | Trading volume. Used in confidence scoring when present. | ### `Zone` Base interface for all zones. | Property | Type | Description | |---|---|---| | `proximalLine` | `number` | Near edge of the zone (closest to current price). Derived from candle **bodies** only — for supply zones: the lowest body (open/close) across the base; for demand zones: the highest body (open/close) across the base. | | `distalLine` | `number` | Far edge of the zone. Spans the **entire formation** (leg-in + base + leg-out) using full wicks — for supply zones: the highest `high`; for demand zones: the lowest `low`. | | `startTimestamp` | `number` | Timestamp of the first candle in the pattern. | | `endTimestamp` | `number` | Timestamp of the last candle in the pattern. | | `confidence` | `number` | Score in [0, 1] indicating zone strength. Blends 7 equally-weighted factors: departure quality (×3), position, freshness, timeframe, and `rrScore`. | | `rrScore` | `number` | Departure-based risk/reward score in [0, 1]. `min(departureExtent / zoneWidth / 5, 1)` — a score of 1.0 represents ≥ 5:1 R:R. Always set by `identifyZones`. | | `entryPrice` | `number` | Suggested limit order entry price. Equal to `proximalLine`. Always set by `identifyZones`. | | `stopPrice` | `number` | Hard stop price — zone invalidation level. Equal to `distalLine`. Always set by `identifyZones`. | | `targetPrice` | `number \| null` | Profit target — proximal line of the nearest opposing zone. `null` when no opposing zone exists in the dataset. Always set by `identifyZones`. | ### `SupplyZone` Extends `Zone`. Represents a supply zone. | Property | Type | Description | |---|---|---| | `direction` | `ZONE_DIRECTION` | Always `ZONE_DIRECTION.SUPPLY`. | | `type` | `ZONE_TYPE` | `ZONE_TYPE.RALLY_BASE_DROP` or `ZONE_TYPE.DROP_BASE_DROP`. | ### `DemandZone` Extends `Zone`. Represents a demand zone. | Property | Type | Description | |---|---|---| | `direction` | `ZONE_DIRECTION` | Always `ZONE_DIRECTION.DEMAND`. | | `type` | `ZONE_TYPE` | `ZONE_TYPE.DROP_BASE_RALLY` or `ZONE_TYPE.RALLY_BASE_RALLY`. | ## Enums ### `ZONE_DIRECTION` | Value | Name | Description | |---|---|---| | `0` | `SUPPLY` | A supply zone — price is expected to fall from here. | | `1` | `DEMAND` | A demand zone — price is expected to rise from here. | ### `ZONE_TYPE` | Value | Name | |---|---| | `0` | `DROP_BASE_DROP` | | `1` | `RALLY_BASE_RALLY` | | `2` | `DROP_BASE_RALLY` | | `3` | `RALLY_BASE_DROP` | ## Constants | Constant | Value | Description | |---|---|---| | `DEFAULT_DECISIVE_THRESHOLD` | `0.5` | Minimum body-to-range ratio for a decisive candle. | | `DEFAULT_EXPLOSIVE_THRESHOLD` | `0.7` | Minimum body-to-range ratio for an explosive candle (ERC). | | `DEFAULT_ATR_PERIOD` | `14` | Look-back period for ATR calculation (Wilder's standard). | | `MIN_RALLY_CANDLES` | `2` | Minimum consecutive bullish candles required to define a rally leg. | | `MIN_DROP_CANDLES` | `2` | Minimum consecutive bearish candles required to define a drop leg. | | `MIN_BASE_CANDLES` | `1` | Minimum indecisive candles required for a valid base. | | `MAX_BASE_CANDLES` | `6` | Maximum indecisive candles allowed in a base. More than 6 indicates the imbalance is too dispersed. | | `MIN_EXPLOSIVE_ATR_MULTIPLIER` | `1.5` | Minimum ERC total range as a multiple of ATR. The leg-out must be significantly larger than surrounding price action. | | `MAX_ZONE_ATR_MULTIPLIER` | `1.5` | Maximum base high-to-low range as a multiple of ATR. | | `MAX_BASE_GAP_ATR_MULTIPLIER` | `0.5` | Maximum open-to-prior-close gap within a base as a multiple of ATR. | ## Developer Tools ### Gathering Datasets The `scripts/fetch_candles.ts` script downloads OHLCV candlestick data for any ticker via [yahoo-finance2](https://www.npmjs.com/package/yahoo-finance2) and saves it as a JSON file in the `data/` folder. The filename is automatically generated from the symbol, interval, and actual date range of the data. #### Prerequisites ```sh npm install ``` #### Usage ```sh npm run fetch-candles <SYMBOL> [INTERVAL] [PERIOD] ``` | Argument | Required | Default | Description | |---|---|---|---| | `SYMBOL` | Yes | — | Ticker symbol (e.g. `SPY`, `AAPL`, `IBIT`) | | `INTERVAL` | No | `1m` | Bar size: `1m`, `2m`, `5m`, `15m`, `30m`, `60m`, `1d` | | `PERIOD` | No | `7d` | Lookback window: `1d`, `5d`, `7d`, `1mo`, `3mo`, `6mo`, `1y` | #### Examples ```sh # 1-minute SPY data for the last 7 days (default) npm run fetch-candles SPY # 5-minute AAPL data for the last 60 days npm run fetch-candles AAPL 5m 3mo # Daily IBIT data for the last year npm run fetch-candles IBIT 1d 1y ``` #### Output Files are saved to `data/` with the naming convention `<SYMBOL>_<INTERVAL>_<STARTDATE>_<ENDDATE>.json`: ``` data/SPY_1m_20260319_20260327.json data/AAPL_5m_20251231_20260327.json data/IBIT_1d_20250329_20260327.json ``` Each candle object contains: ```json { "timestamp": 1773927000000, "open": 656.97, "high": 657.00, "low": 655.17, "close": 655.85, "volume": 5070007 } ``` #### Interval Limits | Interval | Max Lookback | |---|---| | `1m` | 7 days | | `2m`, `5m`, `15m`, `30m` | 60 days | | `60m` | 730 days | | `1d` and above | Unlimited | ### Visualizing Zones Two scripts generate self-contained HTML files that can be opened in any browser. #### `npm run visualize-zones` Renders identified zones on an interactive candlestick chart powered by [Apache ECharts 5](https://echarts.apache.org/). ```sh npm run visualize-zones -- <path/to/data.json> [output.html] ``` If no output path is given, the file is auto-named `<input-basename>_zones.html` and written to the `example/` directory (gitignored). **Chart features:** - Dark-theme ECharts 5 candlestick chart - Gap-free x-axis — timestamps are mapped to sequential indices, eliminating overnight and weekend gaps - Optional volume panel rendered below the price chart when candle data contains a `volume` field - Scroll, pan, and zoom via mouse wheel or the slider below the chart; defaults to the last ~500 candles - Only **intact** zones are displayed: - Stale zones (price-level conflicts between supply and demand) are removed via `filterFreshZones` - Breached zones (price fully wicked through the distal line after the zone formed) are additionally removed - **Zone fill opacity** scales with `confidence` — faint at 0, solid at 1 - **Proximal and distal rays** extend from the zone's end timestamp to the right edge of the chart - Each proximal ray is labeled with the price level, a **letter grade** (A+ through F), and the numeric score - The subtitle bar shows total zones identified, the fresh count, and how many stale/breached zones were removed #### `npm run visualize-tests` Generates `example/testCases.html` — an interactive chart that plots the candle fixtures used in the test suite alongside their detected zones. Useful for visually verifying pattern detection logic. ```sh npm run visualize-tests ``` ## Contributing Contributions are welcome! If you have ideas for improvements or new features, feel free to open an issue or submit a pull request. ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.