Library/Price Action and Candlesticks/Multi-Candle Patterns/Three Inside Up/Down

D06-F04-A05 / Complete engineering topic

Three Inside Up/Down

A production-minded guide to Three Inside Up/Down.

Three Inside Up/DownD06 / D06-F04

Extend a strict harami pair with a third-candle directional confirmation.

The decision this tutorial makes visible

Visual pattern names are ambiguous until body size, shadow size, gaps, containment, confirmation, context, and equality are converted into auditable comparisons.

The precise question is: Do three finalized OHLC candles satisfy the declared Three Inside Up/Down geometry and its required prior-trend context?

A practitioner needs to know what the diagnostic does and does not justify. A builder needs a contract that can be reproduced from the same point-in-time inputs in Python, TypeScript, a visual, and a browser lab.

Intuition before notation

Treat the pattern as a short state machine: validate scale and context, test each candle relationship in order, preserve every failed check, then emit geometry and context separately.

The result depends on the declared algorithm scope, input clocks, units, equality and rounding policies, and unsupported-state treatment. Change one of those and the output represents a different decision even when its field name is unchanged.

Scope and nearby methods

The canonical detector evaluates exactly three finalized candles using contained second body then confirmation past first open, prior median body/range scales, one tick as the minimum strict separation, and explicit trend context.

VariantDefinitionBest useMain limitation
Canonical scale-aware detectorcontained second body then confirmation past first openTransparent cross-language teaching and scanningThresholds require calibration before empirical use
TA-Lib CDL3INSIDEMaintained library-specific candle settings and lookbackCompatibility with TA-Lib ecosystemsDoes not verify prior trend for these functions
Discretionary chart readingVisual judgment with flexible gaps and body labelsHuman contextual reviewNot reproducible without a written rubric

What is sourced, selected, synthetic, and derived

RoleMaterial claimEvidenceBoundary
Sourced factTA-Lib documents a long body, a short contained body, and an opposite third candle closing past the first open, with signed bullish or bearish output.S1 exact function page; S2 confirms the separate pattern-function taxonomyThe maintained library convention is a comparison, not the package specification.
Implementation choiceThe canonical detector evaluates exactly three finalized candles using contained second body then confirmation past first open, prior median body/range scales, one tick as the minimum strict separation, and explicit trend context.Frozen definition contract, canonical fixture, and cross-language testsThresholds and equality policies are repository choices.
Synthetic teaching inputEvery OHLC value, prior scale sample, trend label, and scenario is repository-authored.datasets/canonical-input.json and scenario-results.jsonNo displayed bar is a historical security observation.
Author-derived calculationThe synthetic canonical fixture satisfies the contained second body then confirmation past first open; median prior body is 2.0 price units, median prior range is 3.0, tick size is 0.1, and every emitted check plus required context passes.Formula, independent arithmetic, expected output, and parity testsA correct detector is not evidence of predictive or economic value.

The authoritative sources support only the exact facts named in the claim ledger. They do not certify the synthetic numbers in this tutorial. The repository fixture is deliberately invented for auditability, and the displayed output is author-derived under the selected implementation choice.

Formula, symbols, and numerical policy

Plain text
matched = all(geometry checks) and trend_context = required_context; body_scale = median(last 10 prior bodies); range_scale = median(last 10 prior ranges)
SymbolMeaningUnitPolicy
B_ireal-body length of candle ipriceabsolute close-open
S_Bprior body scalepricemedian of latest ten eligible prior bodies
S_Rprior range scalepricemedian of matching prior ranges
tautick sizepriceminimum strict gap or confirmation step
rhopenetration fractionratiocanonical 0.50
  • Prices, bodies, ranges, gaps, and tick size share one price unit and basis.
  • Equality passes inclusive body-size, penetration, shadow, containment, and open-inside comparisons.
  • Strict directional confirmation and gap separation require at least one tick where declared.
  • No epsilon is hidden; upstream prices should already conform to the instrument tick table.

Read the formula in the same order as the algorithm. Validate identity, ordering, units, and supported state first. Apply the selected equality and window rules second. Calculate with unrounded numeric values. Round only at the declared presentation boundary, and preserve null as a diagnostic rather than coercing it to zero.

Build the algorithm

  1. Validate OHLC and one consistent price basis
  2. Build median prior-body and prior-range scales
  3. Evaluate candle directions and scale thresholds
  4. Evaluate gaps, containment, opens, shadows, or penetration
  5. Apply required trend context
  6. Return matched state and complete check ledger

Production-minded operational checklist

  1. Partition by instrument, interval, session, and price basis.
  2. Use only finalized bars and prior-only scale history.
  3. Compute or obtain causal trend context before evaluating the pattern.
  4. Persist thresholds, check ledger, direction, context, provider basis, and code version.
  5. Evaluate predictive usefulness separately with costs, bias controls, and out-of-sample data.

The checklist is intentionally strict: an explicit rejection is safer than a plausible output built from stale, malformed, or unsupported state.

Worked synthetic example

The canonical fixture is synthetic teaching data, not an observed control event, customer order, or broker execution. Its primary author-derived output, matched, is matched=true, direction bullish or bearish, with a complete check ledger. The complete input and output are in datasets/canonical-input.json and datasets/expected-output.json.

The synthetic canonical fixture satisfies the contained second body then confirmation past first open; median prior body is 2.0 price units, median prior range is 3.0, tick size is 0.1, and every emitted check plus required context passes.

Counterfactual checkpoint

Three Inside Up/Down near miss. Move one candle exactly across the strict containment versus endpoint equality. The output changes because a named pattern must be a testable contract rather than an approximate drawing

The structured result retains state and diagnostics in addition to the primary number. That makes the calculation independently reviewable and prevents a partial, null, rejected, or venue-bounded outcome from being mistaken for an unqualified value.

Boundary and counterexample workbook

The playground computes every scenario at 61 deterministic parameter states. The table uses the declared focus step and states whether that focus reproduces the canonical fixture. The full state ledger and compressed transition segments are in datasets/scenario-results.json.

ScenarioReview focusPurposeStatePrimary outputDiagnosticDecision segments
Canonical confirmation sweepStep 30 · canonical fixtureThe final close moves around the canonical fixture; state 31 exactly reproduces expected output.matched1.000bullish · 0 failed checks2
Equality boundaryStep 30 · comparison focusThe decisive confirmation or penetration level is crossed one tick at a time.matched1.000bullish · 0 failed checks2
Trend-context gateStep 30 · comparison focusIdentical candle geometry is evaluated under uptrend, sideways, and downtrend labels.wrong-context1.000bullish · 0 failed checks2
Warm-up historyStep 30 · comparison focusPrior-only scale history grows from insufficient to ready without using the current candles.matched1.000bullish · 0 failed checks2
Opposite-direction comparisonStep 30 · comparison focusA mirrored three-candle sequence exposes directional symmetry or a deliberate non-match.matched1.000bearish · 0 failed checks1
Structural boundary stressStep 30 · canonical fixtureThe topic's gap, containment, engulfing, open, or shadow boundary is stressed directly.matched1.000bullish · 0 failed checks1
Final-candle rejectionStep 30 · comparison focusThe third candle moves through neutral and opposite direction so rejection remains explicit.not-matched0.800bullish · 1 failed checks2

These rows are not backtest observations. They are controlled counterexamples that expose how one driver changes the state, output, or reason code while the rest of the contract stays fixed.

Visualize the boundary

Three Inside Up/Down annotated teaching map

Open this SVG at full size, or use the guided playground to compare the seven topic-specific canonical, boundary, policy, and failure scenarios.

The Mermaid flow answers where the selected calculation sits in the processing sequence. The SVG keeps the formula, output, decision boundary, and invariant visible together. The lab lets the reader step through the same structured states without changing the underlying definition.

Implementation walkthrough

The Python and TypeScript references begin with the same validation contract, reject malformed and unsupported state before calculation, preserve declared ordering and rounding policies, and return structured diagnostics rather than one context-free number.

The main implementation branches are:

  • Fewer than five prior observations — Return warmup, because Scale-aware thresholds are unavailable.
  • Geometry passes but context differs — Return wrong-context, because A reversal label requires its declared prior trend.
  • The strict containment versus endpoint equality fails — Return not-matched and name the failed check, because Near misses must remain auditable.
  • Every check and context pass — Return matched with direction, because The complete frozen contract is satisfied.

Neither reference silently fetches data, mutates caller-owned inputs outside the declared engine behavior, guesses hidden state, or substitutes a provider default. Shared JSON fixtures make value, null, state, and reason-code drift visible across languages.

Testing and validation

Definition tests compare every canonical field, reject malformed state, and exercise the material boundary. Family validation recomputes every playground state from the reference function. Independent arithmetic is recorded beside the fixture rather than inferred only from implementation output.

The audit must preserve these invariants:

  • Every check is emitted by name with pass/fail state.
  • failed_checks preserves the exact rejected relationships.
  • wrong-context separates valid geometry from an invalid reversal context.
  • warmup never invents a scale or pattern match.

Passing definition and parity checks proves that the implementation matches the selected contract. It does not prove production performance, universal applicability, or a later market outcome.

Failure modes and misuse

  • Pattern thresholds are implementation choices and may disagree with charting libraries or discretionary analysts.
  • An explicit trend label can itself be wrong, stale, or produced by a different causal filter.
  • Finalized OHLC discards intrabar path and order-flow information.
  • A matched detector is not a forecast, entry, exit, or claim of economic value.

Debugging order

When a result looks surprising, inspect the state in this order:

  1. Confirm identifiers, scope, side, and decision clock.
  2. Confirm units, ordering, and point-in-time inputs.
  3. Confirm equality, rounding, null, and reset policies.
  4. Recalculate the invariant and declared scenario focus before changing code.

Evidence and historical boundary

Historical decision: not useful. A named chart occurrence would add story value but not improve the executable definition. Its classification would depend on provider identity, session calendar, adjusted or unadjusted basis, finalized OHLC, trend algorithm, threshold configuration, and point-in-time availability.

The primary sources are TA-Lib CDL3INSIDE, TA-Lib pattern-recognition index, Nison publisher record, NIST median guidance. They support the source roles listed in the research ledger, not a redistributable historical observation, a private participant decision, production conformance certification, execution-quality result, profitability claim, or prediction claim.

Summary and next topic

You can now implement and audit the Three Inside Up/Down detector without conflating recognition with a forecast. The learning flow is: Three Black Crows → Three Inside Up/Down → Three Outside Up/Down. Carry the result forward only with its scope, clock, state, and evidence label.

Three Inside Up/Down calculation flow

This flow identifies the selected calculation stages and the structured output.

Rendering system map…

Takeaway: Three Inside Up/Down is a sequence of explicit comparisons, not a silhouette guessed from three colored bars.

ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

S1 — CDL3INSIDE function documentation

  • Organization or authors: TA-Lib project
  • Source type: Official maintained algorithm documentation
  • Publication or effective date: Current page accessed 2026-08-01
  • Version: Online definition updated 2026-07-27
  • URL or DOI: https://ta-lib.org/functions/cdl3inside
  • Accessed: 2026-08-01
  • Jurisdiction: Library convention; not market-specific
  • Supports: TA-Lib documents a long body, a short contained body, and an opposite third candle closing past the first open, with signed bullish or bearish output.
  • Limitations: The repository declares different scale windows, thresholds, explicit context handling, output schema, and some stricter equality rules; it does not claim parity.

S2 — TA-Lib Pattern Recognition Functions

  • Organization or authors: TA-Lib project
  • Source type: Official maintained technical documentation
  • Publication or effective date: Current page accessed 2026-08-01
  • Version: Online function catalog updated 2026-07-31
  • URL or DOI: https://ta-lib.org/functions/
  • Accessed: 2026-08-01
  • Jurisdiction: Library convention; not market-specific
  • Supports: TA-Lib exposes distinct functions for all seven catalog topics and therefore treats these names as separate algorithm contracts.
  • Limitations: The catalog does not make the package TA-Lib-compatible; exact defaults and lookbacks are implementation conventions.

S3 — Japanese Candlestick Charting Techniques, Second Edition

  • Organization or authors: Steve Nison; Prentice Hall Press
  • Source type: Authoritative book and publisher record
  • Publication or effective date: Published 2001-11-01; record accessed 2026-08-01
  • Version: Second edition; ISBN 9780735201811
  • URL or DOI: https://www.penguinrandomhouse.com/books/350650/japanese-candlestick-charting-techniques-by-steve-nison/
  • Accessed: 2026-08-01
  • Jurisdiction: General market education
  • Supports: Candlestick patterns are established technical-analysis vocabulary and are commonly combined with other context.
  • Limitations: The publisher record is not an executable numerical specification and does not validate package thresholds or predictive value.

S4 — Measures of Location: Mean and Median

  • Organization or authors: NIST/SEMATECH
  • Source type: Authoritative statistical handbook
  • Publication or effective date: Handbook page accessed 2026-08-01
  • Version: e-Handbook of Statistical Methods
  • URL or DOI: https://itl.nist.gov/div898/handbook/eda/section3/eda351.htm
  • Accessed: 2026-08-01
  • Jurisdiction: General statistical method
  • Supports: The median is less affected than the mean by extreme tail observations, motivating the package's transparent prior-body and prior-range scales.
  • Limitations: NIST does not prescribe candlestick thresholds or pattern definitions.

Evidence boundary

The sources establish the exact rule, interface, protocol, or research context named above. They do not verify the repository-authored synthetic fixture, thresholds, empirical usefulness, execution probability, or profitability. Package-selected choices remain labeled as implementation choices wherever they are used.

multi-candle-patterns.ts
export type JsonObject = Record<string, any>;

const TITLES: Record<string, string> = {
  "D06-F04-A01": "Morning Star",
  "D06-F04-A02": "Evening Star",
  "D06-F04-A03": "Three White Soldiers",
  "D06-F04-A04": "Three Black Crows",
  "D06-F04-A05": "Three Inside Up/Down",
  "D06-F04-A06": "Three Outside Up/Down",
  "D06-F04-A07": "Abandoned Baby",
};

function finite(value: unknown, name: string): number {
  if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${name} must be a finite number`);
  return value;
}

function positive(value: unknown, name: string): number {
  const result = finite(value, name);
  if (result <= 0) throw new RangeError(`${name} must be positive`);
  return result;
}

function fraction(value: unknown, name: string): number {
  const result = finite(value, name);
  if (result < 0 || result > 1) throw new RangeError(`${name} must be between 0 and 1`);
  return result;
}

function numbers(value: unknown, name: string): number[] {
  if (!Array.isArray(value)) throw new TypeError(`${name} must be an array`);
  const result = value.map((item, index) => finite(item, `${name}[${index}]`));
  if (result.some(item => item < 0)) throw new RangeError(`${name} cannot contain negative values`);
  return result;
}

function candle(value: unknown, name: string): JsonObject {
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${name} must be an object`);
  const raw = value as JsonObject;
  const result = {open: finite(raw.open, `${name}.open`), high: finite(raw.high, `${name}.high`), low: finite(raw.low, `${name}.low`), close: finite(raw.close, `${name}.close`)};
  if (result.high < Math.max(result.open, result.close, result.low)) throw new RangeError(`${name}.high is below another OHLC value`);
  if (result.low > Math.min(result.open, result.close, result.high)) throw new RangeError(`${name}.low is above another OHLC value`);
  return result;
}

function anatomy(raw: JsonObject): JsonObject {
  const bodyHigh = Math.max(raw.open, raw.close), bodyLow = Math.min(raw.open, raw.close), body = bodyHigh - bodyLow;
  const direction = raw.close > raw.open ? "bullish" : raw.close < raw.open ? "bearish" : "neutral";
  return {...raw, body_high: bodyHigh, body_low: bodyLow, body, upper_shadow: raw.high - bodyHigh, lower_shadow: bodyLow - raw.low, range: raw.high - raw.low, direction};
}

function median(values: number[]): number {
  const ordered = [...values].sort((a, b) => a - b), middle = Math.floor(ordered.length / 2);
  return ordered.length % 2 ? ordered[middle] : (ordered[middle - 1] + ordered[middle]) / 2;
}

function base(data: JsonObject): JsonObject {
  if (!Array.isArray(data.candles) || data.candles.length !== 3) throw new RangeError("candles must contain exactly three OHLC candles");
  const candles = data.candles.map((item: unknown, index: number) => anatomy(candle(item, `candles[${index}]`)));
  const tick = positive(data.tick_size, "tick_size");
  const context = data.trend_context;
  if (!new Set(["uptrend", "downtrend", "sideways"]).has(context)) throw new RangeError("trend_context must be uptrend, downtrend, or sideways");
  const bodies = numbers(data.prior_bodies, "prior_bodies"), ranges = numbers(data.prior_ranges, "prior_ranges");
  if (bodies.length !== ranges.length) throw new RangeError("prior_bodies and prior_ranges must have equal length");
  if (bodies.some((body, index) => body > ranges[index])) throw new RangeError("a prior body cannot exceed its range");
  const count = bodies.length;
  return {candles, tick, context, bodyScale: count >= 5 ? median(bodies.slice(-10)) : null, rangeScale: count >= 5 ? median(ranges.slice(-10)) : null, count, penetration: fraction(data.penetration_fraction ?? 0.5, "penetration_fraction")};
}

function result(topicId: string, candles: JsonObject[], context: string, requiredContext: string | null, bodyScale: number | null, rangeScale: number | null, historyCount: number, direction: string, inputChecks: Record<string, boolean>, thresholds: JsonObject): JsonObject {
  let checks = inputChecks, state: string, matched: boolean;
  if (bodyScale === null || rangeScale === null) { checks = {minimum_history: false}; state = "warmup"; matched = false; }
  else {
    const geometry = Object.values(checks).every(Boolean);
    if (geometry && requiredContext !== null && context !== requiredContext) { state = "wrong-context"; matched = false; }
    else { matched = geometry; state = matched ? "matched" : "not-matched"; }
  }
  const failed = Object.entries(checks).filter(([, passed]) => !passed).map(([name]) => name);
  const score = Object.values(checks).filter(Boolean).length / Math.max(1, Object.keys(checks).length);
  const reason = failed.length ? `failed ${failed[0].replaceAll("_", " ")}` : "all declared checks pass";
  return {topic_id: topicId, pattern: TITLES[topicId], matched, state, direction, trend_context: context, required_context: requiredContext, history_count: historyCount, body_scale: bodyScale, range_scale: rangeScale, geometry_score: score, checks, failed_checks: failed, reason, thresholds, candles};
}

function star(topicId: string, data: JsonObject): JsonObject {
  const b = base(data), [a, middle, c] = b.candles, bullish = topicId === "D06-F04-A01", required = bullish ? "downtrend" : "uptrend";
  if (b.bodyScale === null || b.rangeScale === null) return result(topicId, b.candles, b.context, required, b.bodyScale, b.rangeScale, b.count, bullish ? "bullish" : "bearish", {}, {});
  const longFloor = 1.2 * b.bodyScale, shortCeiling = 0.6 * b.bodyScale;
  let penetrationPrice: number, checks: Record<string, boolean>;
  if (bullish) {
    penetrationPrice = a.close + b.penetration * a.body;
    checks = {first_bearish: a.direction === "bearish", first_long: a.body >= longFloor, second_short: middle.body <= shortCeiling, body_gap_down: middle.body_high <= a.body_low - b.tick, third_bullish: c.direction === "bullish", third_penetrates: c.close >= penetrationPrice};
  } else {
    penetrationPrice = a.close - b.penetration * a.body;
    checks = {first_bullish: a.direction === "bullish", first_long: a.body >= longFloor, second_short: middle.body <= shortCeiling, body_gap_up: middle.body_low >= a.body_high + b.tick, third_bearish: c.direction === "bearish", third_penetrates: c.close <= penetrationPrice};
  }
  return result(topicId, b.candles, b.context, required, b.bodyScale, b.rangeScale, b.count, bullish ? "bullish" : "bearish", checks, {long_body_floor: longFloor, short_body_ceiling: shortCeiling, penetration_price: penetrationPrice, minimum_gap: b.tick, penetration_fraction: b.penetration});
}

function soldiersOrCrows(topicId: string, data: JsonObject): JsonObject {
  const b = base(data), [a, middle, c] = b.candles, bullish = topicId === "D06-F04-A03", required = bullish ? "downtrend" : "uptrend";
  if (b.bodyScale === null || b.rangeScale === null) return result(topicId, b.candles, b.context, required, b.bodyScale, b.rangeScale, b.count, bullish ? "bullish" : "bearish", {}, {});
  const bodyFloor = 0.75 * b.bodyScale, shadowCeiling = 0.2 * b.rangeScale;
  const checks: Record<string, boolean> = bullish ? {
    three_bullish: b.candles.every((item: JsonObject) => item.direction === "bullish"), material_bodies: b.candles.every((item: JsonObject) => item.body >= bodyFloor), rising_closes: middle.close >= a.close + b.tick && c.close >= middle.close + b.tick, second_opens_in_prior_body: a.body_low <= middle.open && middle.open <= a.body_high, third_opens_in_prior_body: middle.body_low <= c.open && c.open <= middle.body_high, short_upper_shadows: b.candles.every((item: JsonObject) => item.upper_shadow <= shadowCeiling)
  } : {
    three_bearish: b.candles.every((item: JsonObject) => item.direction === "bearish"), material_bodies: b.candles.every((item: JsonObject) => item.body >= bodyFloor), falling_closes: middle.close <= a.close - b.tick && c.close <= middle.close - b.tick, second_opens_in_prior_body: a.body_low <= middle.open && middle.open <= a.body_high, third_opens_in_prior_body: middle.body_low <= c.open && c.open <= middle.body_high, short_lower_shadows: b.candles.every((item: JsonObject) => item.lower_shadow <= shadowCeiling)
  };
  return result(topicId, b.candles, b.context, required, b.bodyScale, b.rangeScale, b.count, bullish ? "bullish" : "bearish", checks, {body_floor: bodyFloor, shadow_ceiling: shadowCeiling, minimum_close_step: b.tick});
}

function inside(data: JsonObject): JsonObject {
  const topicId = "D06-F04-A05", b = base(data), [a, middle, c] = b.candles;
  const bullish = a.direction === "bearish" && middle.direction === "bullish", bearish = a.direction === "bullish" && middle.direction === "bearish";
  const direction = bullish ? "bullish" : bearish ? "bearish" : "none", required = bullish ? "downtrend" : bearish ? "uptrend" : null;
  if (b.bodyScale === null || b.rangeScale === null) return result(topicId, b.candles, b.context, required, b.bodyScale, b.rangeScale, b.count, direction, {}, {});
  const longFloor = 1.2 * b.bodyScale, shortCeiling = 0.6 * b.bodyScale;
  const contained = middle.body_low >= a.body_low && middle.body_high <= a.body_high && (middle.body_low > a.body_low || middle.body_high < a.body_high);
  const confirms = (bullish && c.direction === "bullish" && c.close >= a.open + b.tick) || (bearish && c.direction === "bearish" && c.close <= a.open - b.tick);
  return result(topicId, b.candles, b.context, required, b.bodyScale, b.rangeScale, b.count, direction, {opposite_first_pair: bullish || bearish, first_long: a.body >= longFloor, second_short: middle.body <= shortCeiling, second_body_inside: contained, third_confirms_past_first_open: confirms}, {long_body_floor: longFloor, short_body_ceiling: shortCeiling, confirmation_step: b.tick});
}

function outside(data: JsonObject): JsonObject {
  const topicId = "D06-F04-A06", b = base(data), [a, middle, c] = b.candles;
  const bullish = a.direction === "bearish" && middle.direction === "bullish", bearish = a.direction === "bullish" && middle.direction === "bearish";
  const direction = bullish ? "bullish" : bearish ? "bearish" : "none", required = bullish ? "downtrend" : bearish ? "uptrend" : null;
  const engulfed = middle.body_low <= a.body_low && middle.body_high >= a.body_high && middle.body > a.body;
  const confirms = (bullish && c.direction === "bullish" && c.close >= middle.close + b.tick) || (bearish && c.direction === "bearish" && c.close <= middle.close - b.tick);
  return result(topicId, b.candles, b.context, required, b.bodyScale, b.rangeScale, b.count, direction, {opposite_first_pair: bullish || bearish, second_body_engulfs: engulfed, third_confirms_beyond_second_close: confirms}, {confirmation_step: b.tick});
}

function abandoned(data: JsonObject): JsonObject {
  const topicId = "D06-F04-A07", b = base(data), [a, middle, c] = b.candles;
  const bullish = a.direction === "bearish" && c.direction === "bullish", bearish = a.direction === "bullish" && c.direction === "bearish";
  const direction = bullish ? "bullish" : bearish ? "bearish" : "none", required = bullish ? "downtrend" : bearish ? "uptrend" : null;
  if (b.bodyScale === null || b.rangeScale === null) return result(topicId, b.candles, b.context, required, b.bodyScale, b.rangeScale, b.count, direction, {}, {});
  const longFloor = 1.2 * b.bodyScale, dojiCeiling = 0.1 * b.rangeScale;
  let penetrationPrice = a.close, separated = false, penetrates = false;
  if (bullish) { penetrationPrice = a.close + b.penetration * a.body; separated = middle.high <= a.low - b.tick && c.low >= middle.high + b.tick; penetrates = c.close >= penetrationPrice; }
  else if (bearish) { penetrationPrice = a.close - b.penetration * a.body; separated = middle.low >= a.high + b.tick && c.high <= middle.low - b.tick; penetrates = c.close <= penetrationPrice; }
  return result(topicId, b.candles, b.context, required, b.bodyScale, b.rangeScale, b.count, direction, {opposite_outer_candles: bullish || bearish, first_long: a.body >= longFloor, middle_doji: middle.body <= dojiCeiling, full_range_isolation: separated, third_penetrates: penetrates}, {long_body_floor: longFloor, doji_body_ceiling: dojiCeiling, minimum_gap: b.tick, penetration_price: penetrationPrice, penetration_fraction: b.penetration});
}

export function calculate(topicId: string, inputs: JsonObject): JsonObject {
  if (inputs === null || typeof inputs !== "object" || Array.isArray(inputs)) throw new TypeError("inputs must be an object");
  if (["D06-F04-A01", "D06-F04-A02"].includes(topicId)) return star(topicId, inputs);
  if (["D06-F04-A03", "D06-F04-A04"].includes(topicId)) return soldiersOrCrows(topicId, inputs);
  if (topicId === "D06-F04-A05") return inside(inputs);
  if (topicId === "D06-F04-A06") return outside(inputs);
  if (topicId === "D06-F04-A07") return abandoned(inputs);
  throw new RangeError(`unsupported topic id ${topicId}`);
}
Full-height labplaygroundOpen full screen