major update
Some checks failed
Deploy / lint (push) Failing after 8s
Deploy / test (push) Has been skipped
Deploy / deploy (push) Has been skipped

This commit is contained in:
Dennis Thiessen
2026-02-27 16:08:09 +01:00
parent 61ab24490d
commit 181cfe6588
71 changed files with 7647 additions and 281 deletions

View File

@@ -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,