fintech-algorithms
Using a coding agent? Give it the skill: npx skills add IslamBaraka90/Fintech-Algorithms-Library What it does →

Scale-Aware Body Classification

Install and import#

bash
npm install fintech-algorithms
ts
import { classifyBody } from "fintech-algorithms/price-action-and-candlesticks/candle-foundations/scale-aware-body-classification";

Signature#

classifyBody(observation, priorClosedBodies, configInput)

Classifies a candle body as long, short or doji **relative to recent bodies** rather than against a fixed threshold. A 50-point body is enormous on one instrument and unremarkable on another, so an absolute rule cannot transfer.

Parameters#

NameTypeNotes
observationObservationThe candle's body and range, with its price basis and session.
priorClosedBodiesnumber[]Recent closed bodies forming the comparison distribution. Closed only — including the forming candle would let the present redefine its own context.
configInput{ window: number; min_periods: number; tick_size: number; thresholds: object }min_periods refuses to classify until enough history exists, rather than judging against two candles. tick_size floors the comparison so sub-tick noise is not treated as structure.

Returns#

{ classification, percentile, window, min_periods, … }

The classification with the distribution statistics behind it, so the same body can be seen to be long in one regime and short in another.

Errors#

  • When fewer prior bodies are supplied than min_periods — reported as an unclassified state rather than thrown

Complexity: time O(window), space O(window).

Worked example#

verified This is the worked example published in the article, replayed by the test suite on every run. The output cannot drift.

Input#

observation
{
  "timestamp": "2026-01-05T10:25:00Z",
  "instrument_id": "SYNTH:ABC",
  "interval": "5m",
  "body": 4,
  "range": 6,
  "is_closed": true,
  "price_basis": "raw",
  "session": "continuous"
}
priorClosedBodies
[1, 2, 2, 3, 12]
configInput
{
  "window": 5,
  "min_periods": 5,
  "tick_size": 0.01,
  "thresholds": {
    "tiny_max": 0.25,
    "small_max": 0.75,
    "long_min": 1.5
  }
}

Call#

classifyBody(observation, priorClosedBodies, configInput)

Returns#

object with 9 fields: status, body_class, body_score, raw_scale, effective_scale, scale_floor_applied, history_count, body_to_current_range, …

{
  "status": "ready",
  "body_class": "long",
  "body_score": 2,
  "raw_scale": 2,
  "effective_scale": 2,
  "scale_floor_applied": false,
  "history_count": 5,
  "body_to_current_range": 0.6666666666666666,
  "is_provisional": false
}

Other exports#

This module also exports classifyBodySeries. Every module additionally exports run as an alias of its primary function, and a meta object carrying its catalog id, domain, family, shape and article URL.

Diagrams#

Scale-Aware Body Classification — body scale classification
Scale-Aware Body Classification — mean vs median

Calculation flow#

Causal window lifecycle
sequenceDiagram
    participant Feed as Ordered candle feed
    participant A01 as A01 anatomy
    participant A02 as A02 classifier
    participant H as Closed-body history

    Feed->>A01: Closed candle t
    A01->>A02: body and range
    A02->>H: Read prior closed bodies only
    H-->>A02: causal reference window
    A02-->>Feed: classification for t
    A02->>H: Append body t after classification

    Feed->>A01: Open candle t+1 update
    A01->>A02: provisional body and range
    A02->>H: Read unchanged closed history
    H-->>A02: same causal reference window
    A02-->>Feed: provisional classification
    Note over A02,H: Open body is not appended
Classification flow
flowchart TD
    A["Receive validated A01 body and range"] --> B["Select last W prior closed bodies"]
    B --> C{"History count at least M?"}
    C -->|"No"| D["Return warmup: scale, score, class = null"]
    C -->|"Yes"| E["Compute median raw scale S"]
    E --> F["Apply optional tick floor: E = max(S, tick)"]
    F --> G{"Effective scale E greater than 0?"}
    G -->|"No"| H["Return zero_scale: score, class = null"]
    G -->|"Yes"| I["Compute q = current body / E"]
    I --> J{"Threshold interval"}
    J -->|"q <= 0.25"| K["tiny"]
    J -->|"0.25 < q <= 0.75"| L["small"]
    J -->|"0.75 < q < 1.50"| M["normal"]
    J -->|"q >= 1.50"| N["long"]

How it works#

This page states the contract — how to call it correctly. The article explains the concept: why it works, and where it breaks.

Read the article →

References#

The rest of the Candle Foundations family#