D06-F05-A01 / Editorial preview

Unified Candlestick Pattern Registry

A production-minded guide to Unified Candlestick Pattern Registry.

Unified Candlestick Pattern RegistryD06 / D06-F05

Candlestick scanners become difficult to trust when a pattern name is the only evidence they return. This tutorial builds the missing contract around one question: Which detector definition is allowed to run, under which stable identifier and version? By the end, you can construct a deterministic, versioned registry that rejects duplicate identities and ambiguous detector metadata and verify the same result in Python and TypeScript.

Begin with the decision, not the chart decoration

A registry is a controlled dispatch table. It freezes identity, direction, window length, implementation entry point, priority, version, and enabled state before a scan begins. TA-Lib’s official catalog demonstrates that many candlestick detectors can share an OHLC-array interface, while its API supports both whole-array and streaming execution (function catalog, core API). Our task is not to imitate undocumented vendor settings. It is to make the scanner boundary observable.

The selected formula is:

R=sortpattern_id(S),Rid=join(pattern_id@version)R=\operatorname{sort}_{pattern\_id}(S),\qquad R_{id}=\operatorname{join}(pattern\_id@version)

The comparison is inclusive, the remaining tie-breaks are stable, and unavailable evidence never becomes a convenient zero. Those are implementation choices. They make behavior testable; they do not prove a market edge.

Follow the data contract

The canonical input and output tables live in the topic README. Preserve source-qualified instrument identity, interval and session, price basis, detector version, event time, availability time, and closed-bar state. Binance’s official kline stream shows why finality must be explicit: its payload distinguishes an updating bar from a closed one (provider schema). The exact fields differ across providers, so adapt at ingestion and keep the algorithm provider-neutral.

What to notice: validation and time eligibility happen before the decision. A plausible pattern found with later information is still invalid for a point-in-time scan.

Unified Candlestick Pattern Registry worked decision

Open the accessible full-size diagram. The guided lab lets you step through canonical, equality-boundary, and rejected states with reduced-motion support.

Recalculate the fixture

Two specifications arrive out of order. Sorting yields D06-F02-A01 before D06-F03-A01; one disabled detector leaves enabled_count = 1 and max_window = 2. This is labeled synthetic teaching data. The stored expected values were frozen independently of either implementation; both languages must match them within 1e-9 for numbers and exactly for identifiers, arrays, booleans, reason codes, and states.

Implement one source of scanner semantics

The topic-specific Python and TypeScript entry points delegate to a shared D06-F05 family core. That architecture prevents nine packages from inventing nine versions of finite-number checks, inclusive boundaries, and error semantics. It does not hide the topic algorithm: each public wrapper names exactly one core function, and the canonical README freezes its behavior before derivatives.

Run the Python test with python tests/test_pattern_registry.py and the TypeScript test with node --test tests/pattern_registry.test.ts from the topic folder. The shared fixture is the cross-language oracle.

Interpret the result carefully

A detector can match geometry, the context can score highly, confirmation can occur, and the ranked alert can still lose money. Lo, Mamaysky, and Wang’s computational work is useful here because it separates systematic recognition from an empirical evaluation design (NBER Working Paper 7613). A production claim needs point-in-time universe membership, survivorship controls, costs, multiple-testing controls, out-of-sample data, and uncertainty. This tutorial claims only reproducible infrastructure behavior.

Failure checklist

  • Reject provisional terminal bars and future-known levels.
  • Do not mix raw and adjusted prices or incompatible session calendars.
  • Preserve warm-up, zero-scale, invalid, suppressed, and expired states.
  • Version definitions before changing thresholds or precedence.
  • Keep ranking scores distinct from probabilities and expected returns.
  • Retain reason codes so an operator can explain every accepted or rejected event.

What comes next

Continue through the related topics listed in the canonical package. Together the family moves from a stable detector registry to occurrence evidence, causal scanning, context, conflict handling, confirmation, and final ranked deduplicated output. Full sources and applicability boundaries are in REFERENCES.md.

Decision flow

Purpose: show the causal position of Unified Candlestick Pattern Registry.

Takeaway: the output records both the decision and the evidence boundary.

ReferencesPrimary sources and evidence notes

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

SRC-01 — TA-Lib function catalog and pattern-recognition group

  • Organization or authors: TA-Lib project
  • Source type: Official maintained function documentation
  • Publication or effective date: Continuously maintained
  • Version: Website state accessed 2026-08-01
  • URL or DOI: https://ta-lib.org/functions/
  • Accessed: 2026-08-01
  • Jurisdiction: Software documentation; not market regulation
  • Supports: Pattern recognition is a distinct function category; documented functions consume OHLC arrays and may be dispatched programmatically.
  • Limitations: TA-Lib's names and outputs do not define this package's registry metadata, thresholds, scores, conflict policy, or state machine.

SRC-02 — TA-Lib C/C++ Core API

  • Organization or authors: TA-Lib project
  • Source type: Official API documentation
  • Publication or effective date: Continuously maintained
  • Version: Website state accessed 2026-08-01
  • URL or DOI: https://ta-lib.org/api/
  • Accessed: 2026-08-01
  • Jurisdiction: Software documentation
  • Supports: The library distinguishes batch array processing, streaming, and dynamic abstraction-layer dispatch.
  • Limitations: This family is dependency-free and does not claim TA-Lib numerical parity.

SRC-03 — Binance Spot kline/candlestick stream

  • Organization or authors: Binance
  • Source type: Official provider API documentation
  • Publication or effective date: Continuously maintained
  • Version: Website state accessed 2026-08-01
  • URL or DOI: https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#klinecandlestick-streams-for-utc
  • Accessed: 2026-08-01
  • Jurisdiction: Provider-specific spot-market data
  • Supports: A provider kline message can expose open/close times, OHLC, volume, trade counts, and an explicit closed-bar flag.
  • Limitations: Field meaning, interval calendars, revision behavior, and licensing are provider-specific; the synthetic fixture is not Binance market data.

SRC-04 — CME Group chart types and support/resistance lessons

  • Organization or authors: CME Group
  • Source type: Official exchange education
  • Publication or effective date: Continuously maintained
  • Version: Website state 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: General market education
  • Supports: Candlesticks encode OHLC and practitioners commonly interpret them with trend, volume, support, and resistance context.
  • Limitations: Educational descriptions do not prescribe universal detector thresholds or validate forecasting value.

SRC-05 — Foundations of Technical Analysis

  • Organization or authors: Andrew W. Lo, Harry Mamaysky, and Jiang Wang
  • Source type: Original academic working paper and Journal of Finance article
  • Publication or effective date: 2000
  • Version: NBER Working Paper 7613
  • URL or DOI: https://doi.org/10.3386/w7613
  • Accessed: 2026-08-01
  • Jurisdiction: Empirical U.S. equities study for its declared historical sample
  • Supports: Subjective chart recognition can be translated into systematic computational rules; empirical usefulness requires a separate study design.
  • Limitations: It does not validate this family’s candlestick rules, weights, universe, or current predictive performance.

Evidence and licensing boundary

The external sources support data fields, software organization, common contextual concepts, and the need for systematic definitions. Every numeric weight, inclusive comparison, tie-break, expiry rule, and synthetic value in this package is an implementation choice or author-derived calculation. The fixtures are synthetic and CC0-1.0; no provider observations are redistributed.

pattern_registry.ts
/** Topic entry point for Unified Candlestick Pattern Registry. */
/** Reference algorithms for D06-F05. These are explicit teaching conventions,
 * not TA-Lib parity and not estimates of trading profitability. */

type AnyMap = Record<string, any>;
const PATTERN_ID = /^D06-F0[2-4]-A\d{2}$/;
const DIRECTIONS = new Set(["bullish", "bearish", "neutral"]);
const FEATURE_WEIGHTS: AnyMap = {geometry:.30,support_resistance:.20,trend:.15,volatility:.10,volume:.10,confirmation:.15};

function object(value:any,name:string):AnyMap { if(!value||typeof value!=="object"||Array.isArray(value))throw new TypeError(`${name} must be an object`);return value; }
function array(value:any,name:string):any[] { if(!Array.isArray(value))throw new TypeError(`${name} must be an array`);return value; }
function text(value:any,name:string):string { if(typeof value!=="string"||!value.trim())throw new TypeError(`${name} must be a non-empty string`);return value.trim(); }
function number(value:any,name:string):number { if(typeof value!=="number"||!Number.isFinite(value))throw new TypeError(`${name} must be a finite number`);return value; }
function integer(value:any,name:string,minimum?:number):number { if(!Number.isInteger(value))throw new TypeError(`${name} must be an integer`);if(minimum!==undefined&&value<minimum)throw new RangeError(`${name} must be at least ${minimum}`);return value; }
function unit(value:any,name:string):number { const n=number(value,name);if(n<0||n>1)throw new RangeError(`${name} must be in [0, 1]`);return n; }
function positive(value:any,name:string):number { const n=number(value,name);if(n<=0)throw new RangeError(`${name} must be positive`);return n; }
function direction(value:any,name="direction"):string { const d=text(value,name);if(!DIRECTIONS.has(d))throw new RangeError(`${name} must be bullish, bearish, or neutral`);return d; }
function clamp(value:number):number{return Math.max(0,Math.min(1,value));}
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; }

export function buildRegistry(data:AnyMap):AnyMap {
  const source=object(data,"data"),specs=array(source.patterns,"patterns"),ids=new Set<string>(),names=new Set<string>();
  const patterns=specs.map((raw,index)=>{const spec=object(raw,`patterns[${index}]`),patternId=text(spec.pattern_id,`patterns[${index}].pattern_id`),name=text(spec.name,`patterns[${index}].name`),folded=name.toLocaleLowerCase();
    if(!PATTERN_ID.test(patternId))throw new RangeError(`patterns[${index}].pattern_id is outside D06-F02 through D06-F04`);
    if(ids.has(patternId)||names.has(folded))throw new RangeError("pattern IDs and names must be unique");ids.add(patternId);names.add(folded);
    const window=integer(spec.window,`patterns[${index}].window`,1);if(window>5)throw new RangeError("pattern windows above five bars are outside this registry");
    const enabled=spec.enabled??true;if(typeof enabled!=="boolean")throw new TypeError(`patterns[${index}].enabled must be boolean`);
    return {pattern_id:patternId,name,direction:direction(spec.direction,`patterns[${index}].direction`),window,detector:text(spec.detector,`patterns[${index}].detector`),version:text(spec.version,`patterns[${index}].version`),enabled,priority:integer(spec.priority??0,`patterns[${index}].priority`,0)};
  }).sort((a,b)=>a.pattern_id.localeCompare(b.pattern_id));
  return {state:"ready",registry_id:patterns.map(p=>`${p.pattern_id}@${p.version}`).join(";"),pattern_count:patterns.length,enabled_count:patterns.filter(p=>p.enabled).length,max_window:patterns.length?Math.max(...patterns.map(p=>p.window)):0,patterns};
}

function makeOccurrence(data:AnyMap):AnyMap {
  const s=object(data,"data"),instrument=text(s.instrument_id,"instrument_id"),interval=text(s.interval,"interval"),patternId=text(s.pattern_id,"pattern_id");
  if(!PATTERN_ID.test(patternId))throw new RangeError("pattern_id is outside D06-F02 through D06-F04");
  const start=integer(s.start_index,"start_index",0),end=integer(s.end_index,"end_index",0);if(end<start)throw new RangeError("end_index cannot precede start_index");
  if(typeof s.bar_closed!=="boolean")throw new TypeError("bar_closed must be boolean");if(!s.bar_closed)throw new RangeError("canonical occurrences require a closed terminal bar");
  const detectedAt=text(s.detected_at,"detected_at"),availableAt=text(s.available_at,"available_at"),endTime=text(s.end_time,"end_time"),version=text(s.detector_version,"detector_version");
  return {occurrence_id:`${instrument}|${interval}|${endTime}|${patternId}|${start}:${end}|${version}`,instrument_id:instrument,interval,price_basis:text(s.price_basis,"price_basis"),session:text(s.session,"session"),pattern_id:patternId,pattern_name:text(s.pattern_name,"pattern_name"),direction:direction(s.direction),start_index:start,end_index:end,start_time:text(s.start_time,"start_time"),end_time:endTime,detected_at:detectedAt,available_at:availableAt,bar_closed:true,detector_version:version,geometry_score:unit(s.geometry_score,"geometry_score"),reason_codes:array(s.reason_codes??[],"reason_codes").map((v,i)=>text(v,`reason_codes[${i}]`))};
}

function scanMarket(data:AnyMap):AnyMap {
  const s=object(data,"data"),asOf=text(s.as_of,"as_of"),minimum=unit(s.minimum_geometry_score??0,"minimum_geometry_score"),registry=buildRegistry({patterns:s.registry}),enabled=new Map<string,AnyMap>(registry.patterns.filter((p:AnyMap)=>p.enabled).map((p:AnyMap)=>[p.pattern_id,p]));
  const occurrences:AnyMap[]=[],skipped:AnyMap[]=[],candidates=array(s.candidates,"candidates");
  candidates.forEach((raw,index)=>{const c=object(raw,`candidates[${index}]`),spec=enabled.get(c.pattern_id);if(!spec){skipped.push({candidate:String(index),reason:"disabled-or-unregistered"});return;}if(c.bar_closed!==true){skipped.push({candidate:String(index),reason:"provisional-terminal-bar"});return;}if(text(c.available_at,`candidates[${index}].available_at`)>asOf){skipped.push({candidate:String(index),reason:"future-at-scan-time"});return;}if(unit(c.geometry_score,`candidates[${index}].geometry_score`)<minimum){skipped.push({candidate:String(index),reason:"below-geometry-threshold"});return;}occurrences.push(makeOccurrence({...c,pattern_name:c.pattern_name??spec.name,detector_version:c.detector_version??spec.version,reason_codes:c.reason_codes??["geometry-matched"]}));});
  occurrences.sort((a,b)=>[a.instrument_id,a.interval,a.end_time,a.pattern_id].join("|").localeCompare([b.instrument_id,b.interval,b.end_time,b.pattern_id].join("|")));
  return {state:"ready",as_of:asOf,candidate_count:candidates.length,occurrence_count:occurrences.length,skipped_count:skipped.length,occurrences,skipped};
}

function scoreContext(data:AnyMap):AnyMap {
  const s=object(data,"data"),features=object(s.features,"features"),weights=object(s.weights??FEATURE_WEIGHTS,"weights"),normalized:AnyMap={};
  for(const [name,raw] of Object.entries(weights)){if(!(name in FEATURE_WEIGHTS))throw new RangeError(`unsupported context feature ${name}`);const w=number(raw,`weights.${name}`);if(w<0)throw new RangeError("weights must be non-negative");normalized[name]=w;}
  const total=Object.values(normalized).reduce((sum:any,v:any)=>sum+v,0) as number;if(total<=0)throw new RangeError("at least one context weight must be positive");let available=0,weighted=0;const contributions:AnyMap={};
  for(const [name,w] of Object.entries(normalized) as [string,number][]){const raw=features[name];if(raw===null||raw===undefined){contributions[name]=null;continue;}const v=unit(raw,`features.${name}`);available+=w;weighted+=w*v;contributions[name]=w*v;}
  const coverage=available/total,minimum=unit(s.minimum_coverage??.70,"minimum_coverage"),ready=available>0&&coverage>=minimum;
  return {state:ready?"ready":"insufficient-evidence",score:ready?100*weighted/available:null,evidence_coverage:coverage,minimum_coverage:minimum,contributions,interpretation:"context-fit-index-not-probability"};
}

function supportResistanceContext(data:AnyMap):AnyMap {
  const s=object(data,"data"),price=number(s.pattern_price,"pattern_price"),atr=positive(s.atr,"atr"),d=direction(s.direction);if(d==="neutral")return {state:"unsupported-direction",context_score:null,matched_level:null};
  const occurrenceIndex=integer(s.occurrence_index,"occurrence_index",0),maximum=positive(s.max_distance_atr??2,"max_distance_atr"),target=d==="bullish"?"support":"resistance",eligible:any[]=[];
  array(s.levels,"levels").forEach((raw,index)=>{const level=object(raw,`levels[${index}]`),kind=text(level.kind,`levels[${index}].kind`);if(!["support","resistance"].includes(kind))throw new RangeError("level kind must be support or resistance");const confirmed=integer(level.confirmed_index,`levels[${index}].confirmed_index`,0),levelPrice=number(level.price,`levels[${index}].price`),strength=unit(level.strength,`levels[${index}].strength`);if(kind===target&&confirmed<=occurrenceIndex)eligible.push({distance:Math.abs(price-levelPrice)/atr,level:{price:levelPrice,kind,strength,confirmed_index:confirmed}});});
  if(!eligible.length)return {state:"no-causal-level",context_score:null,matched_level:null,target_kind:target};eligible.sort((a,b)=>a.distance-b.distance||b.level.strength-a.level.strength||a.level.price-b.level.price);const best=eligible[0];
  return {state:"ready",target_kind:target,distance_atr:best.distance,context_score:clamp(1-best.distance/maximum),matched_level:best.level};
}

function trendVolatilityVolumeContext(data:AnyMap):AnyMap {
  const s=object(data,"data"),closes=array(s.prior_closes,"prior_closes").map((v,i)=>number(v,`prior_closes[${i}]`)),ranges=array(s.prior_ranges,"prior_ranges").map((v,i)=>number(v,`prior_ranges[${i}]`)),volumes=array(s.prior_volumes,"prior_volumes").map((v,i)=>number(v,`prior_volumes[${i}]`)),lookback=integer(s.lookback,"lookback",3),count=Math.min(closes.length,ranges.length,volumes.length);
  if(count<lookback)return {state:"warmup",history_count:count};const cs=closes.slice(-lookback),rs=ranges.slice(-lookback),vs=volumes.slice(-lookback);if([...rs,...vs].some(v=>v<0))throw new RangeError("ranges and volumes must be non-negative");const rangeScale=median(rs),volumeScale=median(vs);if(rangeScale<=0||volumeScale<=0)return {state:"zero-scale",range_scale:rangeScale,volume_scale:volumeScale};
  const expected=text(s.expected_prior_trend,"expected_prior_trend");if(!["uptrend","downtrend","any"].includes(expected))throw new RangeError("expected_prior_trend must be uptrend, downtrend, or any");const slope=(cs.at(-1)!-cs[0])/((lookback-1)*rangeScale),trendScore=expected==="any"?1:clamp(expected==="uptrend"?slope:-slope),volatilityRatio=number(s.current_range,"current_range")/rangeScale,volumeRatio=number(s.current_volume,"current_volume")/volumeScale;if(volatilityRatio<0)throw new RangeError("current_range must be non-negative");if(volumeRatio<0)throw new RangeError("current_volume must be non-negative");
  return {state:"ready",history_count:lookback,range_scale:rangeScale,volume_scale:volumeScale,normalized_slope:slope,trend_score:trendScore,volatility_ratio:volatilityRatio,volatility_score:clamp(1-Math.abs(volatilityRatio-1)/2),volume_ratio:volumeRatio,volume_score:clamp(volumeRatio/positive(s.target_volume_ratio??1.5,"target_volume_ratio"))};
}

function resolveConflicts(data:AnyMap):AnyMap {
  const s=object(data,"data"),rows=array(s.occurrences,"occurrences").map((raw,index)=>{const row={...object(raw,`occurrences[${index}]`)};for(const field of ["occurrence_id","instrument_id","interval","pattern_id"])row[field]=text(row[field],`occurrences[${index}].${field}`);row.direction=direction(row.direction,`occurrences[${index}].direction`);row.start_index=integer(row.start_index,`occurrences[${index}].start_index`,0);row.end_index=integer(row.end_index,`occurrences[${index}].end_index`,0);if(row.end_index<row.start_index)throw new RangeError("occurrence end cannot precede start");row.priority=integer(row.priority??0,`occurrences[${index}].priority`,0);row.confidence=number(row.confidence,`occurrences[${index}].confidence`);row.geometry_score=unit(row.geometry_score,`occurrences[${index}].geometry_score`);return row;}),groups=new Map<string,AnyMap[]>();
  for(const row of rows){const key=`${row.instrument_id}|${row.interval}`;groups.set(key,[...(groups.get(key)??[]),row]);}const winners:AnyMap[]=[],suppressed:AnyMap[]=[];
  for(const group of groups.values()){const ordered=[...group].sort((a,b)=>a.start_index-b.start_index||a.end_index-b.end_index||a.occurrence_id.localeCompare(b.occurrence_id)),components:AnyMap[][]=[];let current:AnyMap[]=[],currentEnd=-1;for(const row of ordered){if(current.length&&row.start_index>currentEnd){components.push(current);current=[];}current.push(row);currentEnd=Math.max(currentEnd,row.end_index);}if(current.length)components.push(current);components.forEach((component,componentIndex)=>{const ranked=[...component].sort((a,b)=>b.priority-a.priority||b.confidence-a.confidence||b.geometry_score-a.geometry_score||a.pattern_id.localeCompare(b.pattern_id)||a.occurrence_id.localeCompare(b.occurrence_id)),winner:AnyMap={...ranked[0],conflict_component:componentIndex,direction_conflict:new Set(component.map(r=>r.direction)).size>1};winners.push(winner);ranked.slice(1).forEach(loser=>suppressed.push({occurrence_id:loser.occurrence_id,winner_id:winner.occurrence_id,reason:"overlap-lower-precedence"}));});}
  winners.sort((a,b)=>[a.instrument_id,a.interval,String(a.start_index).padStart(12,"0"),a.occurrence_id].join("|").localeCompare([b.instrument_id,b.interval,String(b.start_index).padStart(12,"0"),b.occurrence_id].join("|")));suppressed.sort((a,b)=>a.occurrence_id.localeCompare(b.occurrence_id));return {state:"ready",winner_count:winners.length,suppressed_count:suppressed.length,winners,suppressed};
}

function runConfirmationStateMachine(data:AnyMap):AnyMap {
  const s=object(data,"data"),detectionIndex=integer(s.detection_index,"detection_index",0),d=direction(s.direction);if(d==="neutral")throw new RangeError("neutral candidates need a topic-specific state machine");const confirmation=number(s.confirmation_level,"confirmation_level"),invalidation=number(s.invalidation_level,"invalidation_level");if(d==="bullish"&&invalidation>=confirmation)throw new RangeError("bullish invalidation must be below confirmation");if(d==="bearish"&&invalidation<=confirmation)throw new RangeError("bearish invalidation must be above confirmation");const expires=integer(s.expires_after_bars,"expires_after_bars",1),transitions:AnyMap[]=[{bar_index:detectionIndex,from:"searching",to:"detected",reason:"geometry-matched"},{bar_index:detectionIndex,from:"detected",to:"awaiting-confirmation",reason:"levels-armed"}];let state="awaiting-confirmation",prior=detectionIndex;
  const events=array(s.events,"events");for(let eventIndex=0;eventIndex<events.length;eventIndex++){const event=object(events[eventIndex],`events[${eventIndex}]`),index=integer(event.bar_index,`events[${eventIndex}].bar_index`,detectionIndex+1);if(index<=prior)throw new RangeError("event bar indexes must be strictly increasing");prior=index;if(typeof event.bar_closed!=="boolean")throw new TypeError("event bar_closed must be boolean");if(!event.bar_closed){transitions.push({bar_index:index,from:state,to:state,reason:"provisional-bar-ignored"});continue;}const close=number(event.close,`events[${eventIndex}].close`),confirmed=d==="bullish"?close>=confirmation:close<=confirmation,invalidated=d==="bullish"?close<=invalidation:close>=invalidation;if(confirmed){transitions.push({bar_index:index,from:state,to:"confirmed",reason:"confirmation-close-crossed"});state="confirmed";break;}if(invalidated){transitions.push({bar_index:index,from:state,to:"invalidated",reason:"invalidation-close-crossed"});state="invalidated";break;}if(index>=detectionIndex+expires){transitions.push({bar_index:index,from:state,to:"expired",reason:"confirmation-window-ended"});state="expired";break;}transitions.push({bar_index:index,from:state,to:state,reason:"inside-active-band"});}
  return {state,direction:d,confirmation_level:confirmation,invalidation_level:invalidation,transitions,terminal:["confirmed","invalidated","expired"].includes(state)};
}

function rankAndDeduplicate(data:AnyMap):AnyMap {
  const s=object(data,"data"),halfLife=positive(s.recency_half_life_bars??5,"recency_half_life_bars"),dedupWindow=integer(s.dedup_window_bars??2,"dedup_window_bars",0),weights=object(s.weights??{confidence:.55,recency:.20,liquidity:.15,priority:.10},"weights"),names=["confidence","recency","liquidity","priority"];if(Object.keys(weights).sort().join("|")!==[...names].sort().join("|"))throw new RangeError("ranking weights must define confidence, recency, liquidity, and priority");const normalized:AnyMap={};for(const name of names)normalized[name]=number(weights[name],`weights.${name}`);const divisor=Object.values(normalized).reduce((sum:any,v:any)=>sum+v,0) as number;if(Object.values(normalized).some((v:any)=>v<0)||divisor<=0)throw new RangeError("ranking weights must be non-negative with a positive sum");
  const ranked=array(s.occurrences,"occurrences").map((raw,index)=>{const row={...object(raw,`occurrences[${index}]`)};for(const field of ["occurrence_id","instrument_id","interval","pattern_id","direction"])row[field]=text(row[field],`occurrences[${index}].${field}`);row.end_index=integer(row.end_index,`occurrences[${index}].end_index`,0);const confidence=number(row.confidence,`occurrences[${index}].confidence`);if(confidence<0||confidence>100)throw new RangeError("confidence must be in [0, 100]");const age=number(row.age_bars,`occurrences[${index}].age_bars`);if(age<0)throw new RangeError("age_bars must be non-negative");const components={confidence:confidence/100,recency:1/(1+age/halfLife),liquidity:unit(row.liquidity_score,`occurrences[${index}].liquidity_score`),priority:unit(row.priority_score,`occurrences[${index}].priority_score`)};row.rank_score=100*names.reduce((sum,name)=>sum+normalized[name]*components[name as keyof typeof components],0)/divisor;row.rank_components=components;return row;}).sort((a,b)=>b.rank_score-a.rank_score||a.occurrence_id.localeCompare(b.occurrence_id));
  const kept:AnyMap[]=[],suppressed:AnyMap[]=[];for(const row of ranked){const duplicate=kept.find(w=>w.instrument_id===row.instrument_id&&w.interval===row.interval&&w.pattern_id===row.pattern_id&&w.direction===row.direction&&Math.abs(w.end_index-row.end_index)<=dedupWindow);if(duplicate)suppressed.push({occurrence_id:row.occurrence_id,winner_id:duplicate.occurrence_id,reason:"same-pattern-within-dedup-window"});else kept.push(row);}kept.forEach((row,index)=>row.rank=index+1);return {state:"ready",ranked_count:kept.length,suppressed_count:suppressed.length,ranked:kept,suppressed};
}

const FUNCTIONS:AnyMap={"D06-F05-A01":buildRegistry,"D06-F05-A02":makeOccurrence,"D06-F05-A03":scanMarket,"D06-F05-A04":scoreContext,"D06-F05-A05":supportResistanceContext,"D06-F05-A06":trendVolatilityVolumeContext,"D06-F05-A07":resolveConflicts,"D06-F05-A08":runConfirmationStateMachine,"D06-F05-A09":rankAndDeduplicate};
function calculate(topicId:string,data:AnyMap):AnyMap { const fn=FUNCTIONS[topicId];if(!fn)throw new RangeError(`unsupported D06-F05 topic ${topicId}`);return fn(data); }
Full-height labplaygroundOpen full screen
Written by

Fintech engineer building market-data and financial systems, and the author of every article, glossary record, and reference implementation on The Fintech Builder.