generate targets from S/R zones, not raw levels (consistency + strength)
Trade-setup targets now pre-merge near-duplicate S/R levels into zone representatives (same 2% clusterer as chart + alerts) before generate_targets runs. A clustered wall (e.g. 183 + 185) becomes one target carrying the zone's COMBINED strength (capped 100) instead of two near-identical targets that each undervalue the wall — which also feeds a more honest reach-probability via the S/R-strength magnet. Representative price is the zone's near edge; the strongest constituent's id is retained. Singleton levels pass through unchanged, so the downstream band-spreading / probability / primary-selection pipeline and its tests are untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -226,3 +226,38 @@ def test_classify_by_probability_thresholds():
|
||||
assert _classify_by_probability(75) == "Conservative"
|
||||
assert _classify_by_probability(50) == "Moderate"
|
||||
assert _classify_by_probability(20) == "Aggressive"
|
||||
|
||||
|
||||
def test_zone_representative_levels_merges_wall():
|
||||
"""Near-duplicate resistances collapse to one zone with combined strength."""
|
||||
from types import SimpleNamespace
|
||||
from app.services.recommendation_service import _zone_representative_levels
|
||||
|
||||
levels = [
|
||||
SimpleNamespace(id=1, price_level=183.0, type="resistance", strength=60),
|
||||
SimpleNamespace(id=2, price_level=185.0, type="resistance", strength=60),
|
||||
SimpleNamespace(id=3, price_level=200.0, type="resistance", strength=50),
|
||||
]
|
||||
reps = _zone_representative_levels(levels, entry_price=180.0)
|
||||
|
||||
# 183/185 merge into one zone; 200 stays separate → 2 representatives
|
||||
assert len(reps) == 2
|
||||
wall = min(reps, key=lambda r: r.price_level)
|
||||
assert wall.price_level == 183.0 # near edge of the wall
|
||||
assert wall.strength == 100 # 60 + 60, capped at 100
|
||||
far = max(reps, key=lambda r: r.price_level)
|
||||
assert far.price_level == 200.0
|
||||
assert far.strength == 50
|
||||
|
||||
|
||||
def test_zone_representative_levels_singletons_unchanged():
|
||||
from types import SimpleNamespace
|
||||
from app.services.recommendation_service import _zone_representative_levels
|
||||
|
||||
levels = [
|
||||
SimpleNamespace(id=1, price_level=120.0, type="resistance", strength=70),
|
||||
SimpleNamespace(id=2, price_level=150.0, type="resistance", strength=40),
|
||||
]
|
||||
reps = _zone_representative_levels(levels, entry_price=100.0)
|
||||
assert len(reps) == 2
|
||||
assert {round(r.price_level) for r in reps} == {120, 150}
|
||||
|
||||
Reference in New Issue
Block a user