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 Strength and Quality Scoring. You will build a detector that can score definition strength transparently without relabeling the score as probability. The output is deliberately modest: an auditable geometry or workflow state. It is not a prediction or recommendation.
The decision made visible
How strong is the detected geometry after price distance, indicator distance, prominence, alignment lag, and separation are audited? 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 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
Q=100(0.28P+0.28I+0.18R+0.14A+0.12S)
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
The quality score summarizes definition strength after an event already exists. Five bounded components remain visible, weights sum to one, and the 0–100 output is never described as probability.
| Decision step | Canonical calculation | Observed result | Interpretation |
|---|---|---|---|
| Bounded components | P=1, I=1, R=0.7675, A=1, S=1 | five diagnostics | caps prevent one large leg from dominating |
| Weighted sum | 0.28+0.28+0.18×0.7675+0.14+0.12 | 0.95815 | weights sum to 1 |
| Display score | 100 × 0.95815 | 95.815 | quality label exceptional |
| Interpretation | 95.815 / 100 | definition strength | not a 95.815% chance of reversal |
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
A high quality score means the event strongly satisfies this chosen definition. It does not mean the market is likely to reverse, the trade has positive expected value, or the component weights are empirically optimal.
The boundary worth testing is equally concrete: Components are clipped to [0,1] before weighting. Weights must be finite, nonnegative, and sum to one within tolerance. A missing event returns no score; zero is not a substitute for missing geometry.
Compare before you combine
| Alternative | Contract | Best use | Do not confuse with |
|---|---|---|---|
| Binary detector | pass/fail thresholds | clear eligibility | does not rank definition strength |
| Transparent quality score | named bounded components | auditable triage | weights remain subjective |
| Calibrated probability | estimated outcome frequency | forecasting | requires a separate point-in-time empirical study |
Operational review checklist
- Require a valid event first
- Print every component
- Cap before weighting
- Validate weight sum
- Retain unrounded score
- Label score as definition strength
- Recalibrate only in a separate study
Evidence boundary. The score formula, component caps, weights, and quality bands are entirely package-selected. S10 is limitation-only: it warns that searching configurations can overfit; it does not validate this score or its predictive value.
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.
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:
{"topic_id": "D08-F05-A05", "state": "scored", "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": "moving-
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 Strength and Quality Scoring 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 Divergence Confirmation and Invalidation State Machine.
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. |
Rendered from the canonical Mermaid sources linked by this article.
Divergence Strength and Quality Scoring calculation flow
Takeaway: preserve clocks and geometry before aggregation.
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.
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: The score formula, component caps, weights, and quality bands are entirely package-selected. S10 is limitation-only: it warns that searching configurations can overfit; it does not validate this score or its predictive value.
- Selected contract:
Q=100(0.28P+0.28I+0.18R+0.14A+0.12S), with every component bounded and exposed.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.
Full dependency-light reference implementations in both supported languages.
/** 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}`);
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.