Classify adjacent-bar gaps by increasing geometric strength while retaining every underlying flag and an explicit tick-distance threshold.
The decision this tutorial makes visible
The word gap is overloaded: an opening jump can coexist with later overlap, while a full-range window leaves no traded-price overlap between bars. Downstream patterns need those states separated.
The precise question is: Does the current candle create an open, real-body, or full-range gap relative to the prior candle, and in which direction?
A practitioner can see exactly which geometry or context rule failed. A builder can reproduce the same decision from frozen OHLC, scales, tick policy, and prior-only state in Python, TypeScript, fixtures, visuals, and the guided lab.
Intuition before notation
Open gaps describe the first print relationship; body gaps compare open-close regions; full-range gaps prove the two high-low intervals do not overlap. One label should not silently stand for all three.
The package teaches one auditable convention and names nearby variants. It does not imply that platforms sharing the same label use identical thresholds.
Scope and nearby methods
Two consecutive comparable closed candles. Full-range gap has precedence over body gap, which has precedence over open-only gap. A positive distance equal to the configured tick threshold qualifies; zero distance never qualifies.
| Variant | Definition | Best use | Main limitation |
|---|---|---|---|
| Open gap | current open versus prior close | Opening discontinuity | May later overlap |
| Body gap | real-body intervals do not overlap | Body geometry | Shadows may overlap |
| Full-range gap | high-low intervals do not overlap | Strict price window | Rare in continuous markets |
What is sourced, selected, synthetic, and derived
| Role | Material claim | Evidence | Boundary |
|---|---|---|---|
| Sourced fact | Candlesticks encode OHLC as a real body and shadows, and established pattern names are interpreted under configurable conventions and market context. | S1-S4 | Sources support terminology and convention/context roles, not this package's thresholds or a forecast. |
| Implementation choice | Two consecutive comparable closed candles. Full-range gap has precedence over body gap, which has precedence over open-only gap. A positive distance equal to the configured tick threshold qualifies; zero distance never qualifies. | Frozen package definition | Other books and software can use different averages, factors, equality, gaps, or trend filters. |
| Synthetic teaching input | Every OHLC value, scale history, trend input, tick, tolerance, and scenario path in the package is repository-authored. | datasets/canonical-input.json and scenario-results.json | No value is an observed security, exchange session, provider bar, or return. |
| Author-derived calculation | The structured gap_distance output follows from the printed formula and synthetic fixture. | expected-output.json, independent arithmetic, and parity tests | Definition fidelity is not empirical or predictive validation. |
The sources support candle construction, established names, configurable relative thresholds, and the need for context. The numeric fixture, medians, thresholds, equality operators, and outputs are repository-authored synthetic choices and author-derived calculations.
Formula, symbols, and numerical policy
For up gaps compare O_t-C_{t-1}, bodyLow_t-bodyHigh_{t-1}, and L_t-H_{t-1}; mirror signs for down gaps. A distance qualifies iff distance>0 and distance>=tick*minimumGapTicks.
| Symbol | Meaning | Unit | Policy |
|---|---|---|---|
| O_t,L_t,H_t | Current open, low, high | price | Same basis as prior bar |
| C_{t-1} | Prior close | price | Open-gap anchor |
| g_min | Minimum gap distance | price | tick × configured ticks |
- Calculate with unrounded finite OHLC differences in one declared price unit and adjustment basis.
- Use an explicit positive tick size; threshold equality follows the operator printed in the formula.
- Reference medians use at most the latest ten eligible prior closed bars and never include a candidate candle in its own baseline.
- Round only for display; preserve null, warm-up, zero-body, wrong-context, and failed-check states instead of coercing them to a match.
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
- Validate both candles and tick configuration.
- Calculate six directional gap distances.
- Apply positive-distance and threshold rules.
- Select full-range, then body, then open-only precedence and retain all flags.
Production-minded operational checklist
- Confirm consecutive comparable bars
- Record session calendar
- Record tick threshold
- Retain all gap flags
Reject a plausible-looking label when the bar is provisional, reference state is insufficient, session/basis is mixed, or required prior context is unavailable.
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,
gap_distance, is 1.0. The complete input and output
are in datasets/canonical-input.json and datasets/expected-output.json.
The current low 106 is one unit above the prior high 105. With a one-tick threshold of one, the equality qualifies as an up full-range gap; the selected distance is 1 while the more specific flags remain inspectable.
Counterfactual checkpoint
Exact decision boundary. Move the teaching driver through the printed threshold. The output changes because The detector compares unrounded geometry with an explicit scale-aware threshold.
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.
| Scenario | Review focus | Purpose | State | Primary output | Diagnostic | Decision segments |
|---|---|---|---|---|---|---|
| Canonical threshold sweep | Step 30 · canonical fixture | Move the defining synthetic geometry through the exact canonical boundary; state 31 reproduces the complete fixture. | gap | full-range up gap 1.0000 | threshold 1.0000 | 1 |
| Threshold equality | Step 30 · canonical fixture | Retain the exact one-tick full-range gap. | gap | full-range up gap 1.0000 | threshold 1.0000 | 1 |
| Open-only gap | Step 30 · comparison focus | Opening jumps but the range overlaps. | gap | open up gap 2.0000 | threshold 1.0000 | 1 |
| Body gap | Step 30 · comparison focus | Bodies separate while shadows overlap. | gap | body up gap 2.0000 | threshold 1.0000 | 1 |
| Exact touch | Step 30 · comparison focus | Zero distance remains overlap. | gap | body up gap 1.0000 | threshold 0.0000 | 1 |
| Down full-range gap | Step 30 · comparison focus | Mirror the direction. | gap | full-range down gap 1.0000 | threshold 1.0000 | 1 |
| Two-tick policy | Step 30 · comparison focus | Reject the one-tick gap under a stricter policy. | gap | body up gap 2.0000 | threshold 2.0000 | 1 |
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
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 references validate OHLC first, derive body and shadows once, calculate causal scales or context, evaluate named Boolean checks in formula order, then expose match, state, diagnostics, and every failure reason.
The main implementation branches are:
- full-range distance qualifies — Select full-range gap, because Strongest non-overlap geometry.
- only body distance qualifies — Select body gap, because Bodies separate but ranges overlap.
- only open distance qualifies — Select open gap, because Initial jump later overlaps.
- no positive distance qualifies — Return overlap, because No declared gap.
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:
- All six signed candidate distances
- Tick threshold and equality
- Precedence-selected type and direction
Passing these checks proves that both implementations match the selected detector contract on the fixture and scenarios. It does not validate a market forecast.
Failure modes and misuse
- A geometry match is not confirmation, a directional forecast, an order, or investment advice.
- Thresholds and lookbacks are explicit teaching choices, not universal market standards or calibrated parameters.
- Feed construction, sessions, corporate actions, contract rolls, ticks, missing bars, provisional candles, and later corrections can change a result.
- Passing definition and parity tests does not establish historical association, cost-adjusted performance, robustness, or profitability.
Debugging order
When a result looks surprising, inspect the state in this order:
- Validate OHLC ordering, closure, interval, session, price basis, and tick size.
- Recalculate body, shadows, body endpoints, and the prior median scales.
- Confirm prior trend was frozen before the pattern began and inspect equality/tolerance rules.
- Compare every named Boolean check before interpreting the final match label.
Evidence and historical boundary
Historical decision: not useful. A named historical bar is not useful for defining deterministic candle geometry. The packages use synthetic, fully redistributable OHLC fixtures; a production substitution requires licensed point-in-time bars with instrument, venue, interval, session, price basis, retrieval time, revision state, and tick size.
The primary sources are CME candlestick chart lesson, Nasdaq candlestick glossary, TA-Lib function catalog. They support the source roles listed in the research ledger, not a redistributable historical observation, a universal candlestick definition, a licensed historical market event, a predictive edge, a trading recommendation, or a profitability claim.
Summary and next topic
You can now calculate Gap Classification under an explicit definition and carry its structured evidence into Trend-Context Filter. The learning flow is: Shadow-to-Body Ratio → Gap Classification → Trend-Context Filter. Carry the result forward only with its scope, clock, state, and evidence label.
Rendered from the canonical Mermaid sources linked by this article.
Gap Classification calculation flow
This flow identifies the selected calculation stages and the structured output.
Takeaway: An opening jump, separated bodies, and a full price window are distinct geometries and should remain distinct fields.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
S1 — Chart Types: Candlestick, Line, Bar
- Organization or authors: CME Group
- Source type: Official exchange education
- Publication or effective date: current
- Version: Accessed 2026-08-01
- URL or DOI: https://www.cmegroup.com/education/courses/technical-analysis/chart-types-candlestick-line-bar
- Accessed: 2026-08-01
- Jurisdiction: Global futures education
- Supports: Candles encode open, high, low, and close; body and wick sizes describe bar geometry, and gaps compare adjacent bars.
- Limitations: Educational interpretation does not define the repository thresholds or prove a forecast.
S2 — Candlestick chart
- Organization or authors: Nasdaq
- Source type: Official market glossary
- Publication or effective date: current
- Version: Accessed 2026-08-01
- URL or DOI: https://www.nasdaq.com/glossary/b/candlestick-chart
- Accessed: 2026-08-01
- Jurisdiction: General market terminology
- Supports: The real body spans open and close while the vertical lines reach the period high and low.
- Limitations: Does not standardize pattern thresholds, trend filters, or empirical meaning.
S3 — TA-Lib Pattern Recognition Functions
- Organization or authors: TA-Lib project
- Source type: Official maintained function catalog
- Publication or effective date: 2026
- Version: Function catalog accessed 2026-08-01
- URL or DOI: https://ta-lib.org/functions/
- Accessed: 2026-08-01
- Jurisdiction: Cross-platform software convention
- Supports: The named doji, hammer, marubozu, spinning-top, engulfing, harami, piercing, dark-cloud, and related recognizers are established programmable pattern categories.
- Limitations: Catalog presence does not make any threshold universal or validate the package's result against TA-Lib.
Evidence boundary
Sources establish the chart geometry, names, configurable-convention context, and contextual interpretation named in their records. They do not certify repository thresholds, synthetic bars, matches, or future returns.
Full dependency-light reference implementations in both supported languages.
/** Transparent, repository-selected D06 candle and pattern contracts. */
type AnyMap = Record<string, any>;
const F02_TITLES: Record<string, string> = {
"D06-F02-A01": "Doji", "D06-F02-A02": "Dragonfly Doji", "D06-F02-A03": "Gravestone Doji",
"D06-F02-A04": "Marubozu", "D06-F02-A05": "Spinning Top", "D06-F02-A06": "Hammer",
"D06-F02-A07": "Hanging Man", "D06-F02-A08": "Inverted Hammer", "D06-F02-A09": "Shooting Star",
};
const F03_TITLES: Record<string, string> = {
"D06-F03-A01": "Bullish Engulfing", "D06-F03-A02": "Bearish Engulfing", "D06-F03-A03": "Bullish Harami",
"D06-F03-A04": "Bearish Harami", "D06-F03-A05": "Piercing Line", "D06-F03-A06": "Dark Cloud Cover",
"D06-F03-A07": "Tweezer Top", "D06-F03-A08": "Tweezer Bottom",
};
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 n = finite(value, name); if (n <= 0) throw new RangeError(`${name} must be positive`); return n; }
function nonnegative(value: unknown, name: string): number { const n = finite(value, name); if (n < 0) throw new RangeError(`${name} must be non-negative`); return n; }
function candle(value: unknown, name: string): AnyMap {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${name} must be an object`);
const source = value as AnyMap;
const result = {open: finite(source.open, `${name}.open`), high: finite(source.high, `${name}.high`), low: finite(source.low, `${name}.low`), close: finite(source.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(c: AnyMap): AnyMap {
const bodyHigh = Math.max(c.open, c.close), bodyLow = Math.min(c.open, c.close);
return {body_high: bodyHigh, body_low: bodyLow, body: bodyHigh-bodyLow, upper_shadow: c.high-bodyHigh, lower_shadow: bodyLow-c.low, range: c.high-c.low, direction: c.close>c.open?"bullish":c.close<c.open?"bearish":"neutral"};
}
function numberList(value: unknown, name: string): number[] {
if (!Array.isArray(value)) throw new TypeError(`${name} must be an array`);
return value.map((item,index)=>nonnegative(item, `${name}[${index}]`));
}
function median(values: number[]): number { const ordered=[...values].sort((a,b)=>a-b), n=ordered.length; return n%2?ordered[(n-1)/2]:(ordered[n/2-1]+ordered[n/2])/2; }
function referenceScales(data: AnyMap): [number|null,number|null,number] {
const bodies=numberList(data.prior_bodies,"prior_bodies"), ranges=numberList(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");
if(bodies.length<5) return [null,null,bodies.length];
return [median(bodies.slice(-10)),median(ranges.slice(-10)),Math.min(10,bodies.length)];
}
function context(data: AnyMap): string { if(!["uptrend","downtrend","sideways"].includes(data.trend_context)) throw new RangeError("trend_context must be uptrend, downtrend, or sideways"); return data.trend_context; }
function patternResult(args: AnyMap): AnyMap {
let state:string, matched:boolean;
if(args.bodyScale===null||args.rangeScale===null){state="warmup";matched=false;}
else if(args.matchedGeometry&&args.requiredContext!==null&&args.actualContext!==args.requiredContext){state="wrong-context";matched=false;}
else {matched=args.matchedGeometry;state=matched?"matched":"not-matched";}
return {topic_id:args.topicId,pattern:args.title,matched,state,direction:args.a.direction,trend_context:args.actualContext,required_context:args.requiredContext,body:args.a.body,upper_shadow:args.a.upper_shadow,lower_shadow:args.a.lower_shadow,range:args.a.range,body_scale:args.bodyScale,range_scale:args.rangeScale,history_count:args.historyCount,geometry_score:args.geometryScore,checks:args.checks,failed_checks:Object.keys(args.checks).filter(key=>!args.checks[key])};
}
function shadowToBody(data: AnyMap): AnyMap {
const c=candle(data.candle,"candle"), tick=positive(data.tick_size,"tick_size"), a=anatomy(c), body=a.body, effective=Math.max(body,tick);
const upperRaw=body===0?null:a.upper_shadow/body, lowerRaw=body===0?null:a.lower_shadow/body, upperEffective=a.upper_shadow/effective, lowerEffective=a.lower_shadow/effective;
const dominant=upperEffective>lowerEffective?"upper":lowerEffective>upperEffective?"lower":"balanced";
return {state:body===0?"zero-body":"calculated",body,upper_shadow:a.upper_shadow,lower_shadow:a.lower_shadow,range:a.range,tick_size:tick,effective_body:effective,upper_to_body:upperRaw,lower_to_body:lowerRaw,upper_to_effective_body:upperEffective,lower_to_effective_body:lowerEffective,upper_to_range:a.range===0?null:a.upper_shadow/a.range,lower_to_range:a.range===0?null:a.lower_shadow/a.range,dominant_shadow:dominant,dominant_shadow_ratio:Math.max(upperEffective,lowerEffective)};
}
function gapClassification(data: AnyMap): AnyMap {
const previous=candle(data.previous,"previous"), current=candle(data.current,"current"), tick=positive(data.tick_size,"tick_size"), minimumTicks=nonnegative(data.minimum_gap_ticks,"minimum_gap_ticks"), threshold=tick*minimumTicks;
const pa=anatomy(previous), ca=anatomy(current);
const candidates:AnyMap={up:{open:current.open-previous.close,body:ca.body_low-pa.body_high,"full-range":current.low-previous.high},down:{open:previous.close-current.open,body:pa.body_low-ca.body_high,"full-range":previous.low-current.high}};
const qualifies=(distance:number)=>distance>0&&distance>=threshold;
let direction="none", gapType="overlap", distance=0;
outer: for(const candidateDirection of ["up","down"]) for(const candidateType of ["full-range","body","open"]){const d=candidates[candidateDirection][candidateType];if(qualifies(d)){direction=candidateDirection;gapType=candidateType;distance=d;break outer;}}
return {state:direction!=="none"?"gap":"overlap",gap_direction:direction,gap_type:gapType,gap_distance:distance,threshold_distance:threshold,open_gap_up:qualifies(candidates.up.open),open_gap_down:qualifies(candidates.down.open),body_gap_up:qualifies(candidates.up.body),body_gap_down:qualifies(candidates.down.body),full_range_gap_up:qualifies(candidates.up["full-range"]),full_range_gap_down:qualifies(candidates.down["full-range"]),signed_open_gap:current.open-previous.close,previous_close:previous.close,current_open:current.open};
}
function trendContext(data: AnyMap): AnyMap {
const closes=numberList(data.prior_closes,"prior_closes"), ranges=numberList(data.prior_ranges,"prior_ranges"), lookback=data.lookback;
if(!Number.isInteger(lookback)||lookback<3) throw new RangeError("lookback must be an integer of at least 3");
const efficiencyFloor=nonnegative(data.minimum_efficiency,"minimum_efficiency"), moveFloor=nonnegative(data.minimum_move_scale,"minimum_move_scale");
if(efficiencyFloor>1) throw new RangeError("minimum_efficiency cannot exceed 1");
if(closes.length!==ranges.length) throw new RangeError("prior_closes and prior_ranges must have equal length");
if(closes.length<lookback) return {state:"warmup",trend_context:"warmup",history_count:closes.length,net_move:null,path_move:null,efficiency:null,range_scale:null,normalized_move:null};
const sample=closes.slice(-lookback), rangeSample=ranges.slice(-lookback), net=sample.at(-1)!-sample[0];
let path=0;for(let i=1;i<sample.length;i++)path+=Math.abs(sample[i]-sample[i-1]);
const efficiency=path===0?0:Math.abs(net)/path, scale=median(rangeSample);
if(scale===0)return {state:"zero-scale",trend_context:"sideways",history_count:lookback,net_move:net,path_move:path,efficiency,range_scale:scale,normalized_move:null};
const normalized=net/scale, trend=efficiency>=efficiencyFloor&&normalized>=moveFloor?"uptrend":efficiency>=efficiencyFloor&&normalized<=-moveFloor?"downtrend":"sideways";
return {state:"ready",trend_context:trend,history_count:lookback,net_move:net,path_move:path,efficiency,range_scale:scale,normalized_move:normalized};
}
function singlePattern(topicId:string,data:AnyMap):AnyMap{
const c=candle(data.current,"current"),tick=positive(data.tick_size,"tick_size"),actualContext=context(data),[bodyScale,rangeScale,count]=referenceScales(data),a=anatomy(c);
if(bodyScale===null||rangeScale===null)return patternResult({topicId,title:F02_TITLES[topicId],a,matchedGeometry:false,requiredContext:null,actualContext,bodyScale:null,rangeScale:null,historyCount:count,geometryScore:0,checks:{minimum_history:false}});
const effectiveBody=Math.max(a.body,tick),dojiLimit=.10*rangeScale,shortShadowLimit=.05*rangeScale;let checks:Record<string,boolean>,requiredContext:string|null=null,score:number;
switch(topicId){
case"D06-F02-A01":checks={near_zero_body:a.body<=dojiLimit};score=1-a.body/Math.max(dojiLimit,tick);break;
case"D06-F02-A02":checks={doji_body:a.body<=dojiLimit,short_upper_shadow:a.upper_shadow<=shortShadowLimit,long_lower_shadow:a.lower_shadow>=.60*rangeScale};score=a.lower_shadow/Math.max(rangeScale,tick);break;
case"D06-F02-A03":checks={doji_body:a.body<=dojiLimit,long_upper_shadow:a.upper_shadow>=.60*rangeScale,short_lower_shadow:a.lower_shadow<=shortShadowLimit};score=a.upper_shadow/Math.max(rangeScale,tick);break;
case"D06-F02-A04":checks={long_body:a.body>=1.20*bodyScale,short_upper_shadow:a.upper_shadow<=shortShadowLimit,short_lower_shadow:a.lower_shadow<=shortShadowLimit};score=a.body/Math.max(bodyScale,tick);break;
case"D06-F02-A05":checks={short_body:a.body<=.60*bodyScale+1e-12,material_upper_shadow:a.upper_shadow+1e-12>=Math.max(a.body,.20*rangeScale),material_lower_shadow:a.lower_shadow+1e-12>=Math.max(a.body,.20*rangeScale)};score=Math.min(a.upper_shadow,a.lower_shadow)/effectiveBody;break;
case"D06-F02-A06":case"D06-F02-A07":requiredContext=topicId.endsWith("A06")?"downtrend":"uptrend";checks={compact_body:a.body<=.75*bodyScale,long_lower_shadow:a.lower_shadow>=2*effectiveBody,short_upper_shadow:a.upper_shadow<=.25*effectiveBody,body_near_high:a.upper_shadow<=.25*Math.max(a.range,tick)};score=a.lower_shadow/effectiveBody;break;
case"D06-F02-A08":case"D06-F02-A09":requiredContext=topicId.endsWith("A08")?"downtrend":"uptrend";checks={compact_body:a.body<=.75*bodyScale,long_upper_shadow:a.upper_shadow>=2*effectiveBody,short_lower_shadow:a.lower_shadow<=.25*effectiveBody,body_near_low:a.lower_shadow<=.25*Math.max(a.range,tick)};score=a.upper_shadow/effectiveBody;break;
default:throw new RangeError(`unsupported single-candle topic ${topicId}`);
}
return patternResult({topicId,title:F02_TITLES[topicId],a,matchedGeometry:Object.values(checks).every(Boolean),requiredContext,actualContext,bodyScale,rangeScale,historyCount:count,geometryScore:score,checks});
}
function twoPattern(topicId:string,data:AnyMap):AnyMap{
const first=candle(data.first,"first"),second=candle(data.second,"second"),tick=positive(data.tick_size,"tick_size"),tolerance=tick*nonnegative(data.price_tolerance_ticks,"price_tolerance_ticks"),actualContext=context(data),[bodyScale,rangeScale,count]=referenceScales(data),a=anatomy(first),b=anatomy(second);
if(bodyScale===null||rangeScale===null){const result=patternResult({topicId,title:F03_TITLES[topicId],a:{body:b.body,upper_shadow:b.upper_shadow,lower_shadow:b.lower_shadow,range:b.range,direction:b.direction},matchedGeometry:false,requiredContext:null,actualContext,bodyScale:null,rangeScale:null,historyCount:count,geometryScore:0,checks:{minimum_history:false}});Object.assign(result,{first_direction:a.direction,second_direction:b.direction,first_body:a.body,second_body:b.body,price_tolerance:tolerance});return result;}
const longFirst=a.body>=1.20*bodyScale,shortSecond=b.body<=.60*bodyScale,midpoint=(first.open+first.close)/2;let requiredContext:string,checks:Record<string,boolean>,score:number;
switch(topicId){
case"D06-F03-A01":requiredContext="downtrend";checks={first_bearish:a.direction==="bearish",second_bullish:b.direction==="bullish",lower_end_engulfed:b.body_low<=a.body_low,upper_end_engulfed:b.body_high>=a.body_high,strictly_larger_body:b.body>a.body};score=b.body/Math.max(a.body,tick);break;
case"D06-F03-A02":requiredContext="uptrend";checks={first_bullish:a.direction==="bullish",second_bearish:b.direction==="bearish",lower_end_engulfed:b.body_low<=a.body_low,upper_end_engulfed:b.body_high>=a.body_high,strictly_larger_body:b.body>a.body};score=b.body/Math.max(a.body,tick);break;
case"D06-F03-A03":requiredContext="downtrend";checks={first_bearish:a.direction==="bearish",long_first:longFirst,second_bullish:b.direction==="bullish",short_second:shortSecond,contained_low:b.body_low>=a.body_low,contained_high:b.body_high<=a.body_high,strict_containment:b.body_low>a.body_low||b.body_high<a.body_high};score=1-b.body/Math.max(a.body,tick);break;
case"D06-F03-A04":requiredContext="uptrend";checks={first_bullish:a.direction==="bullish",long_first:longFirst,second_bearish:b.direction==="bearish",short_second:shortSecond,contained_low:b.body_low>=a.body_low,contained_high:b.body_high<=a.body_high,strict_containment:b.body_low>a.body_low||b.body_high<a.body_high};score=1-b.body/Math.max(a.body,tick);break;
case"D06-F03-A05":requiredContext="downtrend";checks={first_bearish:a.direction==="bearish",long_first:longFirst,second_bullish:b.direction==="bullish",gap_below_first_low:second.open<=first.low-tick,close_above_midpoint:second.close>midpoint,close_below_first_open:second.close<first.open};score=(second.close-midpoint)/Math.max(a.body,tick);break;
case"D06-F03-A06":requiredContext="uptrend";checks={first_bullish:a.direction==="bullish",long_first:longFirst,second_bearish:b.direction==="bearish",gap_above_first_high:second.open>=first.high+tick,close_below_midpoint:second.close<midpoint,close_above_first_open:second.close>first.open};score=(midpoint-second.close)/Math.max(a.body,tick);break;
case"D06-F03-A07":requiredContext="uptrend";checks={first_bullish:a.direction==="bullish",second_bearish:b.direction==="bearish",highs_within_tolerance:Math.abs(first.high-second.high)<=tolerance,first_upper_rejection:a.upper_shadow>=.50*Math.max(a.body,tick),second_upper_rejection:b.upper_shadow>=.50*Math.max(b.body,tick)};score=Math.max(0,1-Math.abs(first.high-second.high)/Math.max(tolerance,tick));break;
case"D06-F03-A08":requiredContext="downtrend";checks={first_bearish:a.direction==="bearish",second_bullish:b.direction==="bullish",lows_within_tolerance:Math.abs(first.low-second.low)<=tolerance,first_lower_rejection:a.lower_shadow>=.50*Math.max(a.body,tick),second_lower_rejection:b.lower_shadow>=.50*Math.max(b.body,tick)};score=Math.max(0,1-Math.abs(first.low-second.low)/Math.max(tolerance,tick));break;
default:throw new RangeError(`unsupported two-candle topic ${topicId}`);
}
const result=patternResult({topicId,title:F03_TITLES[topicId],a:{body:b.body,upper_shadow:b.upper_shadow,lower_shadow:b.lower_shadow,range:b.range,direction:b.direction},matchedGeometry:Object.values(checks).every(Boolean),requiredContext,actualContext,bodyScale,rangeScale,historyCount:count,geometryScore:score,checks});
Object.assign(result,{first_direction:a.direction,second_direction:b.direction,first_body:a.body,second_body:b.body,first_body_low:a.body_low,first_body_high:a.body_high,second_body_low:b.body_low,second_body_high:b.body_high,first_high:first.high,second_high:second.high,first_low:first.low,second_low:second.low,midpoint,price_tolerance:tolerance});return result;
}
export function calculate(topicId:string,inputs:AnyMap):AnyMap{
if(!inputs||typeof inputs!=="object"||Array.isArray(inputs))throw new TypeError("inputs must be an object");
if(topicId==="D06-F01-A03")return shadowToBody(inputs);
if(topicId==="D06-F01-A04")return gapClassification(inputs);
if(topicId==="D06-F01-A05")return trendContext(inputs);
if(topicId in F02_TITLES)return singlePattern(topicId,inputs);
if(topicId in F03_TITLES)return twoPattern(topicId,inputs);
throw new RangeError(`unsupported topic id ${topicId}`);
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.