from fastapi import FastAPI from fastapi.testclient import TestClient from app.analysis.confluence import cluster_levels from app.analysis.levels import Level, LevelKind, Side from app.analysis.manual_lines import ManualLine from app.api.ws import router as ws_router, snapshot from app.bars.models import Bar, Timeframe from app.config import Settings from app.runtime import Runtime def ma(id_: str, period: int, price: float) -> Level: return Level( id_, LevelKind.MA, Timeframe.M1, Side.RESISTANCE, 1, 1, f"SMA{period}", 1000, price, 0, None, 0, 1000, 1000, False, False, period, ) def test_drawings_off_hides_manual_lines_from_confluence(): from app.api.ws import level_enabled line = Level( "ml1", LevelKind.MANUAL, Timeframe.D1, Side.RESISTANCE, 12, 1, "line", 1000, 100.0, 0, None, 0, 1000, 1000, False, False, ) assert level_enabled(line, {"manual": True, "drawings": True}) assert not level_enabled(line, {"manual": True, "drawings": False}) assert not level_enabled(line, {"manual": False, "drawings": True}) # Prefs written before the drawings key still show manuals. assert level_enabled(line, {"manual": True}) def test_websocket_layer_preferences_are_isolated_per_connection(tmp_path): app = FastAPI() app.include_router(ws_router) runtime = Runtime(Settings(manual_lines_path=tmp_path / "manual_lines.json")) runtime.levels = [ma("ma20", 20, 100.0), ma("ma50", 50, 100.1)] runtime.price = 99.9 runtime.atr15 = 1 runtime.stream.last_bar_t = 1000 runtime.clusters = cluster_levels(runtime.levels, 1000, runtime.price, runtime.atr15) app.state.runtime = runtime client = TestClient(app) original_levels = list(runtime.levels) original_clusters = list(runtime.clusters) with client.websocket_connect("/ws") as filtered, client.websocket_connect("/ws") as default: assert len(filtered.receive_json()["clusters"]) == 1 assert len(default.receive_json()["clusters"]) == 1 filtered.send_json( { "type": "prefs", "hidden_levels_score": False, "enabled": {"ma": {"1m": [20]}}, } ) assert filtered.receive_json()["clusters"] == [] default.send_json({"type": "subscribe", "tf": "1m"}) assert len(default.receive_json()["clusters"]) == 1 assert runtime.levels == original_levels assert runtime.clusters == original_clusters def test_a_1m_snapshot_is_not_capped_shorter_than_the_store(tmp_path): runtime = Runtime(Settings(manual_lines_path=tmp_path / "manual_lines.json")) for minute in range(1001): runtime.store.put(Bar(Timeframe.M1, 60 * minute, 1, 1, 1, 1, 1, True, "/ES", "test")) assert len(snapshot(runtime, Timeframe.M1)["bars"]) == 1001 def test_quote_change_always_uses_the_daily_session_open(tmp_path): runtime = Runtime(Settings(manual_lines_path=tmp_path / "manual_lines.json")) runtime.store.put(Bar(Timeframe.D1, 1000, 6123.25, 6140, 6100, 6130, 1, False, "/ES", "test")) runtime.store.put(Bar(Timeframe.H1, 2000, 6130, 6150, 6120, 6145, 1, False, "/ES", "test")) message = snapshot(runtime, Timeframe.H1) assert message["session_open"] == 6123.25 def test_snapshot_carries_source_times_for_manual_trendlines(tmp_path): runtime = Runtime(Settings( manual_lines_path=tmp_path / "manual_lines.json", trendline_source_geometry=True, )) runtime.manual_lines.add(ManualLine( "ml_30m", Timeframe.M30, Side.SUPPORT, 1000, 100, 1 / 1800, 2800, 2800, )) runtime.store.put(Bar(Timeframe.M30, 1000, 100, 101, 99, 100, 1, True, "/ES", "test")) runtime.store.put(Bar(Timeframe.M30, 2800, 100, 101, 99, 100, 1, True, "/ES", "test")) runtime.rebuild_levels() geometry = snapshot(runtime, Timeframe.M1)["trendline_geometry"] assert geometry["mode"] == "source_tf" source = geometry["series"]["30m"] assert source["times"] == [1000, 2800] assert source["duration"] == 1800 assert source["future_times"][:2] == [4600, 6400] assert source["future_durations"][:2] == [1800, 1800] assert len(source["future_times"]) == 180 def test_daily_snapshot_carries_session_aware_display_future_times(tmp_path): runtime = Runtime(Settings(manual_lines_path=tmp_path / "manual_lines.json")) thursday = 1786658400 # 2026-08-13 18:00 America/New_York runtime.store.put(Bar(Timeframe.D1, thursday, 1, 1, 1, 1, 1, True, "/ES", "test")) message = snapshot(runtime, Timeframe.D1) assert len(message["future_times"]) == 180 assert message["future_times"][0] == 1786917600 # Sunday 18:00 Eastern def test_snapshot_rollback_gate_omits_source_geometry(tmp_path): runtime = Runtime(Settings( manual_lines_path=tmp_path / "manual_lines.json", trendline_source_geometry=False, )) assert snapshot(runtime, Timeframe.M1)["trendline_geometry"] == { "mode": "legacy", "series": {}, } def test_browser_can_request_full_geometry_after_a_source_delta_gap(tmp_path): app = FastAPI() app.include_router(ws_router) runtime = Runtime(Settings( manual_lines_path=tmp_path / "manual_lines.json", trendline_source_geometry=True, )) app.state.runtime = runtime with TestClient(app).websocket_connect("/ws") as websocket: websocket.receive_json() websocket.send_json({"type": "trendline_geometry"}) assert websocket.receive_json() == { "type": "trendline_geometry", "geometry": {"mode": "source_tf", "series": {}}, }