chart/tests/test_manual_lines.py

138 lines
5.4 KiB
Python

from app.analysis.levels import Side
from app.analysis.manual_lines import ManualLine, ManualLineStore
from app.analysis.confluence import cluster_levels
from app.analysis.levels import Level, LevelKind
from app.bars.models import Timeframe
def sample_line():
return ManualLine("ml_test", Timeframe.H1, Side.RESISTANCE, 100, 5000, -0.01, 200, 300, number=1)
def test_json_persistence_round_trip(tmp_path):
path = tmp_path / "manual_lines.json"
store = ManualLineStore(path)
store.add(sample_line())
loaded = ManualLineStore(path)
assert list(loaded.lines.values()) == [sample_line()]
loaded.update("ml_test", {"note": "major swing"})
assert ManualLineStore(path).lines["ml_test"].note == "major swing"
loaded.delete("ml_test")
assert ManualLineStore(path).lines == {}
def test_price_level_alert_offset_survives_json_round_trip(tmp_path):
path = tmp_path / "manual_lines.json"
line = sample_line()
line.slope = 0.0
line.alert_early_points = 1.25
ManualLineStore(path).add(line)
loaded = ManualLineStore(path).lines["ml_test"]
assert loaded.alert_early_points == 1.25
assert loaded.to_level().alert_early_points == 1.25
def test_hourly_line_uses_absolute_time_on_one_minute_chart():
level = sample_line().to_level()
instant = 160
assert level.tf is Timeframe.H1
assert level.price_at(instant) == 4999.4
assert level.weight == 4
def test_unnamed_trendlines_are_named_for_their_direction():
resistance = sample_line()
support = ManualLine(
"ml_support", Timeframe.M1, Side.SUPPORT, 100, 5000, 0.01, 200, 300
)
assert resistance.default_label() == "down1h"
assert support.default_label() == "up1m"
def test_manual_line_raises_existing_ma_cluster_score():
ma = Level(
"ma", LevelKind.MA, Timeframe.D1, Side.RESISTANCE, 12, 1, "1d SMA20",
100, 5000, 0, None, 0, 100, 100, False, False, 20,
)
before = cluster_levels([ma], 160, 4998, 2)[0]
after = cluster_levels([ma, sample_line().to_level()], 160, 4998, 2)[0]
assert before.score == 12
assert after.score == 16
def test_an_annotation_is_never_a_level(tmp_path):
# Comments live with the lines so they share numbering, filtering and
# deletion — but a comment reaching levels() would join a confluence
# cluster and fire a push notification about a piece of text.
from app.analysis.manual_lines import ManualLine, ManualLineStore
from app.analysis.levels import Side
from app.bars.models import Timeframe
store = ManualLineStore(tmp_path / "lines.json")
common = dict(tf=Timeframe.M1, side=Side.SUPPORT, anchor_p=100.0,
anchor_t=1000, last_t=2000, created_at=1000)
store.add(ManualLine(id="ml_level", slope=0.0, **common))
store.add(ManualLine(id="ml_note", slope=0.0, kind="comment",
note="watch this", **common))
store.add(ManualLine(id="ml_symbol", slope=0.0, kind="symbol",
icon="skull", note="Skull", **common))
assert [level.id for level in store.levels()] == ["ml_level"]
assert [line.id for line in store.drawings()] == ["ml_level", "ml_note", "ml_symbol"]
def test_drawing_kind_is_derived_for_lines_saved_before_comments(tmp_path):
from app.analysis.manual_lines import ManualLine, ManualLineStore
from app.analysis.levels import Side
from app.bars.models import Timeframe
store = ManualLineStore(tmp_path / "lines.json")
common = dict(tf=Timeframe.M1, side=Side.SUPPORT, anchor_p=100.0,
anchor_t=1000, last_t=2000, created_at=1000)
flat = store.add(ManualLine(id="ml_flat", slope=0.0, **common))
sloped = store.add(ManualLine(id="ml_sloped", slope=0.5, **common))
assert flat.drawing_kind == "level"
assert sloped.drawing_kind == "trendline"
def test_a_mark_scale_survives_json_and_does_not_move_its_anchor(tmp_path):
path = tmp_path / "lines.json"
store = ManualLineStore(path)
common = dict(tf=Timeframe.M1, side=Side.SUPPORT, anchor_p=100.0,
anchor_t=1000, last_t=2000, created_at=1000)
store.add(ManualLine(id="ml_mark", slope=0.0, kind="symbol",
icon="skull", note="Skull", scale=2.0, **common))
raw = ManualLine(
id="ml_old", slope=0.0, kind="symbol", icon="play", note="Go", **common,
).to_dict()
del raw["scale"]
store.add(ManualLine.from_dict(raw))
loaded = ManualLineStore(path)
mark = loaded.lines["ml_mark"]
old = loaded.lines["ml_old"]
assert mark.scale == 2.0
assert (mark.anchor_t, mark.anchor_p) == (1000, 100.0)
assert old.scale == 1.0
assert (old.anchor_t, old.anchor_p) == (1000, 100.0)
assert [level.id for level in loaded.levels()] == []
def test_every_drawing_gets_a_number_including_comments(tmp_path):
from app.analysis.manual_lines import ManualLine, ManualLineStore
from app.analysis.levels import Side
from app.bars.models import Timeframe
store = ManualLineStore(tmp_path / "lines.json")
common = dict(tf=Timeframe.M1, side=Side.SUPPORT, anchor_p=100.0,
anchor_t=1000, last_t=2000, created_at=1000)
first = store.add(ManualLine(id="ml_a", slope=0.0, **common))
note = store.add(ManualLine(id="ml_b", slope=0.0, kind="comment", **common))
third = store.add(ManualLine(id="ml_c", slope=1.0, **common))
assert [first.number, note.number, third.number] == [1, 2, 3]