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:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
@@ -12,6 +13,7 @@ from app.models.settings import SystemSetting
|
||||
from app.models.sr_level import SRLevel
|
||||
from app.models.ticker import Ticker
|
||||
from app.models.trade_setup import TradeSetup
|
||||
from app.services.sr_service import cluster_sr_zones
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,11 +40,58 @@ _TARGET_HORIZON_DAYS = 30.0
|
||||
_CONSERVATIVE_MAX_ATR = 2.9
|
||||
_MODERATE_MAX_ATR = 4.6
|
||||
|
||||
# Merge S/R levels within this fraction into one zone before generating targets —
|
||||
# the same tolerance the chart and alerts use, so S/R is one model app-wide.
|
||||
_SR_ZONE_TOLERANCE = 0.02
|
||||
|
||||
|
||||
def _clamp(value: float, low: float, high: float) -> float:
|
||||
return max(low, min(high, value))
|
||||
|
||||
|
||||
def _zone_representative_levels(sr_levels: list[SRLevel], entry_price: float) -> list[Any]:
|
||||
"""Collapse near-duplicate S/R levels into one representative per zone.
|
||||
|
||||
Targets are generated from these representatives, so a clustered wall (e.g.
|
||||
183 + 185) becomes a single target carrying the zone's COMBINED strength
|
||||
(capped at 100) instead of two near-identical targets, each undervaluing the
|
||||
wall. Same clusterer as the chart and alerts → one S/R model everywhere.
|
||||
|
||||
The representative price is the zone's near edge (the reachable side of the
|
||||
wall) and it keeps the strongest constituent's id for reference. Singleton
|
||||
levels pass through unchanged.
|
||||
"""
|
||||
if not sr_levels or entry_price <= 0:
|
||||
return list(sr_levels)
|
||||
|
||||
level_dicts = [
|
||||
{"price_level": float(lv.price_level), "strength": int(lv.strength), "type": lv.type}
|
||||
for lv in sr_levels
|
||||
]
|
||||
zones = cluster_sr_zones(level_dicts, entry_price, tolerance=_SR_ZONE_TOLERANCE)
|
||||
|
||||
reps: list[Any] = []
|
||||
for zone in zones:
|
||||
constituents = [
|
||||
lv for lv in sr_levels if zone["low"] <= float(lv.price_level) <= zone["high"]
|
||||
]
|
||||
if not constituents:
|
||||
continue
|
||||
strongest = max(constituents, key=lambda lv: lv.strength)
|
||||
# Near edge: bottom of a resistance wall (above entry), top of a support
|
||||
# wall (below entry) — the first price the move reaches.
|
||||
near_edge = zone["low"] if zone["type"] == "resistance" else zone["high"]
|
||||
reps.append(
|
||||
SimpleNamespace(
|
||||
id=int(strongest.id),
|
||||
price_level=float(near_edge),
|
||||
type=zone["type"],
|
||||
strength=int(zone["strength"]),
|
||||
)
|
||||
)
|
||||
return reps
|
||||
|
||||
|
||||
def _norm_cdf(x: float) -> float:
|
||||
"""Standard normal CDF via erf (no SciPy dependency)."""
|
||||
return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))
|
||||
@@ -522,11 +571,15 @@ async def enhance_trade_setup(
|
||||
direction = setup.direction.lower()
|
||||
confidence = long_confidence if direction == "long" else short_confidence
|
||||
|
||||
# Merge near-duplicate levels into zone representatives first, so a clustered
|
||||
# wall yields one strength-combined target instead of several near-identical
|
||||
# ones — consistent with the chart and alerts.
|
||||
zone_levels = _zone_representative_levels(sr_levels, setup.entry_price)
|
||||
targets = target_generator.generate_targets(
|
||||
direction=direction,
|
||||
entry_price=setup.entry_price,
|
||||
stop_loss=setup.stop_loss,
|
||||
sr_levels=sr_levels,
|
||||
sr_levels=zone_levels,
|
||||
atr_value=atr_value,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user