Library/Geometric Chart Patterns/Indicator Divergence Detection/Divergence Confirmation and Invalidation State Machine

D08-F05-A06 / Complete engineering topic

Divergence Confirmation and Invalidation State Machine

A production-minded guide to Divergence Confirmation and Invalidation State Machine.

Divergence Confirmation and Invalidation State MachineD08 / D08-F05

The attractive part of divergence is the picture: two price extrema slope one way while a momentum line slopes another. The difficult part is everything the picture hides. Which extrema were eligible? Was the indicator extremum on the same bar? When did each pivot become confirmable? Did the chart draw a marker into the past? What happens when one indicator disagrees, or price invalidates the candidate before confirmation?

This tutorial answers those engineering questions for Divergence Confirmation and Invalidation State Machine. You will build a detector that can advance a divergence through searching, candidate, confirmed, invalidated, and expired states causally. The output is deliberately modest: an auditable geometry or workflow state. It is not a prediction or recommendation.

The decision made visible

After geometry becomes knowable, which later finalized bar confirms, invalidates, or expires the candidate? The selected contract starts from confirmed pivot events, not a retrospective line drawn after the whole chart is visible. Price and indicator pivots retain both event index and confirmation index. Alignment becomes knowable only at the later of those confirmations.

The aligned price and indicator geometry

The image uses synthetic data. Notice the solid extrema at their event locations and the knowledge-time label beside the later confirmation. That distinction is the foundation for every later score and state.

Intuition before notation

Imagine two price lows. If the second price low is materially lower but the aligned RSI low is materially higher, the selected contract calls the geometry regular bullish divergence. Mirror that construction at highs for regular bearish divergence. Hidden divergence reverses which series makes the higher or lower extremum and is kept in a separate family.

The word “materially” needs numbers. We use a fractional price threshold, a standardized indicator threshold, an inclusive maximum alignment lag, and a bounded pivot-pair separation. These are selected teaching defaults, not laws of markets.

Formula and equality policy

candidate→confirmed on strict structure break; candidate→invalidated on strict pivot breach; candidate→expired at deadline

Price delta is (P2-P1)/abs(P1). Indicator delta is calculated after I*=polarity×raw/scale. A positive standardized change always means the adapted indicator increased. Equality at the declared detection thresholds passes within a numerical tolerance of 1e-12. Lifecycle confirmation and invalidation use strict crossings so one bar cannot satisfy both at equality.

Work the actual decision, not just the picture

Geometry creates a candidate; only later finalized bars may confirm, invalidate, or expire it. Strict crossing and explicit precedence prevent equality and same-bar ambiguity.

Decision stepCanonical calculationObserved resultInterpretation
Candidatemax required confirmationsindex 45geometry first becomes knowable
Invalidation level94 × (1 - 0.005)93.53strict close below invalidates bullish candidate
Deadline45 + 1257expire if neither strict crossing occurs
Confirmationclose[47]=112 > 109.5372confirmed at 47close[46]=103 remains candidate

The arithmetic is author-derived from labeled synthetic inputs. It checks definition fidelity only. The detector uses unrounded values and preserves the exact event and knowledge clocks shown in the fixture.

Misconception clinic

Drawing the divergence at pivot event 42 does not make the candidate available at 42, and creating a candidate at 45 does not mean it is confirmed. Geometry time and lifecycle time are different clocks.

The boundary worth testing is equally concrete: A close exactly equal to a level does not cross it. Confirmation and invalidation are tested on finalized bars only. The package evaluates decisive strict crossings before deadline expiry and records the first terminal transition.

Compare before you combine

AlternativeContractBest useDo not confuse with
Candidategeometry knownmonitoring statenot yet confirmed
Confirmedstrict structure breakterminal positive diagnosticnot a trade order
Invalidatedstrict pivot-buffer breachterminal rejectiondoes not erase prior candidate history
Expireddeadline reached firstterminal no-resolution statenot equivalent to invalidated

Operational review checklist

  • Start at candidate knowledge time
  • Use finalized closes
  • Print confirmation/invalidation levels
  • Use strict comparisons
  • Apply deterministic precedence
  • Stop after first terminal state
  • Replay after source revision

Evidence boundary. S3 and S4 are central to finalized-bar and back-plotting discipline. The chosen structure level, buffer, horizon, strict comparison, and precedence are package-selected and must not be attributed to a platform default.

Build the causal pipeline

First validate finalized bars and pivot clocks. Then adapt each indicator without dropping raw values. For each price pivot, search unused same-kind indicator pivots inside the inclusive lag. Choose the smallest absolute event-time difference; if two candidates tie, choose the earlier indicator event. The alignment knowledge time is the maximum of both confirmation indexes.

Next form consecutive aligned same-kind pairs and apply the regular/hidden geometry. Only after geometry exists may downstream modules score it, wait for confirmation, combine indicators, or rank a universe. This order prevents a high score or ranking formula from manufacturing a missing divergence.

Plain text
confirmed pivots → declared adapters → causal alignment
→ divergence classification → score/state/confluence → as-of ranking

Worked synthetic example

The canonical fixture contains 72 repository-authored hourly bars. Confirmed price lows occur at event indexes 18 and 42. RSI low events occur at 19 and 41, so both align inside the three-bar lag. Their knowledge time is the later confirmation carried by each pair. The complete expected result begins:

JSON
{"topic_id": "D08-F05-A06", "state": "confirmed", "event": {"type": "regular-bullish", "direction": "bullish", "kind": "low", "indicator": "MACD-Histogram", "indicator_family": "moving-average-momentum", "price_pair": [18, 42], "indicator_pair": [18, 42], "price_values": [98.0, 94.0], "indicator_values": [-0.44000000000000006, -0.2], "price_delta_fraction": -0.04081632653061224, "indicator_delta": 0.24000000000000005, "separation": 24, "max_alignment_lag": 0, "prominence": 0.7675, "candidate_knowledge_index": 45, "alignment": [{"kind": "low", "price_event_index": 18, "price_confirmation_index": 21, "price": 98.0, "price_prominence": 0.72, "indicator_event_index": 18, "indicator_confirmation_index": 21, "indicator_value": -0.44000000000000006, "indicator_raw_value": -2.2, "indicator_prominence": 0.7, "lag": 0, "knowledge_index": 21, "indicator": "MACD-Histogram", "indicator_family": "movi

Use the complete expected-output file for machine comparison. The compact excerpt is not a substitute for the structured audit.

Seven experiments, three presets

The guided lab retains all 72 observations and recomputes 1,512 states: seven scenarios multiplied by three parameter presets. Start with the canonical fixture, step through the second pivot confirmation, and observe when the output changes. Then move to the exact equality case, reject an alignment outside the lag, compare hidden geometry, create mixed indicator evidence, invalidate a candidate, and remove the second pivot entirely.

Open the self-contained playground.

The strict preset requires larger price and indicator legs and a smaller alignment lag. The permissive preset does the opposite. The purpose is not to select the most signals; it is to expose how declared policy changes the output.

Implementation walkthrough

The Python and TypeScript implementations share fixtures but not calculation code. Both validate types, finite values, clocks, scales, weights, and state ranges. Both return structured warm-up or empty states rather than replacing missing evidence with zero. Both preserve raw and normalized indicator values so an adapter can be audited later.

Scoring uses named components: price geometry, indicator geometry, prominence, alignment, and separation. Confluence groups events only when divergence type and price-pivot pair match. Scanner ranking admits only records available at the as-of index and applies a stable symbol tie-break. None of these scores is calibrated probability.

Testing what matters

Canonical output is recursively compared across languages. Independent arithmetic checks do not call either implementation. Boundary tests cover equality, no alignment, insufficient history, invalid scale, lifecycle precedence, deterministic reset, and scanner ordering. Visual checks compare the displayed state with the same scenario-results fixture.

Definition tests answer whether code implements the selected rules. They do not answer whether the signal predicts returns. That would require a point-in-time dataset, parameter freeze, transaction costs, survivorship controls, multiple-testing correction, and genuinely out-of-sample evaluation.

Failure atlas

The most dangerous failure is temporal: a pivot appears on an old event bar but was not knowable until later. Other failures include mismatching a price low to an indicator high, comparing raw RSI points with raw MACD units, counting three highly correlated indicators as three independent confirmations, confirming with an intrabar value that later disappears, and ranking stale candidates above fresher confirmed records.

Strong trends also matter. Divergence can persist while price continues in the same direction. TradingView explicitly warns that divergence should not be used alone, and Fidelity notes MACD whipsaw in ranges. Treat detection as a structured observation requiring separate validation and risk policy.

Historical example: deferred

A named chart would be more vivid but less honest without the exact point-in-time data and implementation vintage. A future historical case must publish the security, venue, timeframe, session, adjustment basis, finalized bars, indicator source and parameters, pivot convention, confirmation clocks, revisions, and redistribution rights. Until then, synthetic fixtures provide stronger reproducibility.

What you can now hand downstream

You can hand off a structured Divergence Confirmation and Invalidation State Machine result with scope, formula version, thresholds, raw and adapted inputs, pivot identities, event and knowledge times, state, reason, and evidence boundary intact. The next tutorial is Multi-Indicator Divergence Confluence.

Primary references

| S1 | TradingView — RSI divergence indicator | Regular divergence terminology and explicit warning against standalone use. | | S2 | TradingView — MACD indicator | Regular versus hidden interpretation and MACD component definitions. | | S3 | TradingView Pine Script — Repainting | Future-leak, back-plotting, confirmed-bar, and revision risks. | | S4 | TradingView Pine Script — Visuals FAQ | Pivot confirmation delay and explicit state-machine guidance. | | S5 | Fidelity — Relative Strength Index | RSI formula and price/RSI divergence interpretation. | | S6 | Fidelity — MACD | MACD calculation, range behavior, whipsaw limitation, and divergence context. | | S7 | TA-Lib — Function index | Official indicator inventory, documented inputs/outputs, and implementation links. | | S8 | TA-Lib — RSI | RSI lookback, initial unstable period, input, and output contract. | | S9 | SciPy — find_peaks | Nearby retrospective peak, distance, prominence, width, plateau, and NaN conventions. | | S10 | Bailey et al. — Effects of Backtest Overfitting | Multiple-configuration selection raises overfitting risk; used only as a misuse boundary. |

Divergence Confirmation and Invalidation State Machine calculation flow

Rendering system map…

Takeaway: preserve clocks and geometry before aggregation.

ReferencesPrimary sources and evidence notes

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

The source ledger records what each source can support. Synthetic fixtures and author-derived arithmetic are not provider observations.

S1 — TradingView — RSI divergence indicator

  • Organization or authors: TradingView
  • Source type: official documentation, institutional education, or original research record
  • Publication or effective date: current page or recorded paper edition
  • Version: page retrieved on the access date
  • URL or DOI: https://www.tradingview.com/support/solutions/43000589127-rsi-divergence-indicator/
  • Accessed: 2026-08-03
  • Jurisdiction: educational and technical; not a trading rule
  • Supports: Regular divergence terminology and explicit warning against standalone use.
  • Limitations: Does not establish this package's thresholds, scoring weights, predictive value, or profitability. Those are explicitly selected implementation conventions.

S2 — TradingView — MACD indicator

  • Organization or authors: TradingView
  • Source type: official documentation, institutional education, or original research record
  • Publication or effective date: current page or recorded paper edition
  • Version: page retrieved on the access date
  • URL or DOI: https://www.tradingview.com/support/solutions/43000502344-moving-average-convergence-divergence-macd-indicator/
  • Accessed: 2026-08-03
  • Jurisdiction: educational and technical; not a trading rule
  • Supports: Regular versus hidden interpretation and MACD component definitions.
  • Limitations: Does not establish this package's thresholds, scoring weights, predictive value, or profitability. Those are explicitly selected implementation conventions.

S3 — TradingView Pine Script — Repainting

  • Organization or authors: TradingView Pine Script
  • Source type: official documentation, institutional education, or original research record
  • Publication or effective date: current page or recorded paper edition
  • Version: page retrieved on the access date
  • URL or DOI: https://www.tradingview.com/pine-script-docs/concepts/repainting/
  • Accessed: 2026-08-03
  • Jurisdiction: educational and technical; not a trading rule
  • Supports: Future-leak, back-plotting, confirmed-bar, and revision risks.
  • Limitations: Does not establish this package's thresholds, scoring weights, predictive value, or profitability. Those are explicitly selected implementation conventions.

S4 — TradingView Pine Script — Visuals FAQ

  • Organization or authors: TradingView Pine Script
  • Source type: official documentation, institutional education, or original research record
  • Publication or effective date: current page or recorded paper edition
  • Version: page retrieved on the access date
  • URL or DOI: https://www.tradingview.com/pine-script-docs/faq/visuals/
  • Accessed: 2026-08-03
  • Jurisdiction: educational and technical; not a trading rule
  • Supports: Pivot confirmation delay and explicit state-machine guidance.
  • Limitations: Does not establish this package's thresholds, scoring weights, predictive value, or profitability. Those are explicitly selected implementation conventions.

S5 — Fidelity — Relative Strength Index

  • Organization or authors: Fidelity
  • Source type: official documentation, institutional education, or original research record
  • Publication or effective date: current page or recorded paper edition
  • Version: page retrieved on the access date
  • URL or DOI: https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/RSI
  • Accessed: 2026-08-03
  • Jurisdiction: educational and technical; not a trading rule
  • Supports: RSI formula and price/RSI divergence interpretation.
  • Limitations: Does not establish this package's thresholds, scoring weights, predictive value, or profitability. Those are explicitly selected implementation conventions.

S6 — Fidelity — MACD

  • Organization or authors: Fidelity
  • Source type: official documentation, institutional education, or original research record
  • Publication or effective date: current page or recorded paper edition
  • Version: page retrieved on the access date
  • URL or DOI: https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/macd
  • Accessed: 2026-08-03
  • Jurisdiction: educational and technical; not a trading rule
  • Supports: MACD calculation, range behavior, whipsaw limitation, and divergence context.
  • Limitations: Does not establish this package's thresholds, scoring weights, predictive value, or profitability. Those are explicitly selected implementation conventions.

S7 — TA-Lib — Function index

  • Organization or authors: TA-Lib
  • Source type: official documentation, institutional education, or original research record
  • Publication or effective date: current page or recorded paper edition
  • Version: page retrieved on the access date
  • URL or DOI: https://ta-lib.org/functions/
  • Accessed: 2026-08-03
  • Jurisdiction: educational and technical; not a trading rule
  • Supports: Official indicator inventory, documented inputs/outputs, and implementation links.
  • Limitations: Does not establish this package's thresholds, scoring weights, predictive value, or profitability. Those are explicitly selected implementation conventions.

S8 — TA-Lib — RSI

  • Organization or authors: TA-Lib
  • Source type: official documentation, institutional education, or original research record
  • Publication or effective date: current page or recorded paper edition
  • Version: page retrieved on the access date
  • URL or DOI: https://ta-lib.org/functions/rsi.html
  • Accessed: 2026-08-03
  • Jurisdiction: educational and technical; not a trading rule
  • Supports: RSI lookback, initial unstable period, input, and output contract.
  • Limitations: Does not establish this package's thresholds, scoring weights, predictive value, or profitability. Those are explicitly selected implementation conventions.

S9 — SciPy — find_peaks

  • Organization or authors: SciPy
  • Source type: official documentation, institutional education, or original research record
  • Publication or effective date: current page or recorded paper edition
  • Version: page retrieved on the access date
  • URL or DOI: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html
  • Accessed: 2026-08-03
  • Jurisdiction: educational and technical; not a trading rule
  • Supports: Nearby retrospective peak, distance, prominence, width, plateau, and NaN conventions.
  • Limitations: Does not establish this package's thresholds, scoring weights, predictive value, or profitability. Those are explicitly selected implementation conventions.

S10 — Bailey et al. — Effects of Backtest Overfitting

  • Organization or authors: Bailey et al.
  • Source type: official documentation, institutional education, or original research record
  • Publication or effective date: current page or recorded paper edition
  • Version: page retrieved on the access date
  • URL or DOI: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2308659
  • Accessed: 2026-08-03
  • Jurisdiction: educational and technical; not a trading rule
  • Supports: Multiple-configuration selection raises overfitting risk; used only as a misuse boundary.
  • Limitations: Does not establish this package's thresholds, scoring weights, predictive value, or profitability. Those are explicitly selected implementation conventions.

Historical-example decision

Deferred. A named market case would require licensed point-in-time OHLC and indicator inputs, verified symbol and venue, exact interval/session and adjustment basis, parameter vintage, event and knowledge times, revision state, and redistribution permission. The package therefore uses deterministic synthetic data and makes no historical-performance claim.

Topic-specific applicability map

  • Central evidence: S3 and S4 are central to finalized-bar and back-plotting discipline. The chosen structure level, buffer, horizon, strict comparison, and precedence are package-selected and must not be attributed to a platform default.
  • Selected contract: Candidate → confirmed on strict structure break; → invalidated on strict pivot-buffer breach; → expired when the deadline arrives first. This rule is repository-authored and may have valid alternatives.
  • Synthetic/derived boundary: every printed price, indicator value, state, score, group, and symbol comes from repository-authored fixtures and deterministic arithmetic.
  • Historical decision: deferred until identity, point-in-time basis, exact implementation vintage, revision history, and redistribution rights are reproducible.
  • Unsupported inference: the cited material does not establish prediction, causation, profitability, calibration, or optimality for this package.
divergence.ts
/** Independent TypeScript reference for D08-F05. Geometry is diagnostic only. */

const EPS = 1e-12;
type Row = Record<string, any>;

function finite(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value); }
function requireRule(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); }

export function adaptIndicator(indicator: Row): Row {
  const name = String(indicator.name ?? "").trim();
  const family = String(indicator.family ?? "").trim();
  const polarity = indicator.polarity ?? 1;
  const scale = indicator.scale;
  const pivots = indicator.pivots;
  requireRule(name, "indicator name is required"); requireRule(family, "indicator family is required");
  requireRule(polarity === -1 || polarity === 1, "indicator polarity must be -1 or 1");
  requireRule(finite(scale) && scale > 0, "indicator scale must be finite and > 0");
  requireRule(Array.isArray(pivots), "indicator pivots must be a list");
  const adapted = pivots.map((pivot: Row, index: number) => {
    requireRule(pivot && typeof pivot === "object", `indicator pivot ${index} must be an object`);
    requireRule(pivot.kind === "high" || pivot.kind === "low", `indicator pivot ${index} kind is invalid`);
    for (const field of ["event_index", "confirmation_index"]) requireRule(Number.isInteger(pivot[field]), `indicator pivot ${index} ${field} is invalid`);
    requireRule(pivot.confirmation_index >= pivot.event_index, "indicator confirmation precedes event");
    requireRule(finite(pivot.value), `indicator pivot ${index} value is invalid`);
    const prominence = pivot.prominence ?? 0.5;
    requireRule(finite(prominence) && prominence >= 0 && prominence <= 1, "indicator prominence must be in [0,1]");
    return {kind:pivot.kind,event_index:pivot.event_index,confirmation_index:pivot.confirmation_index,raw_value:pivot.value,normalized_value:polarity*pivot.value/scale,prominence};
  });
  return {name,family,polarity,scale,pivots:adapted};
}

function validatePricePivots(pivots: Row[]): void {
  requireRule(Array.isArray(pivots), "price_pivots must be a list"); const identities = new Set<string>();
  pivots.forEach((pivot,index) => {
    requireRule(pivot && typeof pivot === "object", `price pivot ${index} must be an object`);
    requireRule(pivot.kind === "high" || pivot.kind === "low", `price pivot ${index} kind is invalid`);
    for (const field of ["event_index","confirmation_index"]) requireRule(Number.isInteger(pivot[field]), `price pivot ${index} ${field} is invalid`);
    requireRule(pivot.confirmation_index >= pivot.event_index, "price confirmation precedes event");
    requireRule(finite(pivot.price) && pivot.price > 0, "price pivot price must be finite and > 0");
    const prominence = pivot.prominence ?? 0.5; requireRule(finite(prominence) && prominence >= 0 && prominence <= 1, "price prominence must be in [0,1]");
    const key=`${pivot.kind}:${pivot.event_index}`; requireRule(!identities.has(key), "same-kind price pivot event indexes must be unique"); identities.add(key);
  });
}

export function alignPivots(pricePivots: Row[], indicator: Row, maxLag=3): Row[] {
  validatePricePivots(pricePivots); requireRule(Number.isInteger(maxLag) && maxLag >= 0, "max_lag must be an integer >= 0");
  const adapted=adaptIndicator(indicator); const used=new Set<number>(); const aligned:Row[]=[];
  const ordered=[...pricePivots].sort((a,b)=>a.event_index-b.event_index || String(a.kind).localeCompare(String(b.kind)));
  for(const price of ordered){
    const candidates=adapted.pivots.map((item:Row,position:number)=>({lag:Math.abs(item.event_index-price.event_index),event:item.event_index,position,item}))
      .filter((x:Row)=>!used.has(x.position)&&x.item.kind===price.kind&&x.lag<=maxLag)
      .sort((a:Row,b:Row)=>a.lag-b.lag||a.event-b.event);
    if(!candidates.length) continue; const chosen=candidates[0]; used.add(chosen.position); const item=chosen.item;
    aligned.push({kind:price.kind,price_event_index:price.event_index,price_confirmation_index:price.confirmation_index,price:Number(price.price),price_prominence:Number(price.prominence??0.5),indicator_event_index:item.event_index,indicator_confirmation_index:item.confirmation_index,indicator_value:item.normalized_value,indicator_raw_value:item.raw_value,indicator_prominence:item.prominence,lag:chosen.lag,knowledge_index:Math.max(price.confirmation_index,item.confirmation_index),indicator:adapted.name,indicator_family:adapted.family});
  }
  return aligned;
}

function classifyPair(first:Row,second:Row,minPrice:number,minIndicator:number):string|null{
  const pd=(second.price-first.price)/Math.abs(first.price); const id=second.indicator_value-first.indicator_value;
  if(first.kind==="low"){
    if(pd<=-minPrice+EPS&&id>=minIndicator-EPS)return "regular-bullish";
    if(pd>=minPrice-EPS&&id<=-minIndicator+EPS)return "hidden-bullish";
  }else{
    if(pd>=minPrice-EPS&&id<=-minIndicator+EPS)return "regular-bearish";
    if(pd<=-minPrice+EPS&&id>=minIndicator-EPS)return "hidden-bearish";
  }
  return null;
}

export function detectDivergences(pricePivots:Row[],indicators:Row[],options:Row={}):Row[]{
  requireRule(Array.isArray(indicators)&&indicators.length>0,"indicators must be a non-empty list");
  const maxLag=options.max_lag??3,minSeparation=options.min_separation??5,maxSeparation=options.max_separation??40,minPrice=options.min_price_fraction??0.005,minIndicator=options.min_indicator_delta??0.05;
  requireRule(Number.isInteger(minSeparation)&&minSeparation>=1,"min_separation must be an integer >= 1"); requireRule(Number.isInteger(maxSeparation)&&maxSeparation>=minSeparation,"max_separation must be >= min_separation");
  requireRule(finite(minPrice)&&minPrice>=0,"min_price_fraction must be finite and >= 0"); requireRule(finite(minIndicator)&&minIndicator>=0,"min_indicator_delta must be finite and >= 0");
  const events:Row[]=[];
  for(const indicator of indicators){ const aligned=alignPivots(pricePivots,indicator,maxLag);
    for(const kind of ["low","high"]){const same=aligned.filter(x=>x.kind===kind); for(let j=0;j+1<same.length;j++){const first=same[j],second=same[j+1],span=second.price_event_index-first.price_event_index;if(span<minSeparation||span>maxSeparation)continue;const type=classifyPair(first,second,minPrice,minIndicator);if(!type)continue;const pd=(second.price-first.price)/Math.abs(first.price),id=second.indicator_value-first.indicator_value;
      events.push({type,direction:kind==="low"?"bullish":"bearish",kind,indicator:first.indicator,indicator_family:first.indicator_family,price_pair:[first.price_event_index,second.price_event_index],indicator_pair:[first.indicator_event_index,second.indicator_event_index],price_values:[first.price,second.price],indicator_values:[first.indicator_value,second.indicator_value],price_delta_fraction:pd,indicator_delta:id,separation:span,max_alignment_lag:Math.max(first.lag,second.lag),prominence:(first.price_prominence+second.price_prominence+first.indicator_prominence+second.indicator_prominence)/4,candidate_knowledge_index:Math.max(first.knowledge_index,second.knowledge_index),alignment:[first,second]});
    }}
  }
  return events.sort((a,b)=>a.candidate_knowledge_index-b.candidate_knowledge_index||String(a.type).localeCompare(String(b.type))||String(a.indicator).localeCompare(String(b.indicator)));
}

export function scoreDivergence(event:Row,maxLag=3):Row{
  requireRule(event&&typeof event==="object","event must be an object");
  const price=Math.min(Math.abs(event.price_delta_fraction)/0.04,1),indicator=Math.min(Math.abs(event.indicator_delta)/0.15,1),prominence=Math.min(Math.max(event.prominence,0),1),alignment=maxLag===0&&event.max_alignment_lag===0?1:Math.max(0,1-event.max_alignment_lag/Math.max(maxLag,1)),separation=Math.max(0,1-Math.abs(event.separation-24)/24);
  const score=Number((100*(.28*price+.28*indicator+.18*prominence+.14*alignment+.12*separation)).toFixed(6));
  const quality=score>=85?"exceptional":score>=70?"strong":score>=50?"moderate":"weak";
  const round=(x:number)=>Number(x.toFixed(6));
  return {score,quality,components:{price_geometry:round(price),indicator_geometry:round(indicator),prominence:round(prominence),alignment:round(alignment),separation:round(separation)},interpretation:"definition-strength score; not probability or expected return"};
}

export function advanceState(event:Row,bars:Row[],confirmationHorizon=12,invalidationBuffer=.005):Row{
  requireRule(Array.isArray(bars)&&bars.length>0,"bars must be a non-empty list"); requireRule(Number.isInteger(confirmationHorizon)&&confirmationHorizon>=1,"confirmation_horizon must be an integer >= 1"); requireRule(finite(invalidationBuffer)&&invalidationBuffer>=0&&invalidationBuffer<1,"invalidation_buffer must be in [0,1)");
  bars.forEach((bar,index)=>requireRule(bar&&finite(bar.close)&&finite(bar.high)&&finite(bar.low),`bar ${index} is invalid`));
  const [first,second]=event.price_pair,candidate=event.candidate_knowledge_index,direction=event.direction;
  const slice=bars.slice(first,second+1); const confirmation=direction==="bullish"?Math.max(...slice.map(x=>x.high)):Math.min(...slice.map(x=>x.low)); const invalidation=event.price_values[1]*(direction==="bullish"?1-invalidationBuffer:1+invalidationBuffer); const deadline=candidate+confirmationHorizon; const trace:Row[]=[]; let state="candidate",finalIndex:number|null=null;
  for(let index=candidate;index<Math.min(bars.length,deadline+1);index++){const bar=bars[index];if(direction==="bullish"&&bar.low<invalidation-EPS){state="invalidated";finalIndex=index}else if(direction==="bearish"&&bar.high>invalidation+EPS){state="invalidated";finalIndex=index}else if(direction==="bullish"&&bar.close>confirmation+EPS){state="confirmed";finalIndex=index}else if(direction==="bearish"&&bar.close<confirmation-EPS){state="confirmed";finalIndex=index}trace.push({index,state,close:bar.close});if(finalIndex!==null)break;}
  if(finalIndex===null&&bars.length-1>=deadline){state="expired";finalIndex=deadline;if(trace.length)trace[trace.length-1].state="expired"}
  return {state,candidate_index:candidate,final_index:finalIndex,confirmation_level:Number(confirmation.toFixed(6)),invalidation_level:Number(invalidation.toFixed(6)),deadline,trace};
}

export function combineConfluence(events:Row[],weights:Record<string,number>,minimumIndicators=2):Row[]{
  requireRule(Array.isArray(events),"events must be a list");requireRule(weights&&Object.keys(weights).length>0,"weights must be a non-empty object");requireRule(Number.isInteger(minimumIndicators)&&minimumIndicators>=1,"minimum_indicators must be >= 1");const total=Object.values(weights).reduce((a,b)=>a+b,0);requireRule(total>0&&Object.values(weights).every(x=>finite(x)&&x>=0),"weights must be finite, non-negative, and sum > 0");
  const groups=new Map<string,Row[]>();for(const event of events){const key=`${event.type}|${event.price_pair.join(",")}`;groups.set(key,[...(groups.get(key)??[]),event]);}const out:Row[]=[];
  for(const group of groups.values()){const unique=new Map(group.map(x=>[x.indicator,x]));if(unique.size<minimumIndicators)continue;const selected=[...unique.values()],selectedWeight=selected.reduce((sum,x)=>sum+(weights[x.indicator]??0),0);if(selectedWeight<=0)continue;const quality=selected.reduce((sum,x)=>sum+(weights[x.indicator]??0)*scoreDivergence(x).score,0)/selectedWeight,coverage=selectedWeight/total,families=new Set(selected.map(x=>x.indicator_family)).size/selected.length,score=Number((.45*quality+35*coverage+20*families).toFixed(6));out.push({type:selected[0].type,direction:selected[0].direction,price_pair:selected[0].price_pair,indicators:[...unique.keys()].sort(),indicator_count:unique.size,weighted_quality:Number(quality.toFixed(6)),weight_coverage:Number(coverage.toFixed(6)),family_coverage:Number(families.toFixed(6)),confluence_score:score,knowledge_index:Math.max(...selected.map(x=>x.candidate_knowledge_index)),interpretation:"agreement score; not independent evidence or probability"});}
  return out.sort((a,b)=>b.confluence_score-a.confluence_score||String(a.type).localeCompare(String(b.type))||String(a.price_pair).localeCompare(String(b.price_pair)));
}

export function rankUniverse(records:Row[],asOfIndex:number):Row[]{
  requireRule(Array.isArray(records),"records must be a list");requireRule(Number.isInteger(asOfIndex)&&asOfIndex>=0,"as_of_index must be an integer >= 0");const ranked:Row[]=[];
  for(const record of records){if((record.available_index??0)>asOfIndex||record.eligible===false)continue;for(const field of ["confluence_score","freshness","liquidity","data_quality"])requireRule(finite(record[field])&&record[field]>=0&&record[field]<=100,`${field} must be in [0,100]`);requireRule(record.state==="candidate"||record.state==="confirmed","scanner state must be candidate or confirmed");const state=record.state==="confirmed"?100:70;const rank=Number((.55*record.confluence_score+.15*state+.1*record.freshness+.1*record.liquidity+.1*record.data_quality).toFixed(6));ranked.push({symbol:String(record.symbol),state:record.state,divergence_type:record.divergence_type,rank_score:rank,components:{confluence_score:record.confluence_score,freshness:record.freshness,liquidity:record.liquidity,data_quality:record.data_quality},as_of_index:asOfIndex,interpretation:"deterministic triage priority; not an order or return forecast"});}
  ranked.sort((a,b)=>b.rank_score-a.rank_score||a.symbol.localeCompare(b.symbol));ranked.forEach((x,i)=>x.rank=i+1);return ranked;
}

export function runTopic(payload:Row,topicId?:string):Row{
  const selected=topicId??String(payload.topic_id??""),p=payload.parameters??{},price=payload.price_pivots??[],indicators=payload.indicators??[],maxLag=p.max_lag??3;
  if(selected.endsWith("A01")){if(!indicators.length)return{topic_id:selected,state:"warmup",alignments:[],reason:"no-indicator-pivots"};const alignments=alignPivots(price,indicators[0],maxLag);return{topic_id:selected,state:alignments.length>=2?"ready":"warmup",alignments,alignment_count:alignments.length,reason:alignments.length>=2?"aligned":"fewer-than-two-alignments"};}
  if(selected.endsWith("A04")){const adapters=indicators.map(adaptIndicator);return{topic_id:selected,state:adapters.length?"ready":"warmup",adapters,adapter_count:adapters.length};}
  const events=indicators.length?detectDivergences(price,indicators,p):[];
  if(selected.endsWith("A02")){const filtered=events.filter(x=>x.type.startsWith("regular-"));return{topic_id:selected,state:filtered.length?"detected":"not-detected",events:filtered,event_count:filtered.length};}
  if(selected.endsWith("A03")){const filtered=events.filter(x=>x.type.startsWith("hidden-"));return{topic_id:selected,state:filtered.length?"detected":"not-detected",events:filtered,event_count:filtered.length};}
  if(selected.endsWith("A05")){if(!events.length)return{topic_id:selected,state:"not-detected",score:null,reason:"no-divergence-event"};return{topic_id:selected,state:"scored",event:events[0],score:scoreDivergence(events[0],maxLag)};}
  if(selected.endsWith("A06")){if(!events.length)return{topic_id:selected,state:"searching",event:null,lifecycle:null};const event=events[0],lifecycle=advanceState(event,payload.bars??[],p.confirmation_horizon??12,p.invalidation_buffer??.005);return{topic_id:selected,state:lifecycle.state,event,lifecycle};}
  if(selected.endsWith("A07")){const groups=combineConfluence(events,payload.weights??{},p.minimum_indicators??2);return{topic_id:selected,state:groups.length?"confluent":"insufficient-confluence",groups,group_count:groups.length};}
  if(selected.endsWith("A08")){const ranking=rankUniverse(payload.universe??[],payload.as_of_index??((payload.bars??[]).length-1));return{topic_id:selected,state:ranking.length?"ranked":"empty",ranking,eligible_count:ranking.length};}
  throw new Error(`unsupported topic id: ${selected}`);
}
Full-height labplaygroundOpen full screen