Finalize GTL and retire S/R research harness

This commit is contained in:
2026-07-13 17:58:04 +02:00
parent 9d362bd568
commit bee5a5ce89
35 changed files with 374 additions and 5385779 deletions
+23 -103
View File
@@ -358,55 +358,13 @@ def _extract_candidate_levels(
return candidates
def _legacy_volume_profile_nodes(
highs: list[float],
lows: list[float],
closes: list[float],
volumes: list[int],
num_bins: int = 20,
) -> list[float]:
"""Reproduce the deployed pre-rewrite HVN/LVN grid for a control arm.
This intentionally retains the old span-volume double counting. It exists
only so a local research report can prove that its control still reproduces
the frozen production baseline while the live detector is being rewritten.
"""
if len(closes) < 20:
raise ValidationError(
f"Volume Profile requires at least 20 bars, got {len(closes)}"
)
price_min = min(lows)
price_max = max(highs)
if price_max == price_min:
price_max = price_min + 1.0
bin_width = (price_max - price_min) / num_bins
bins: list[float] = [0.0] * num_bins
prices = [price_min + (i + 0.5) * bin_width for i in range(num_bins)]
for i in range(len(closes)):
for b in range(num_bins):
low_edge = price_min + b * bin_width
high_edge = low_edge + bin_width
if highs[i] >= low_edge and lows[i] <= high_edge:
bins[b] += volumes[i]
average = sum(bins) / num_bins
hvn = [round(prices[i], 4) for i in range(num_bins) if bins[i] > average]
lvn = [round(prices[i], 4) for i in range(num_bins) if bins[i] < average]
return hvn + lvn
def _legacy_range_grid_nodes(
def _gate_target_range_centers(
highs: list[float],
lows: list[float],
closes: list[float],
num_bins: int = 20,
) -> list[float]:
"""Return every deployed grid center without the irrelevant volume pass.
The legacy profile returns both its above-average (HVN) and below-average
(LVN) bins. Their union is therefore the complete range grid except for the
rare bin whose traffic equals the average exactly. This explicit helper
isolates that geometry from the misleading volume-profile label.
"""
"""Return the evenly spaced price proposals used by the production GTL."""
if len(closes) < 20:
raise ValidationError(
f"Range grid requires at least 20 bars, got {len(closes)}"
@@ -422,49 +380,38 @@ def _legacy_range_grid_nodes(
]
def detect_sr_levels_legacy(
def detect_gate_target_ladder(
highs: list[float],
lows: list[float],
closes: list[float],
volumes: list[int],
tolerance: float = DEFAULT_TOLERANCE,
*,
include_volume_profile: bool = True,
include_pivots: bool = True,
neutral_strength: bool = False,
explicit_range_grid: bool = False,
) -> list[dict]:
"""Deployed detector plus source-isolation controls for local research.
"""Build the scanner's internal, volume-free target proposal ladder.
The defaults remain the exact production control. Keyword-only switches
allow a research arm to remove one source or neutralize strength without
copying or subtly changing the legacy implementation.
This is intentionally not human-facing support/resistance. It builds the
production gate's broad 20-bin range grid, adds unfiltered pivots, scores
historical price traffic, and merges nearby proposals. The returned levels
are transient and must not be persisted as chart S/R.
"""
if not closes:
return []
candidates: list[tuple[float, str]] = []
if include_volume_profile:
try:
nodes = (
_legacy_range_grid_nodes(highs, lows, closes)
if explicit_range_grid
else _legacy_volume_profile_nodes(highs, lows, closes, volumes)
)
method = "range_grid" if explicit_range_grid else "volume_profile"
for price in nodes:
candidates.append((float(price), method))
except ValidationError:
pass
if include_pivots:
try:
pivots = compute_pivot_points(highs, lows, closes)
candidates.extend(
(float(price), "pivot_point")
for price in pivots.get("swing_highs", []) + pivots.get("swing_lows", [])
)
except ValidationError:
pass
try:
candidates.extend(
(float(price), "range_grid")
for price in _gate_target_range_centers(highs, lows, closes)
)
except ValidationError:
pass
try:
pivots = compute_pivot_points(highs, lows, closes)
candidates.extend(
(float(price), "pivot_point")
for price in pivots.get("swing_highs", []) + pivots.get("swing_lows", [])
)
except ValidationError:
pass
if not candidates:
return []
@@ -515,37 +462,10 @@ def detect_sr_levels_legacy(
)
_tag_levels(merged, closes[-1])
if neutral_strength:
for level in merged:
level["strength"] = 50
merged.sort(key=lambda row: row["strength"], reverse=True)
return merged
def detect_gate_target_ladder(
highs: list[float],
lows: list[float],
closes: list[float],
tolerance: float = DEFAULT_TOLERANCE,
) -> list[dict]:
"""Build the scanner's internal, volume-free target proposal ladder.
This is intentionally not human-facing support/resistance. It preserves
the production gate's broad 20-bin range grid, unfiltered pivots, touch
strength, and merge geometry without performing or claiming a volume
profile calculation. The returned levels are transient and must not be
persisted as chart S/R.
"""
return detect_sr_levels_legacy(
highs,
lows,
closes,
[0] * len(closes),
tolerance,
explicit_range_grid=True,
)
def _merge_levels(
levels: list[dict],
tolerance: float = DEFAULT_TOLERANCE,