Library/Geometric Chart Patterns/Level Confluence and Zone Scoring/Market-Wide Zone-Proximity Scanner and Ranking

D08-F06-A09 / Complete engineering topic

Market-Wide Zone-Proximity Scanner and Ranking

A production-minded guide to Market-Wide Zone-Proximity Scanner and Ranking.

Market-Wide Zone-Proximity Scanner and RankingD08 / D08-F06

Rank a synthetic market snapshot reproducibly without leaking future zone updates or disguising a watchlist as advice.

The decision this tutorial makes visible

For market-wide monitoring systems, ranking is trustworthy only when universe membership, current prices, zone clocks, distance filters, score components, and deterministic ties are retained on every row.

The precise question is: Which point-in-time instruments are closest to eligible zones after proximity, strength, and freshness are combined transparently?

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

Intuition before notation

A scanner compresses many comparable zone records into an inspection queue; every row must retain the values that earned its rank.

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

Scope and nearby methods

One current price and one eligible zone per instrument, 24 cross-sectional records, a maximum distance filter, half-life freshness, and deterministic ranking.

VariantDefinitionBest useMain limitation
Canonical repository conventionOne current price and one eligible zone per instrument, 24 cross-sectional records, a maximum distance filter, half-life freshness, and deterministic ranking.Reproducible teaching and reference implementationRequires calibration before empirical use
Platform-specific implementationVendor-selected defaults, data resolution, and tiesParity with one named platformNot portable without a parity specification
Discretionary chart annotationHuman-selected levels and contextExploratory reviewNot reproducible without a written rubric

What is sourced, selected, synthetic, and derived

RoleMaterial claimEvidenceBoundary
Primary computational technical-analysis researchShows why subjective chart concepts require explicit computational definitions and separates pattern recognition from statistical evaluation.S1 exact source recordIt does not define this family's zone algorithms or establish current out-of-sample economic value.
Primary empirical researchReports clustering of requested execution rates at round numbers in the studied FX order data and connects those clusters with support/resistance practice.S2 exact source recordIt does not validate a universal grid, every asset class, future reactions, or this package's salience weights.
Sourced nearby methodologyDefines POC as the highest-volume price row, value area as a configured share of volume, and HVN/LVN as local peaks and valleys; also documents one deterministic value-area expansion convention.S3 exact source recordThe repository uses exact trade bins and explicitly frozen tie and overshoot rules; it does not claim TradingView output parity.
Session and data-boundary referenceDocuments that session choice, volume mode, row layout, row size, and value-area percentage affect a session volume profile.S4 exact source recordIt does not define this package's exact-trade bin contract or cross-platform parity.
Implementation choiceOne current price and one eligible zone per instrument, 24 cross-sectional records, a maximum distance filter, half-life freshness, and deterministic ranking.Frozen definition contract, formula, fixtures, and parity testsThis is the package convention, not a universal definition.
Synthetic teaching inputEvery displayed price, trade, profile, touch, bar, zone, and instrument snapshot is repository-authored.datasets/canonical-input.json and scenario-results.jsonNo value is presented as an observed security or provider record.
Author-derived calculationThe canonical fixture produces an auditable ordered watchlist of eligible synthetic instruments with proximity, strength, freshness, and score.Formula, expected output, examples, and cross-language parityCalculation correctness does not establish predictive or economic value.

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

Formula, symbols, and numerical policy

Plain text
rank_score = 100(0.55 proximity + 0.30 strength/100 + 0.15 freshness)
SymbolMeaningUnitPolicy
d_izone distancebpszero inside
P_iproximity componentratiolinear to cutoff
S_izone strength0–100upstream diagnostic
F_ifreshnessratiohalf-life decay
R_irank score0–100weighted sum
  • Prices and tolerances share one currency, adjustment basis, and tick table.
  • records observed after as_of are invalid rather than silently used.
  • Published values use full-precision calculation and explicit display rounding; no hidden epsilon changes a decision.
  • Ties are broken deterministically and preserved in the audit output.

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

Build the algorithm

  1. Validate as_of, unique instrument IDs, positive prices, zones, strengths, and observation clocks
  2. Compute zero distance inside each zone and edge distance outside
  3. Convert distance to bps and apply the eligibility radius
  4. Decay freshness and calculate the three-component score
  5. Sort by score, distance, and instrument ID while retaining the full evidence row

Production-minded operational checklist

  1. Persist universe version, as_of, observed_at, distance, component inputs, score, and deterministic tie fields for every ranked row.
  2. Partition by instrument, venue/session, currency, interval, and adjustment basis.
  3. Persist code version, source records, inputs, output diagnostics, and tie decisions.
  4. Evaluate empirical association separately with costs, leakage controls, and out-of-sample data.

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

Worked synthetic example

The canonical fixture is synthetic teaching data, not an observed control event, customer order, or broker execution. Its primary author-derived output, ranked, is an auditable ordered watchlist of eligible synthetic instruments with proximity, strength, freshness, and score. The complete input and output are in datasets/canonical-input.json and datasets/expected-output.json.

The labeled synthetic canonical fixture applies rank_score = 100(0.55 proximity + 0.30 strength/100 + 0.15 freshness) and returns an auditable ordered watchlist of eligible synthetic instruments with proximity, strength, freshness, and score. The expected JSON is generated once from the frozen contract and independently checked in both language implementations.

Counterfactual checkpoint

Market-Wide Zone-Proximity Scanner and Ranking boundary case. A stale strong zone can rank below a fresh moderate zone because freshness is an explicit component. The output changes because Reproducibility requires counterfactual behavior at the boundary.

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

Boundary and counterexample workbook

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

ScenarioReview focusPurposeStatePrimary outputDiagnosticDecision segments
Canonical leader-strength sweepStep 30 · canonical fixtureMove canonical leader SYN24 through a strength range while retaining the exact midpoint fixture.calculatedSYN24 · score 94.14 · 24 eligibleState calculated; SYN24 · score 94.14 · 24 eligible.1
Eligibility-cutoff equalityStep 30 · comparison focusMove SYN24 through the exact 40 bps maximum-distance boundary.calculatedno eligible instrumentsState calculated; no eligible instruments.1
Sparse six-instrument universeStep 30 · comparison focusRank a valid point-in-time subset and vary its leading candidate.calculatedSYN06 · score 93.24 · 6 eligibleState calculated; SYN06 · score 93.24 · 6 eligible.1
Freshness half-life sensitivityStep 30 · comparison focusChange the freshness half-life while holding observations and prices fixed.calculatedSYN24 · score 95.99 · 24 eligibleState calculated; SYN24 · score 95.99 · 24 eligible.1
Stale former leaderStep 30 · comparison focusAge SYN24 by one day and vary its strength to expose rank turnover.calculatedSYN23 · score 86.36 · 2 eligibleState calculated; SYN23 · score 86.36 · 2 eligible.1
Empty eligible universeStep 30 · comparison focusShift every price far outside its zone so the distance filter returns a valid empty result.calculatedno eligible instrumentsState calculated; no eligible instruments.1
Cross-sectional strength turnoverStep 30 · comparison focusMove SYN24 strength through the range where another instrument becomes leader.calculatedSYN18 · score 93.84 · 24 eligibleState calculated; SYN18 · score 93.84 · 24 eligible.1

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

Visualize the boundary

Market-Wide Zone-Proximity Scanner and Ranking annotated teaching map

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

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

Implementation walkthrough

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

The main implementation branches are:

  • Input or clock contract fails — Reject, because A precise output would be misleading.
  • records observed after as_of are invalid rather than silently used — Apply the declared equality/tie policy, because Boundary behavior must be reproducible.
  • Valid evidence does not satisfy the algorithm — Return a valid rejection, empty set, or nonconfirmed state, because Absence is different from invalid data.
  • All required conditions pass — Return the structured result and diagnostics, because The frozen contract is satisfied.

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

Testing and validation

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

The audit must preserve these invariants:

  • Return a named state rather than a bare scalar when decisions are sequential.
  • Preserve source IDs, members, expansion order, component scores, or state transitions where applicable.
  • Keep point-in-time cutoffs and selected parameters beside the output.
  • Separate invalid input from a valid nonmatch, rejection, or empty eligible result.

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

Failure modes and misuse

  • The synthetic universe avoids survivorship and licensing claims; a production scanner needs historical universe membership and source-specific revision handling.
  • Platform and analyst conventions can produce different results from the same chart.
  • A recognized level interaction is not evidence of future association or economic value.
  • The output is educational analytics, not investment advice or an order instruction.

Debugging order

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

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

Evidence and historical boundary

Historical decision: not useful. A named chart occurrence would add narrative appeal without improving reproducibility. Classification changes with provider, session, adjustment basis, tick table, anchors, thresholds, and the point-in-time record available to the detector.

The primary sources are Lo–Mamaysky–Wang, New York Fed round-number evidence, TradingView volume-profile concepts, TradingView session profile. They support the source roles listed in the research ledger, not a redistributable historical observation, a private participant decision, production conformance certification, execution-quality result, profitability claim, or prediction claim.

Summary and next topic

You can now implement, inspect, and validate Market-Wide Zone-Proximity Scanner and Ranking with its full topic-specific audit evidence. The learning flow is: Breakout and Retest Detection → Market-Wide Zone-Proximity Scanner and Ranking → ACF. Carry the result forward only with its scope, clock, state, and evidence label.

Market-Wide Zone-Proximity Scanner and Ranking calculation flow

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

Rendering system map…

Takeaway: A rank is an inspection priority backed by proximity, strength, freshness, and clock evidence—not a portfolio recommendation.

ReferencesPrimary sources and evidence notes

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

S1 — Foundations of Technical Analysis: Computational Algorithms, Statistical Inference, and Empirical Implementation

  • Organization or authors: Andrew W. Lo, Harry Mamaysky, and Jiang Wang
  • Source type: Original NBER working paper and Journal of Finance study
  • Publication or effective date: Working Paper 7613, March 2000; accessed 2026-08-03
  • Version: NBER Working Paper 7613
  • URL or DOI: https://www.nber.org/papers/w7613
  • Accessed: 2026-08-03
  • Jurisdiction: U.S. equity sample described by the paper
  • Supports: Shows why subjective chart concepts require explicit computational definitions and separates pattern recognition from statistical evaluation.
  • Limitations: It does not define this family's zone algorithms or establish current out-of-sample economic value.

S2 — Currency Orders and Exchange-Rate Dynamics: Explaining the Success of Technical Analysis

  • Organization or authors: Carol L. Osler
  • Source type: Federal Reserve Bank of New York Staff Report 125
  • Publication or effective date: Published April 2001; accessed 2026-08-03
  • Version: Staff Report 125
  • URL or DOI: https://www.newyorkfed.org/medialibrary/media/research/staff_reports/sr125.html
  • Accessed: 2026-08-03
  • Jurisdiction: Foreign-exchange dealer order data studied in the paper
  • Supports: Reports clustering of requested execution rates at round numbers in the studied FX order data and connects those clusters with support/resistance practice.
  • Limitations: It does not validate a universal grid, every asset class, future reactions, or this package's salience weights.

S3 — Volume profile indicators: basic concepts

  • Organization or authors: TradingView
  • Source type: Official maintained platform documentation
  • Publication or effective date: Current page accessed 2026-08-03
  • Version: Online documentation
  • URL or DOI: https://www.tradingview.com/support/solutions/43000502040-volume-profile-indicators-basic-concepts/
  • Accessed: 2026-08-03
  • Jurisdiction: Platform convention; not market-specific
  • Supports: Defines POC as the highest-volume price row, value area as a configured share of volume, and HVN/LVN as local peaks and valleys; also documents one deterministic value-area expansion convention.
  • Limitations: The repository uses exact trade bins and explicitly frozen tie and overshoot rules; it does not claim TradingView output parity.

S4 — Session volume profile charts explained

  • Organization or authors: TradingView
  • Source type: Official maintained platform documentation
  • Publication or effective date: Current page accessed 2026-08-03
  • Version: Online documentation
  • URL or DOI: https://www.tradingview.com/support/solutions/43000745275-session-volume-profile-charts-explained/
  • Accessed: 2026-08-03
  • Jurisdiction: Platform convention; session configurable
  • Supports: Documents that session choice, volume mode, row layout, row size, and value-area percentage affect a session volume profile.
  • Limitations: It does not define this package's exact-trade bin contract or cross-platform parity.

Evidence boundary

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

level-confluence.ts
/** Reference algorithms for D08-F06. Repository teaching conventions only. */

type Row = Record<string, any>;

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

function integerValue(value: unknown, name: string, minimum = 0): number {
  const result = numberValue(value, name);
  if (!Number.isInteger(result) || result < minimum) throw new RangeError(`${name} must be an integer >= ${minimum}`);
  return result;
}

function timeValue(value: unknown, name: string): number {
  if (typeof value !== "string" || !/(Z|[+-]\d\d:\d\d)$/.test(value)) throw new TypeError(`${name} must be a zoned ISO-8601 string`);
  const result = Date.parse(value);
  if (!Number.isFinite(result)) throw new RangeError(`${name} must be a zoned ISO-8601 string`);
  return result;
}

function rounded(value: number): number { return Number(value.toFixed(12)); }

function alignedTicks(price: unknown, tick: number, name: string): number {
  const value = numberValue(price, name);
  const ticks = Math.round(value / tick);
  if (Math.abs(value - ticks * tick) > tick * 1e-8) throw new RangeError(`${name} must align to tick_size`);
  return ticks;
}

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

function profile(input: Row): Row {
  const tick = numberValue(input.tick_size, "tick_size", 1e-12);
  const widthTicks = integerValue(input.bin_size_ticks, "bin_size_ticks", 1);
  const windowEnd = timeValue(input.window_end, "window_end");
  if (!Array.isArray(input.trades) || input.trades.length === 0) throw new RangeError("trades must be non-empty");
  const seen = new Set<string>();
  const volumes = new Map<number, number>();
  let previous: number | null = null, total = 0, eligible = 0;
  input.trades.forEach((trade: Row, index: number) => {
    if (!trade || typeof trade !== "object") throw new TypeError("each trade must be an object");
    if (typeof trade.trade_id !== "string" || !trade.trade_id || seen.has(trade.trade_id)) throw new RangeError("trade_id must be unique");
    seen.add(trade.trade_id);
    if (trade.final !== true) throw new RangeError("all trades must be final");
    const timestamp = timeValue(trade.timestamp, `trades[${index}].timestamp`);
    if (timestamp > windowEnd || (previous !== null && timestamp < previous)) throw new RangeError("trades must be ordered and available");
    previous = timestamp;
    const priceTicks = alignedTicks(trade.price, tick, `trades[${index}].price`);
    const volume = numberValue(trade.volume, `trades[${index}].volume`, 0);
    if (volume === 0) return;
    eligible += 1; total += volume;
    const binIndex = Math.floor(priceTicks / widthTicks);
    volumes.set(binIndex, (volumes.get(binIndex) ?? 0) + volume);
  });
  if (total <= 0) throw new RangeError("eligible volume must be positive");
  const rows = [...volumes.keys()].sort((a, b) => a - b).map(binIndex => {
    const lowerTicks = binIndex * widthTicks, lower = lowerTicks * tick, upper = (lowerTicks + widthTicks) * tick;
    const volume = volumes.get(binIndex)!;
    return { bin_index: binIndex, lower: rounded(lower), upper: rounded(upper), midpoint: rounded((lower + upper) / 2), volume: rounded(volume), share: rounded(volume / total) };
  });
  return { state: "calculated", tick_size: tick, bin_size_ticks: widthTicks, bin_width: rounded(widthTicks * tick), total_volume: rounded(total), trade_count: input.trades.length, eligible_trade_count: eligible, rows };
}

function profileFeatures(input: Row): Row {
  if (!Array.isArray(input.profile_rows) || input.profile_rows.length < 3) throw new RangeError("profile_rows must contain at least three rows");
  const fraction = numberValue(input.value_area_fraction, "value_area_fraction");
  const hvnRatio = numberValue(input.hvn_median_ratio, "hvn_median_ratio", 1);
  const lvnRatio = numberValue(input.lvn_median_ratio, "lvn_median_ratio");
  if (!(fraction > 0 && fraction <= 1) || lvnRatio < 0 || lvnRatio > 1) throw new RangeError("fractions outside canonical range");
  let previousUpper: number | null = null;
  const rows = input.profile_rows.map((row: Row, index: number) => {
    const lower = numberValue(row.lower, `profile_rows[${index}].lower`), upper = numberValue(row.upper, `profile_rows[${index}].upper`), volume = numberValue(row.volume, `profile_rows[${index}].volume`, 0);
    if (upper <= lower || (previousUpper !== null && Math.abs(lower - previousUpper) > 1e-9)) throw new RangeError("profile rows must be ordered and contiguous");
    previousUpper = upper; return { lower, upper, midpoint: (lower + upper) / 2, volume };
  });
  const total = rows.reduce((sum: number, row: Row) => sum + row.volume, 0);
  if (total <= 0) throw new RangeError("profile volume must be positive");
  const profileMidpoint = (rows[0].lower + rows[rows.length - 1].upper) / 2;
  const maximum = Math.max(...rows.map((row: Row) => row.volume));
  const candidates = rows.map((row: Row, i: number) => row.volume === maximum ? i : -1).filter((i: number) => i >= 0);
  candidates.sort((a: number, b: number) => Math.abs(rows[a].midpoint - profileMidpoint) - Math.abs(rows[b].midpoint - profileMidpoint) || rows[a].midpoint - rows[b].midpoint);
  const pocIndex = candidates[0], included = new Set<number>([pocIndex]), trace = [pocIndex];
  let cumulative = rows[pocIndex].volume, low = pocIndex, high = pocIndex;
  const target = total * fraction;
  while (cumulative < target && (low > 0 || high < rows.length - 1)) {
    const below = low > 0 ? low - 1 : null, above = high < rows.length - 1 ? high + 1 : null;
    let chosen: number;
    if (below === null) chosen = above!; else if (above === null) chosen = below; else chosen = rows[above].volume >= rows[below].volume ? above : below;
    included.add(chosen); cumulative += rows[chosen].volume; low = Math.min(low, chosen); high = Math.max(high, chosen); trace.push(chosen);
  }
  const positiveMedian = median(rows.filter((row: Row) => row.volume > 0).map((row: Row) => row.volume));
  const hvn: Row[] = [], lvn: Row[] = [];
  for (let i = 1; i < rows.length - 1; i += 1) {
    const current = rows[i].volume, item = { price: rounded(rows[i].midpoint), volume: rounded(current), median_ratio: rounded(current / positiveMedian) };
    if (current > rows[i - 1].volume && current >= rows[i + 1].volume && current >= positiveMedian * hvnRatio) hvn.push(item);
    if (current < rows[i - 1].volume && current <= rows[i + 1].volume && current <= positiveMedian * lvnRatio) lvn.push(item);
  }
  return { state: "calculated", poc_index: pocIndex, poc_price: rounded(rows[pocIndex].midpoint), poc_volume: rounded(maximum), value_area_low: rounded(rows[low].lower), value_area_high: rounded(rows[high].upper), value_area_volume: rounded(cumulative), value_area_share: rounded(cumulative / total), target_share: fraction, included_indices: [...included].sort((a, b) => a - b), expansion_trace: trace, hvn, lvn };
}

function fibonacci(input: Row): Row {
  const start = numberValue(input.start_price, "start_price"), end = numberValue(input.end_price, "end_price"), correction = numberValue(input.retracement_end_price, "retracement_end_price"), tick = numberValue(input.tick_size, "tick_size", 1e-12);
  if (start === end || !Array.isArray(input.retracement_ratios) || !input.retracement_ratios.length || !Array.isArray(input.extension_ratios) || !input.extension_ratios.length) throw new RangeError("invalid anchors or ratios");
  const rr = input.retracement_ratios.map((v: unknown) => numberValue(v, "retracement ratio")), er = input.extension_ratios.map((v: unknown) => numberValue(v, "extension ratio"));
  if (rr.some((v: number) => v < 0 || v > 1) || er.some((v: number) => v < 0)) throw new RangeError("ratios outside canonical ranges");
  const direction = end > start ? 1 : -1, magnitude = Math.abs(end - start), snap = (value: number) => {
    const tickUnits = value / tick, roundedUnits = Math.sign(tickUnits) * Math.floor(Math.abs(tickUnits) + 0.5);
    return rounded(roundedUnits * tick);
  };
  return { state: "calculated", direction: direction > 0 ? "up" : "down", leg_size: rounded(magnitude), retracement_levels: rr.map((ratio: number) => ({ ratio, raw_price: rounded(end - direction * ratio * magnitude), price: snap(end - direction * ratio * magnitude) })), extension_levels: er.map((ratio: number) => ({ ratio, raw_price: rounded(correction + direction * ratio * magnitude), price: snap(correction + direction * ratio * magnitude) })), rounding: "nearest tick, half away from zero" };
}

function roundNumbers(input: Row): Row {
  const current = numberValue(input.current_price, "current_price"), lower = numberValue(input.lower_bound, "lower_bound"), upper = numberValue(input.upper_bound, "upper_bound"), tick = numberValue(input.tick_size, "tick_size", 1e-12), unit = numberValue(input.base_unit, "base_unit", tick);
  if (lower > current || current > upper || lower >= upper) throw new RangeError("invalid bounds");
  const unitTicks = alignedTicks(unit, tick, "base_unit"), lowerTicks = Math.ceil(lower / tick - 1e-9), upperTicks = Math.floor(upper / tick + 1e-9), first = Math.ceil(lowerTicks / unitTicks) * unitTicks;
  const levels: Row[] = [];
  for (let priceTicks = first; priceTicks <= upperTicks; priceTicks += unitTicks) {
    const multiple = Math.round(priceTicks / unitTicks), major = multiple % 10 === 0, half = multiple % 5 === 0, price = priceTicks * tick, weight = major ? 1 : half ? 0.75 : 0.5;
    levels.push({ price: rounded(price), class: major ? "major" : half ? "half" : "minor", salience_weight: weight, distance: rounded(Math.abs(price - current)), distance_bps: current === 0 ? null : rounded(Math.abs(price - current) / Math.abs(current) * 10000) });
  }
  if (!levels.length) throw new RangeError("bounds contain no levels");
  const closest = [...levels].sort((a, b) => a.distance - b.distance || b.salience_weight - a.salience_weight || a.price - b.price)[0];
  return { state: "calculated", base_unit: unit, level_count: levels.length, closest_level: closest, levels };
}

function fusion(input: Row): Row {
  if (!Array.isArray(input.levels) || !input.levels.length) throw new RangeError("levels must be non-empty");
  const tolerance = numberValue(input.fusion_tolerance, "fusion_tolerance", 0), minimumSources = integerValue(input.minimum_sources, "minimum_sources", 1), seen = new Set<string>();
  const parsed = input.levels.map((level: Row, index: number) => {
    if (typeof level.level_id !== "string" || !level.level_id || seen.has(level.level_id) || typeof level.source !== "string" || !level.source) throw new RangeError("level identity/source invalid");
    seen.add(level.level_id); return { level_id: level.level_id, source: level.source, price: numberValue(level.price, `levels[${index}].price`), weight: numberValue(level.weight, `levels[${index}].weight`, 0) };
  }).sort((a: Row, b: Row) => a.price - b.price || a.source.localeCompare(b.source) || a.level_id.localeCompare(b.level_id));
  const clusters: Row[][] = [];
  parsed.forEach((level: Row) => { if (!clusters.length || level.price - clusters[clusters.length - 1][0].price > 2 * tolerance) clusters.push([level]); else clusters[clusters.length - 1].push(level); });
  const zones: Row[] = [], rejected_clusters: Row[] = [];
  clusters.forEach((cluster, index) => {
    const sources = [...new Set(cluster.map(row => row.source))].sort(), weightSum = cluster.reduce((sum, row) => sum + row.weight, 0), center = weightSum > 0 ? cluster.reduce((sum, row) => sum + row.price * row.weight, 0) / weightSum : cluster.reduce((sum, row) => sum + row.price, 0) / cluster.length;
    const item = { zone_id: `Z${String(index + 1).padStart(2, "0")}`, lower: rounded(Math.min(...cluster.map(row => row.price)) - tolerance), upper: rounded(Math.max(...cluster.map(row => row.price)) + tolerance), center: rounded(center), source_count: sources.length, sources, weight_sum: rounded(weightSum), member_ids: cluster.map(row => row.level_id) };
    if (sources.length >= minimumSources) zones.push(item); else rejected_clusters.push({ ...item, reason: "insufficient-distinct-sources" });
  });
  zones.sort((a, b) => b.source_count - a.source_count || b.weight_sum - a.weight_sum || a.center - b.center);
  return { state: "calculated", zones, rejected_clusters, fusion_tolerance: tolerance, minimum_sources: minimumSources };
}

function strength(input: Row): Row {
  const source = numberValue(input.source_confluence, "source_confluence"), age = integerValue(input.zone_age_bars, "zone_age_bars"), halfLife = numberValue(input.half_life_bars, "half_life_bars", 1e-12), breakCount = integerValue(input.break_count, "break_count"), rejectionTarget = numberValue(input.rejection_target_atr, "rejection_target_atr", 1e-12);
  if (source < 0 || source > 1 || !Array.isArray(input.touches)) throw new RangeError("invalid source or touches");
  const decayed = input.touches.map((touch: Row, index: number) => { const touchAge = integerValue(touch.age_bars, `touches[${index}].age_bars`), rejection = numberValue(touch.rejection_atr, `touches[${index}].rejection_atr`, 0); return [0.5 ** (touchAge / halfLife), Math.min(rejection / rejectionTarget, 1)]; });
  const evidence = decayed.reduce((sum: number, value: number[]) => sum + value[0], 0), sourceComponent = 30 * source, touchComponent = 35 * (1 - Math.exp(-evidence)), rejectionComponent = evidence === 0 ? 0 : 25 * decayed.reduce((sum: number, value: number[]) => sum + value[0] * value[1], 0) / evidence, durabilityComponent = 10 * (0.5 ** (age / halfLife)), penalty = Math.min(30, 15 * breakCount), score = Math.max(0, Math.min(100, sourceComponent + touchComponent + rejectionComponent + durabilityComponent - penalty));
  return { state: "calculated", score: rounded(score), grade: score >= 75 ? "strong" : score >= 50 ? "moderate" : score >= 25 ? "weak" : "depleted", components: { source: rounded(sourceComponent), touch: rounded(touchComponent), rejection: rounded(rejectionComponent), durability: rounded(durabilityComponent), break_penalty: rounded(penalty) }, decayed_touch_evidence: rounded(evidence), touch_count: input.touches.length };
}

function roleReversal(input: Row): Row {
  const lower = numberValue(input.zone_lower, "zone_lower"), upper = numberValue(input.zone_upper, "zone_upper"), buffer = numberValue(input.break_buffer, "break_buffer", 0), confirmations = integerValue(input.confirmation_closes, "confirmation_closes", 1);
  if (lower >= upper || !["support", "resistance"].includes(input.initial_role) || !Array.isArray(input.closes) || !input.closes.length) throw new RangeError("invalid role-reversal input");
  const values = input.closes.map((value: unknown) => numberValue(value, "close")); let role = input.initial_role, state = `active-${role}`, outsideCount = 0, retestSeen = false; const transitions: Row[] = [];
  values.forEach((close: number, index: number) => {
    const side = close > upper + buffer ? "above" : close < lower - buffer ? "below" : "zone", previous = state;
    if (state === "active-support") { outsideCount = side === "below" ? outsideCount + 1 : 0; if (outsideCount >= confirmations) { state = "awaiting-resistance-retest"; outsideCount = 0; } }
    else if (state === "active-resistance") { outsideCount = side === "above" ? outsideCount + 1 : 0; if (outsideCount >= confirmations) { state = "awaiting-support-retest"; outsideCount = 0; } }
    else if (state === "awaiting-resistance-retest") { if (side === "zone") retestSeen = true; else if (retestSeen && side === "below") { state = "confirmed-resistance"; role = "resistance"; } else if (side === "above") state = "invalidated"; }
    else if (state === "awaiting-support-retest") { if (side === "zone") retestSeen = true; else if (retestSeen && side === "above") { state = "confirmed-support"; role = "support"; } else if (side === "below") state = "invalidated"; }
    if (state !== previous) transitions.push({ index, close, from: previous, to: state, side });
  });
  return { state, final_role: state === "invalidated" ? "none" : role, confirmed: state.startsWith("confirmed-"), retest_seen: retestSeen, transitions, observations: values.length };
}

function breakoutRetest(input: Row): Row {
  const lower = numberValue(input.zone_lower, "zone_lower"), upper = numberValue(input.zone_upper, "zone_upper"), buffer = numberValue(input.break_buffer, "break_buffer", 0), tolerance = numberValue(input.retest_tolerance, "retest_tolerance", 0), needed = integerValue(input.breakout_closes, "breakout_closes", 1), maxBars = integerValue(input.max_retest_bars, "max_retest_bars", 1), direction = input.direction;
  if (lower >= upper || !["up", "down"].includes(direction) || !Array.isArray(input.bars) || !input.bars.length) throw new RangeError("invalid breakout input");
  let state = "searching", count = 0, breakout_index: number | null = null, retest_index: number | null = null, confirmation_index: number | null = null; const transitions: Row[] = [];
  input.bars.forEach((bar: Row, index: number) => {
    const high = numberValue(bar.high, "bar.high"), low = numberValue(bar.low, "bar.low"), close = numberValue(bar.close, "bar.close");
    if (high < Math.max(low, close) || low > Math.min(high, close)) throw new RangeError("inconsistent bar");
    const previous = state, beyond = direction === "up" ? close > upper + buffer : close < lower - buffer, failed = direction === "up" ? close < lower - buffer : close > upper + buffer, contact = low <= upper + tolerance && high >= lower - tolerance;
    if (state === "searching" || state === "breakout-pending") { count = beyond ? count + 1 : 0; state = count ? "breakout-pending" : "searching"; if (count >= needed) { breakout_index = index; state = "awaiting-retest"; } }
    else if (state === "awaiting-retest") { if (breakout_index !== null && index - breakout_index > maxBars) state = "expired"; else if (failed) state = "failed"; else if (contact) { retest_index = index; state = "retest-contact"; } }
    else if (state === "retest-contact") { if (failed) state = "failed"; else if (beyond) { confirmation_index = index; state = "confirmed"; } else if (breakout_index !== null && index - breakout_index > maxBars) state = "expired"; }
    if (state !== previous) transitions.push({ index, from: previous, to: state, close });
  });
  return { state, confirmed: state === "confirmed", direction, breakout_index, retest_index, confirmation_index, transitions };
}

function scanner(input: Row): Row {
  const asOf = timeValue(input.as_of, "as_of"), maxBps = numberValue(input.max_distance_bps, "max_distance_bps", 0), halfLife = numberValue(input.freshness_half_life_hours, "freshness_half_life_hours", 1e-12);
  if (!Array.isArray(input.instruments) || !input.instruments.length) throw new RangeError("instruments must be non-empty");
  const seen = new Set<string>(), ranked: Row[] = [];
  input.instruments.forEach((item: Row, index: number) => {
    if (typeof item.instrument_id !== "string" || !item.instrument_id || seen.has(item.instrument_id)) throw new RangeError("instrument_id must be unique"); seen.add(item.instrument_id);
    const price = numberValue(item.current_price, `instruments[${index}].current_price`, 1e-12), lower = numberValue(item.zone_lower, "zone_lower"), upper = numberValue(item.zone_upper, "zone_upper"), zoneStrength = numberValue(item.zone_strength, "zone_strength"), observed = timeValue(item.observed_at, "observed_at");
    if (lower >= upper || zoneStrength < 0 || zoneStrength > 100 || observed > asOf) throw new RangeError("invalid point-in-time zone record");
    const distance = price < lower ? lower - price : price > upper ? price - upper : 0, distanceBps = distance / price * 10000, ageHours = (asOf - observed) / 3600000, freshness = 0.5 ** (ageHours / halfLife), proximity = maxBps > 0 ? Math.max(0, 1 - distanceBps / maxBps) : distanceBps === 0 ? 1 : 0, score = 100 * (0.55 * proximity + 0.30 * zoneStrength / 100 + 0.15 * freshness);
    if (distanceBps <= maxBps) ranked.push({ instrument_id: item.instrument_id, current_price: price, zone_lower: lower, zone_upper: upper, inside_zone: distance === 0, distance: rounded(distance), distance_bps: rounded(distanceBps), strength: zoneStrength, freshness: rounded(freshness), rank_score: rounded(score) });
  });
  ranked.sort((a, b) => b.rank_score - a.rank_score || a.distance_bps - b.distance_bps || a.instrument_id.localeCompare(b.instrument_id)); ranked.forEach((row, index) => { row.rank = index + 1; });
  return { state: "calculated", as_of: input.as_of, eligible_count: ranked.length, ranked };
}

const calculators: Record<string, (input: Row) => Row> = {
  "D08-F06-A01": profile,
  "D08-F06-A02": profileFeatures,
  "D08-F06-A03": fibonacci,
  "D08-F06-A04": roundNumbers,
  "D08-F06-A05": fusion,
  "D08-F06-A06": strength,
  "D08-F06-A07": roleReversal,
  "D08-F06-A08": breakoutRetest,
  "D08-F06-A09": scanner,
};

export function calculate(topicId: string, input: Row): Row {
  const calculator = calculators[topicId];
  if (!calculator) throw new RangeError(`unsupported topic_id: ${topicId}`);
  if (!input || typeof input !== "object" || Array.isArray(input)) throw new TypeError("inputs must be an object");
  return calculator(input);
}
Full-height labplaygroundOpen full screen