major update
This commit is contained in:
@@ -204,6 +204,121 @@ def detect_sr_levels(
|
||||
|
||||
return tagged
|
||||
|
||||
def cluster_sr_zones(
|
||||
levels: list[dict],
|
||||
current_price: float,
|
||||
tolerance: float = 0.02,
|
||||
max_zones: int | None = None,
|
||||
) -> list[dict]:
|
||||
"""Cluster nearby S/R levels into zones.
|
||||
|
||||
Returns list of zone dicts:
|
||||
{
|
||||
"low": float,
|
||||
"high": float,
|
||||
"midpoint": float,
|
||||
"strength": int, # sum of constituent strengths, capped at 100
|
||||
"type": "support" | "resistance",
|
||||
"level_count": int,
|
||||
}
|
||||
"""
|
||||
if not levels:
|
||||
return []
|
||||
|
||||
if max_zones is not None and max_zones <= 0:
|
||||
return []
|
||||
|
||||
# 1. Sort levels by price_level ascending
|
||||
sorted_levels = sorted(levels, key=lambda x: x["price_level"])
|
||||
|
||||
# 2. Greedy merge into clusters
|
||||
clusters: list[list[dict]] = []
|
||||
current_cluster: list[dict] = [sorted_levels[0]]
|
||||
|
||||
for level in sorted_levels[1:]:
|
||||
# Compute current cluster midpoint
|
||||
prices = [l["price_level"] for l in current_cluster]
|
||||
cluster_low = min(prices)
|
||||
cluster_high = max(prices)
|
||||
cluster_mid = (cluster_low + cluster_high) / 2.0
|
||||
|
||||
# Check if within tolerance of cluster midpoint
|
||||
if cluster_mid != 0:
|
||||
distance_pct = abs(level["price_level"] - cluster_mid) / cluster_mid
|
||||
else:
|
||||
distance_pct = abs(level["price_level"])
|
||||
|
||||
if distance_pct <= tolerance:
|
||||
current_cluster.append(level)
|
||||
else:
|
||||
clusters.append(current_cluster)
|
||||
current_cluster = [level]
|
||||
|
||||
clusters.append(current_cluster)
|
||||
|
||||
# 3. Compute zone for each cluster
|
||||
zones: list[dict] = []
|
||||
for cluster in clusters:
|
||||
prices = [l["price_level"] for l in cluster]
|
||||
low = min(prices)
|
||||
high = max(prices)
|
||||
midpoint = (low + high) / 2.0
|
||||
strength = min(100, sum(l["strength"] for l in cluster))
|
||||
level_count = len(cluster)
|
||||
|
||||
# 4. Tag zone type
|
||||
zone_type = "support" if midpoint < current_price else "resistance"
|
||||
|
||||
zones.append({
|
||||
"low": low,
|
||||
"high": high,
|
||||
"midpoint": midpoint,
|
||||
"strength": strength,
|
||||
"type": zone_type,
|
||||
"level_count": level_count,
|
||||
})
|
||||
|
||||
# 5. Split into support and resistance pools, each sorted by strength desc
|
||||
support_zones = sorted(
|
||||
[z for z in zones if z["type"] == "support"],
|
||||
key=lambda z: z["strength"],
|
||||
reverse=True,
|
||||
)
|
||||
resistance_zones = sorted(
|
||||
[z for z in zones if z["type"] == "resistance"],
|
||||
key=lambda z: z["strength"],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# 6. Interleave pick: alternate strongest from each pool
|
||||
selected: list[dict] = []
|
||||
limit = max_zones if max_zones is not None else len(zones)
|
||||
si, ri = 0, 0
|
||||
pick_support = True # start with support pool
|
||||
|
||||
while len(selected) < limit and (si < len(support_zones) or ri < len(resistance_zones)):
|
||||
if pick_support:
|
||||
if si < len(support_zones):
|
||||
selected.append(support_zones[si])
|
||||
si += 1
|
||||
elif ri < len(resistance_zones):
|
||||
selected.append(resistance_zones[ri])
|
||||
ri += 1
|
||||
else:
|
||||
if ri < len(resistance_zones):
|
||||
selected.append(resistance_zones[ri])
|
||||
ri += 1
|
||||
elif si < len(support_zones):
|
||||
selected.append(support_zones[si])
|
||||
si += 1
|
||||
pick_support = not pick_support
|
||||
|
||||
# 7. Sort final selection by strength descending
|
||||
selected.sort(key=lambda z: z["strength"], reverse=True)
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
|
||||
async def recalculate_sr_levels(
|
||||
db: AsyncSession,
|
||||
|
||||
Reference in New Issue
Block a user