There was no way to say "tell me when ES reaches 7800". The only user-settable alert was a drawn trendline, placed by clicking two points on a canvas — so you could not hit an exact price, and making the line flat was fiddly. A price alert is a manual line with zero slope. Reusing that rather than building a parallel concept means it inherits JSON persistence, renaming, recolouring, deletion, clustering, and the rule that a hand-placed level alerts whatever its confluence score. The only genuinely new code is the input, an endpoint that takes a price instead of two anchors, and the decision to render zero-slope manual lines as price lines — which spans the chart and labels the axis, instead of drawing a stubby two-point segment. Zero-slope lines also skip drag handles, hit-testing and the "end line here" menu: a price line has no endpoints to grab. They are managed from the sidebar. Unlabelled alerts are named by their price, since "1d resistance" does not say which alert fired. Verified live: a level typed 25 points above price clusters as resistance at that price and stays quiet, as it should until price arrives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
45 lines
1.6 KiB
Python
45 lines
1.6 KiB
Python
from app.analysis.alerts import AlertEngine
|
|
from app.analysis.confluence import cluster_levels
|
|
from app.analysis.levels import Side
|
|
from app.analysis.manual_lines import ManualLine
|
|
from app.bars.models import Timeframe
|
|
|
|
|
|
def price_alert(price: float, note: str = "") -> ManualLine:
|
|
return ManualLine(
|
|
"ml_price", Timeframe.D1, Side.RESISTANCE, 1000, price, 0.0, 4600, 1000, note=note
|
|
)
|
|
|
|
|
|
def test_a_typed_level_holds_its_price_at_any_time():
|
|
level = price_alert(7800).to_level()
|
|
assert level.price_at(0) == 7800
|
|
assert level.price_at(10**9) == 7800
|
|
|
|
|
|
def test_unlabelled_alert_is_named_by_its_price():
|
|
# "1d resistance" tells you nothing about which alert fired.
|
|
assert price_alert(7800).to_level().label == "@ 7800.00"
|
|
assert price_alert(7800, note="gap fill").to_level().label == "gap fill"
|
|
|
|
|
|
def test_typed_level_alerts_regardless_of_confluence_score():
|
|
engine = AlertEngine(min_score=28, cooldown_seconds=900)
|
|
clusters = cluster_levels([price_alert(7800, "gap fill").to_level()], 5000, 7800, 1)
|
|
|
|
assert len(clusters) == 1
|
|
alerts = engine.evaluate(clusters, 7800, 1, 5000, "/ES")
|
|
assert len(alerts) == 1
|
|
assert "gap fill" in alerts[0].message
|
|
|
|
|
|
def test_price_alerts_survive_the_json_round_trip(tmp_path):
|
|
from app.analysis.manual_lines import ManualLineStore
|
|
|
|
store = ManualLineStore(tmp_path / "manual_lines.json")
|
|
store.add(price_alert(7800, "gap fill"))
|
|
|
|
reloaded = ManualLineStore(tmp_path / "manual_lines.json").lines["ml_price"]
|
|
assert reloaded.slope == 0.0
|
|
assert reloaded.horizontal
|
|
assert reloaded.anchor_p == 7800
|