Spread trade targets across distance bands
MKC showed 5 targets all far/Aggressive: target selection was top-5 by quality (0.35*R:R + ...), and R:R grows with distance, so far levels crowded out every nearby one. generate_targets now selects for spread: always include the nearest level, plus the best-quality representative from each distance band (Conservative <=2.9 ATR, Moderate <=4.6 ATR, Aggressive beyond), then fill remaining slots by quality. Restores a Conservative/Moderate/ Aggressive mix with the nearest target always present. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,12 @@ DEFAULT_RECOMMENDATION_CONFIG: dict[str, float] = {
|
||||
# metric we actually measure.
|
||||
_TARGET_HORIZON_DAYS = 30.0
|
||||
|
||||
# Distance bands (in ATR) used to spread targets across Conservative / Moderate
|
||||
# / Aggressive. Aligned with where the touch-probability crosses 60% / 40% over
|
||||
# the horizon, so a target from each band tends to land in the matching label.
|
||||
_CONSERVATIVE_MAX_ATR = 2.9
|
||||
_MODERATE_MAX_ATR = 4.6
|
||||
|
||||
|
||||
def _clamp(value: float, low: float, high: float) -> float:
|
||||
return max(low, min(high, value))
|
||||
@@ -255,12 +261,39 @@ class TargetGenerator:
|
||||
}
|
||||
)
|
||||
|
||||
candidates.sort(key=lambda row: row["quality"], reverse=True)
|
||||
selected = candidates[:5]
|
||||
selected.sort(key=lambda row: row["distance_from_entry"])
|
||||
if not candidates:
|
||||
return []
|
||||
|
||||
# Classification is assigned later from the computed reach-probability
|
||||
# (see _classify_by_probability), not from distance rank.
|
||||
# Select up to 5 targets that SPAN the distance range, instead of the
|
||||
# top-5 by quality (which biases toward far, high-R:R levels and buries
|
||||
# every nearby target). Guarantees the nearest level plus a
|
||||
# representative from each distance band when they exist, so the table
|
||||
# offers a real mix of Conservative / Moderate / Aggressive.
|
||||
conservative = [c for c in candidates if c["distance_atr_multiple"] <= _CONSERVATIVE_MAX_ATR]
|
||||
moderate = [c for c in candidates if _CONSERVATIVE_MAX_ATR < c["distance_atr_multiple"] <= _MODERATE_MAX_ATR]
|
||||
aggressive = [c for c in candidates if c["distance_atr_multiple"] > _MODERATE_MAX_ATR]
|
||||
|
||||
selected: list[dict[str, Any]] = []
|
||||
selected_ids: set[int] = set()
|
||||
|
||||
def _add(candidate: dict[str, Any] | None) -> None:
|
||||
if candidate is not None and candidate["sr_level_id"] not in selected_ids:
|
||||
selected.append(candidate)
|
||||
selected_ids.add(candidate["sr_level_id"])
|
||||
|
||||
# Nearest overall (the most likely / Conservative anchor)
|
||||
_add(min(candidates, key=lambda c: c["distance_atr_multiple"]))
|
||||
# Best-quality representative from each band → spread across labels
|
||||
for bucket in (conservative, moderate, aggressive):
|
||||
if bucket:
|
||||
_add(max(bucket, key=lambda c: c["quality"]))
|
||||
# Fill remaining slots with the next-best by quality
|
||||
for candidate in sorted(candidates, key=lambda c: c["quality"], reverse=True):
|
||||
if len(selected) >= 5:
|
||||
break
|
||||
_add(candidate)
|
||||
|
||||
selected.sort(key=lambda row: row["distance_from_entry"])
|
||||
for target in selected:
|
||||
target.pop("quality", None)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user