Finalize GTL and retire S/R research harness
This commit is contained in:
+23
-103
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user