From 51a9636ea7af6526a45cad5587e5eea2f91126b7 Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Mon, 10 Aug 2026 01:51:54 -0500 Subject: [PATCH] Stop re-alerting when price crosses a level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suppression matched on cluster side as well as position, but side is positional: a level sitting at price is resistance when price is a tick below it and support a tick later. Every crossing failed the side match and fired as a brand-new zone — which is exactly when a level is least newsworthy, not most. Found the honest way. A flat line placed at the live price produced four phone pushes in two minutes. Zones are now matched on position alone. Side still determines whether the message reads BULLISH or BEARISH; it just no longer decides whether you are told twice. Over the same six replayed sessions at threshold 28 that is 40 alerts down to 26, and 10 at the configured four-hour cooldown with a worst session of 5. Co-Authored-By: Claude Opus 5 --- README.md | 12 ++++++++---- app/analysis/alerts.py | 12 +++++++----- tests/test_alerts.py | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f0df4a8..b28fd84 100644 --- a/README.md +++ b/README.md @@ -59,15 +59,19 @@ Two fixes brought it back, in this order: - **Alert suppression** keyed on cluster identity, so a level drifting in or out of a group counted as a new zone. It now suppresses by *proximity*: two zones within one ATR are the same zone. 54 → 40. +- **Suppression also matched on side**, and side is positional — a level sitting at + price flips between support and resistance every time price ticks across it. Each + flip read as a new zone. Found by putting a real line at the live price and getting + four pushes in two minutes. 40 → 26. Only then does the cooldown do anything useful. At threshold `28` the sweep reads: | cooldown | total | max/session | |---|---|---| -| 900s | 40 | 30 | -| 3600s | 29 | 20 | -| 7200s | 22 | 14 | -| **14400s (selected)** | **17** | **9** | +| 900s | 26 | 19 | +| 3600s | 18 | 12 | +| 7200s | 12 | 7 | +| **14400s (selected)** | **10** | **5** | Note the threshold itself is a blunt control: scores are sums of 12s (moving averages, VWAP) and 16s (prior-day levels), so `20`, `24` and `28` behave identically and `32` diff --git a/app/analysis/alerts.py b/app/analysis/alerts.py index dc4dc4c..3004301 100644 --- a/app/analysis/alerts.py +++ b/app/analysis/alerts.py @@ -12,7 +12,6 @@ class Alert: @dataclass(slots=True) class _Fired: - side: str center: float at: int @@ -70,13 +69,16 @@ class AlertEngine: continue if abs(cluster.center - current_price) > tolerance: continue + # Deliberately not matched on side. A level sitting at price flips + # between support and resistance every time price ticks across it, + # because the side is positional. Matching on it meant a zone price + # was oscillating on re-alerted on every crossing — which is exactly + # when a level is least newsworthy, not most. if any( - entry.side == cluster.side.value - and abs(entry.center - cluster.center) <= merge_distance - for entry in self._fired + abs(entry.center - cluster.center) <= merge_distance for entry in self._fired ): continue - self._fired.append(_Fired(cluster.side.value, cluster.center, now)) + self._fired.append(_Fired(cluster.center, now)) direction = "BEARISH" if cluster.side.value == "resistance" else "BULLISH" timeframes = ", ".join(dict.fromkeys(member.tf.value for member in cluster.members)) # Naming the line matters: "your line" is actionable in a way that diff --git a/tests/test_alerts.py b/tests/test_alerts.py index 8c74640..2f61fd4 100644 --- a/tests/test_alerts.py +++ b/tests/test_alerts.py @@ -75,6 +75,21 @@ def test_drawn_line_clustering_with_levels_reports_as_a_zone(): assert "swing high" in alerts[0].message # the line is still named +def test_price_crossing_a_level_does_not_re_alert_on_the_side_flip(): + # Side is positional, so a level sitting at price flips between support and + # resistance on every tick across it. This produced a fresh alert per + # crossing — four in two minutes when first tried against a live line. + engine = AlertEngine(min_score=6, cooldown_seconds=900) + line = [drawn_line("ml_1", 100, weight=8)] + + below = cluster_levels(line, 100, 99.9, 1) # level above price -> resistance + assert len(engine.evaluate(below, 99.9, 1, 0, "/ES")) == 1 + + above = cluster_levels(line, 100, 100.1, 1) # price crossed -> now support + assert above and above[0].side is not below[0].side, "the flip must actually occur" + assert engine.evaluate(above, 100.1, 1, 30, "/ES") == [] + + def test_a_genuinely_separate_zone_still_alerts_during_cooldown(): # The cooldown is per zone, not global: a level far away is new information. engine = AlertEngine(min_score=6, cooldown_seconds=900)