import pandas as pd from fastapi.testclient import TestClient import TerraFin.interface.chart.routes as chart_routes from TerraFin.data import DataFactory from TerraFin.data.cache.registry import reset_cache_manager from TerraFin.data.contracts import HistoryChunk from TerraFin.data.contracts.dataframes import TimeSeriesDataFrame from TerraFin.interface.chart.chart_view import apply_view from TerraFin.interface.chart.formatters import build_multi_payload as _build_multi_payload from TerraFin.interface.chart.formatters import build_source_payload as _build_source_payload from TerraFin.interface.chart.formatters import format_dataframe from TerraFin.interface.server import create_app def test_apply_view_returns_line_payload_for_invalid_or_empty_source() -> None: assert apply_view({}, "daily") == {"mode": "multi", "series": [], "dataLength": 0} assert apply_view({"mode": "multi", "series": "bad"}, "weekly") == {"mode": "multi", "series": [], "dataLength": 0} def test_apply_view_keeps_daily_points_for_line_data() -> None: payload = { "mode": "multi", "series": [ { "id": "S1", "seriesType": "line", "data": [ {"time": "2026-01-01", "value": 10.0}, {"time": "2026-01-02", "value": 11.0}, {"time": "2026-01-03", "value": 12.5}, ], } ], } out = apply_view(payload, "daily") assert out["mode"] == "multi" assert len(out["series"]) == 1 assert len(out["series"][0]["data"]) == 3 assert out["series"][0]["data"][0]["time"] == "2026-01-01" assert out["series"][0]["data"][-1]["value"] == 12.5 def test_apply_view_resamples_weekly_monthly_yearly_for_line_data() -> None: payload = { "mode": "multi", "series": [ { "id": "S1", "seriesType": "line", "data": [ {"time": "2026-01-01", "value": 10.0}, {"time": "2026-01-07", "value": 12.0}, {"time": "2026-01-15", "value": 14.0}, {"time": "2026-02-03", "value": 20.0}, ], } ], } weekly = apply_view(payload, "weekly") monthly = apply_view(payload, "monthly") yearly = apply_view(payload, "yearly") assert weekly["mode"] == "multi" assert len(weekly["series"][0]["data"]) >= 3 assert len(monthly["series"][0]["data"]) == 2 assert monthly["series"][0]["data"][0]["value"] == 14.0 assert monthly["series"][0]["data"][1]["value"] == 20.0 assert len(yearly["series"][0]["data"]) == 1 assert yearly["series"][0]["data"][0]["value"] == 20.0 def test_format_dataframe_contract_for_line_and_ohlc_source() -> None: line_df = pd.DataFrame({"time": ["2026-01-01", "2026-01-02"], "close": [100.0, 101.0]}) ohlc_df = pd.DataFrame( { "time": ["2026-01-01", "2026-01-02"], "open": [99.5, 100.5], "high": [101.0, 102.0], "low": [99.0, 100.0], "close": [100.0, 101.0], } ) line_payload = format_dataframe(line_df) assert line_payload["mode"] == "multi" assert line_payload["series"][0]["seriesType"] == "line" assert line_payload["series"][0]["data"][0] == {"time": "2026-01-01", "value": 100.0} ohlc_payload = format_dataframe(ohlc_df) assert ohlc_payload["mode"] == "multi" assert ohlc_payload["series"][0]["seriesType"] == "candlestick" assert ohlc_payload["series"][0]["data"][0]["open"] == 99.5 assert ohlc_payload["series"][0]["data"][0]["close"] == 100.0 assert format_dataframe(pd.DataFrame()) == {"mode": "multi", "series": [], "dataLength": 0} assert format_dataframe(pd.DataFrame({"time": ["2026-01-01"]})) == {"mode": "multi", "series": [], "dataLength": 0} def test_format_dataframe_includes_zone_metadata_for_vol_regime() -> None: df = TimeSeriesDataFrame( pd.DataFrame( { "time": ["2026-01-01", "2026-01-02", "2026-01-03"], "close": [12.0, 42.0, 88.0], } ) ) df.name = "Vol Regime" df.chart_meta = { "zones": [ {"from": 0, "to": 20, "color": "rgba(76,175,80,0.15)"}, {"from": 80, "to": 100, "color": "rgba(244,67,54,0.15)"}, ] } payload = format_dataframe(df) assert payload["series"][0]["zones"] == [ {"from": 0.0, "to": 20.0, "color": "rgba(76,175,80,0.15)"}, {"from": 80.0, "to": 100.0, "color": "rgba(244,67,54,0.15)"}, ] def test_apply_view_preserves_zone_metadata_for_line_series() -> None: payload = { "mode": "multi", "series": [ { "id": "Vol Regime", "seriesType": "line", "data": [ {"time": "2026-01-01", "value": 10.0}, {"time": "2026-01-07", "value": 12.0}, {"time": "2026-01-15", "value": 14.0}, {"time": "2026-02-03", "value": 20.0}, ], "zones": [ {"from": 0, "to": 20, "color": "rgba(76,175,80,0.15)"}, {"from": 80, "to": 100, "color": "rgba(244,67,54,0.15)"}, ], } ], } monthly = apply_view(payload, "monthly") assert monthly["series"][0]["zones"] == payload["series"][0]["zones"] def test_chart_view_endpoint_contract_and_transform() -> None: reset_cache_manager() client = TestClient(create_app()) headers = {"X-Session-ID": "chart-transform-contract"} source = { "mode": "multi", "series": [ { "id": "S&P 500", "seriesType": "line", "data": [ {"time": "2026-01-01", "value": 10.5}, {"time": "2026-01-11", "value": 11.8}, {"time": "2026-02-05", "value": 12.4}, ], } ], } write_response = client.post("/chart/api/chart-data", json=source, headers=headers) assert write_response.status_code == 200 assert write_response.json()["ok"] is True view_response = client.post("/chart/api/chart-view", json={"view": "monthly"}, headers=headers) assert view_response.status_code == 200 payload = view_response.json() assert payload["ok"] is True assert payload["view"] == "monthly" assert payload["dataLength"] >= 1 assert isinstance(payload["entries"], list) chart_payload = client.get("/chart/api/chart-data", headers=headers).json() assert chart_payload["mode"] == "multi" assert chart_payload["dataLength"] >= 1 assert isinstance(chart_payload["entries"], list) assert chart_payload["historyBySeries"]["S&P 500"]["isComplete"] is True assert chart_payload["historyBySeries"]["S&P 500"]["hasOlder"] is False def test_chart_view_endpoint_keeps_candlestick_series_when_valid() -> None: reset_cache_manager() client = TestClient(create_app()) headers = {"X-Session-ID": "chart-candle-contract"} source = { "mode": "multi", "series": [ { "id": "OHLC", "seriesType": "candlestick", "data": [ {"time": "2026-01-01", "open": 10.0, "high": 11.0, "low": 9.5, "close": 10.4}, {"time": "2026-01-10", "open": 10.4, "high": 12.0, "low": 10.1, "close": 11.7}, {"time": "2026-02-03", "open": 11.7, "high": 12.9, "low": 11.5, "close": 12.5}, ], } ], } write_response = client.post("/chart/api/chart-data", json=source, headers=headers) assert write_response.status_code == 200 view_response = client.post("/chart/api/chart-view", json={"view": "monthly"}, headers=headers) assert view_response.status_code == 200 chart_payload = client.get("/chart/api/chart-data", headers=headers).json() assert chart_payload["series"][0]["seriesType"] == "candlestick" assert "open" in chart_payload["series"][0]["data"][0] def test_chart_mfd_defaults_to_single_medium_horizon_series() -> None: dates = pd.date_range("2025-01-01", periods=320, freq="D") candle_data = [ { "time": dt.strftime("%Y-%m-%d"), "open": 100.0 + idx * 0.3, "high": 100.5 + idx * 0.3, "low": 99.5 + idx * 0.3, "close": 100.2 + idx * 0.3, } for idx, dt in enumerate(dates) ] overlays = chart_routes.compute_mandelbrot_fractal_dimension(candle_data) assert [overlay["id"] for overlay in overlays] == ["MFD 130"] assert overlays[0]["priceScaleId"] == "mfd" assert overlays[0]["indicatorGroup"] == "mfd" assert overlays[0]["priceLevels"] == [ {"price": 1.0, "color": "#9e9e9e", "title": "Smooth"}, {"price": 1.5, "color": "#bdbdbd", "title": "Random"}, {"price": 2.0, "color": "#90a4ae", "title": "Choppy"}, ] def test_chart_view_reuses_cached_indicators_for_same_transformed_source(monkeypatch) -> None: counts = { "ma": 0, "bb": 0, "rsi": 0, "macd": 0, "realized_vol": 0, "range_vol": 0, } def _wrap(name: str, fn): def _inner(*args, **kwargs): counts[name] += 1 return fn(*args, **kwargs) return _inner monkeypatch.setattr(chart_routes, "compute_moving_averages", _wrap("ma", chart_routes.compute_moving_averages)) monkeypatch.setattr(chart_routes, "compute_bollinger_bands", _wrap("bb", chart_routes.compute_bollinger_bands)) monkeypatch.setattr(chart_routes, "compute_rsi", _wrap("rsi", chart_routes.compute_rsi)) monkeypatch.setattr(chart_routes, "compute_macd", _wrap("macd", chart_routes.compute_macd)) monkeypatch.setattr( chart_routes, "compute_realized_volatility", _wrap("realized_vol", chart_routes.compute_realized_volatility), ) monkeypatch.setattr( chart_routes, "compute_range_volatility", _wrap("range_vol", chart_routes.compute_range_volatility), ) reset_cache_manager() client = TestClient(create_app()) headers = {"X-Session-ID": "chart-indicator-cache"} dates = pd.date_range("2025-01-01", periods=90, freq="D") source = { "mode": "multi", "series": [ { "id": "OHLC", "seriesType": "candlestick", "data": [ { "time": dt.strftime("%Y-%m-%d"), "open": 100.0 + idx, "high": 101.0 + idx, "low": 99.0 + idx, "close": 100.5 + idx, } for idx, dt in enumerate(dates) ], } ], } write_response = client.post("/chart/api/chart-data", json=source, headers=headers) assert write_response.status_code == 200 first_monthly = client.post("/chart/api/chart-view", json={"view": "monthly"}, headers=headers) assert first_monthly.status_code == 200 counts_after_first_monthly = dict(counts) second_monthly = client.post("/chart/api/chart-view", json={"view": "monthly"}, headers=headers) assert second_monthly.status_code == 200 assert counts == counts_after_first_monthly def test_build_multi_payload_assigns_price_scale_id_for_one_ohlc_plus_lines() -> None: """1 OHLC + N lines: candlestick stays candlestick, gets right; lines get overlay-0, overlay-1, ...""" ohlc_df = pd.DataFrame( { "time": ["2026-01-01", "2026-01-02"], "open": [99.5, 100.5], "high": [101.0, 102.0], "low": [99.0, 100.0], "close": [100.0, 101.0], } ) line_df1 = pd.DataFrame({"time": ["2026-01-01", "2026-01-02"], "close": [10.0, 11.0]}) line_df2 = pd.DataFrame({"time": ["2026-01-01", "2026-01-02"], "close": [20.0, 21.0]}) ohlc_ts = TimeSeriesDataFrame(ohlc_df) ohlc_ts.name = "OHLC" line_ts1 = TimeSeriesDataFrame(line_df1) line_ts1.name = "Line1" line_ts2 = TimeSeriesDataFrame(line_df2) line_ts2.name = "Line2" payload = _build_multi_payload([ohlc_ts, line_ts1, line_ts2]) assert len(payload["series"]) == 3 assert payload["series"][0]["seriesType"] == "candlestick" assert payload["series"][0]["priceScaleId"] == "right" assert payload["series"][1]["seriesType"] == "line" assert payload["series"][1]["priceScaleId"] == "left" assert payload["series"][2]["seriesType"] == "line" assert payload["series"][2]["priceScaleId"] == "overlay-0" def test_build_multi_payload_assigns_separate_visible_scales_for_multi_line_series() -> None: line_df1 = pd.DataFrame({"time": ["2026-01-01", "2026-01-02"], "close": [18.0, 22.0]}) line_df2 = pd.DataFrame({"time": ["2026-01-01", "2026-01-02"], "close": [105.0, 115.0]}) line_df3 = pd.DataFrame({"time": ["2026-01-01", "2026-01-02"], "close": [3.0, 4.0]}) line_ts1 = TimeSeriesDataFrame(line_df1) line_ts1.name = "VIX" line_ts2 = TimeSeriesDataFrame(line_df2) line_ts2.name = "MOVE" line_ts3 = TimeSeriesDataFrame(line_df3) line_ts3.name = "Spread" payload = _build_multi_payload([line_ts1, line_ts2, line_ts3]) assert len(payload["series"]) == 3 assert payload["series"][0]["seriesType"] == "line" assert payload["series"][0]["priceScaleId"] == "left" assert payload["series"][1]["seriesType"] == "line" assert payload["series"][1]["priceScaleId"] == "right" assert payload["series"][2]["seriesType"] == "line" assert payload["series"][2]["priceScaleId"] == "overlay-0" def test_build_source_payload_preserves_raw_candlesticks() -> None: ohlc_df1 = pd.DataFrame( { "time": ["2026-01-01", "2026-01-02"], "open": [99.5, 100.5], "high": [101.0, 102.0], "low": [99.0, 100.0], "close": [100.0, 101.0], } ) ohlc_df2 = pd.DataFrame( { "time": ["2026-01-01", "2026-01-02"], "open": [199.5, 200.5], "high": [201.0, 202.0], "low": [199.0, 200.0], "close": [200.0, 201.0], } ) line_df = pd.DataFrame({"time": ["2026-01-01", "2026-01-02"], "close": [10.0, 11.0]}) ohlc_ts1 = TimeSeriesDataFrame(ohlc_df1) ohlc_ts1.name = "OHLC1" ohlc_ts2 = TimeSeriesDataFrame(ohlc_df2) ohlc_ts2.name = "OHLC2" line_ts = TimeSeriesDataFrame(line_df) line_ts.name = "Line" payload = _build_source_payload([ohlc_ts1, ohlc_ts2, line_ts]) assert payload["forcePercentage"] if "forcePercentage" in payload else False is False assert [series["seriesType"] for series in payload["series"]] == ["candlestick", "candlestick", "line"] def test_progressive_seed_and_backfill_chart_series_contract(monkeypatch) -> None: zones = [ {"from": 0.0, "to": 20.0, "color": "rgba(76,175,80,0.15)"}, {"from": 80.0, "to": 100.0, "color": "rgba(244,67,54,0.15)"}, ] def _recent_history(self, _name: str, *, period: str = "3y") -> HistoryChunk: frame = TimeSeriesDataFrame( pd.DataFrame( { "time": ["2024-01-01", "2025-01-01", "2026-01-01"], "close": [100.0, 120.0, 140.0], } ), chart_meta={"zones": zones}, ) return HistoryChunk( frame=frame, loaded_start="2024-01-01", loaded_end="2026-01-01", requested_period=period, is_complete=False, has_older=True, source_version="test", ) def _backfill_history(self, _name: str, *, loaded_start: str | None = None) -> HistoryChunk: assert loaded_start == "2024-01-01" frame = TimeSeriesDataFrame( pd.DataFrame( { "time": ["2021-01-01", "2022-01-01", "2023-01-01"], "close": [70.0, 80.0, 90.0], } ), chart_meta={"zones": zones}, ) return HistoryChunk( frame=frame, loaded_start="2021-01-01", loaded_end="2026-01-01", requested_period=None, is_complete=True, has_older=False, source_version="test", ) monkeypatch.setattr(DataFactory, "get_recent_history", _recent_history) monkeypatch.setattr(DataFactory, "get_full_history_backfill", _backfill_history) reset_cache_manager() client = TestClient(create_app()) headers = {"X-Session-ID": "chart-progressive"} seed_response = client.post( "/chart/api/chart-series/progressive/set", json={"name": "AAPL", "pinned": True, "seedPeriod": "3y"}, headers=headers, ) assert seed_response.status_code == 200 seed_payload = seed_response.json() assert seed_payload["ok"] is True assert seed_payload["entries"] == [{"name": "AAPL", "pinned": True}] assert seed_payload["historyBySeries"]["AAPL"]["loadedStart"] == "2024-01-01" assert seed_payload["historyBySeries"]["AAPL"]["hasOlder"] is True assert seed_payload["historyBySeries"]["AAPL"]["backfillInFlight"] is True assert seed_payload["series"][0]["zones"] == zones backfill_response = client.post( "/chart/api/chart-series/progressive/backfill", json={"name": "AAPL", "requestToken": seed_payload["requestToken"]}, headers=headers, ) assert backfill_response.status_code == 200 backfill_payload = backfill_response.json() assert backfill_payload["ok"] is True assert backfill_payload["historyBySeries"]["AAPL"]["loadedStart"] == "2021-01-01" assert backfill_payload["historyBySeries"]["AAPL"]["isComplete"] is True assert backfill_payload["historyBySeries"]["AAPL"]["hasOlder"] is False assert backfill_payload["historyBySeries"]["AAPL"]["backfillInFlight"] is False assert any(series["id"] == "AAPL" for series in backfill_payload["mutation"]["upsertSeries"]) assert backfill_payload["mutation"]["upsertSeries"][0]["zones"] == zones chart_payload = client.get("/chart/api/chart-data", headers=headers).json() assert chart_payload["entries"] == [{"name": "AAPL", "pinned": True}] assert chart_payload["series"][0]["data"][0]["time"] == "2021-01-01" assert chart_payload["series"][0]["zones"] == zones def test_progressive_backfill_ignores_stale_request_token(monkeypatch) -> None: def _recent_history(self, _name: str, *, period: str = "3y") -> HistoryChunk: frame = TimeSeriesDataFrame(pd.DataFrame({"time": ["2024-01-01", "2025-01-01"], "close": [100.0, 110.0]})) return HistoryChunk( frame=frame, loaded_start="2024-01-01", loaded_end="2025-01-01", requested_period=period, is_complete=False, has_older=True, source_version="test", ) monkeypatch.setattr(DataFactory, "get_recent_history", _recent_history) monkeypatch.setattr( DataFactory, "get_full_history_backfill", lambda self, _name, *, loaded_start=None: (_ for _ in ()).throw(AssertionError("should not backfill stale")), ) reset_cache_manager() client = TestClient(create_app()) headers = {"X-Session-ID": "chart-progressive-stale"} seed_response = client.post( "/chart/api/chart-series/progressive/set", json={"name": "AAPL", "pinned": True, "seedPeriod": "3y"}, headers=headers, ) assert seed_response.status_code == 200 stale_response = client.post( "/chart/api/chart-series/progressive/backfill", json={"name": "AAPL", "requestToken": "stale-token"}, headers=headers, ) assert stale_response.status_code == 200 stale_payload = stale_response.json() assert stale_payload["ok"] is False assert stale_payload["stale"] is True chart_payload = client.get("/chart/api/chart-data", headers=headers).json() assert chart_payload["series"][0]["data"][0]["time"] == "2024-01-01" def test_chart_data_preserves_price_scale_id_for_mixed_ohlc_and_lines() -> None: """POST mixed OHLC + lines with priceScaleId; GET and chart_view preserve priceScaleId.""" reset_cache_manager() client = TestClient(create_app()) headers = {"X-Session-ID": "chart-multiscale"} source = { "mode": "multi", "series": [ { "id": "OHLC", "seriesType": "candlestick", "priceScaleId": "right", "data": [ {"time": "2026-01-01", "open": 5000, "high": 5010, "low": 4990, "close": 5005}, {"time": "2026-01-02", "open": 5005, "high": 5020, "low": 5000, "close": 5015}, ], }, { "id": "Line1", "seriesType": "line", "priceScaleId": "overlay-0", "data": [{"time": "2026-01-01", "value": 10}, {"time": "2026-01-02", "value": 20}], }, { "id": "Line2", "seriesType": "line", "priceScaleId": "overlay-1", "data": [{"time": "2026-01-01", "value": 30}, {"time": "2026-01-02", "value": 40}], }, ], } post_response = client.post("/chart/api/chart-data", json=source, headers=headers) assert post_response.status_code == 200 get_payload = client.get("/chart/api/chart-data", headers=headers).json() assert get_payload["series"][0]["priceScaleId"] == "right" assert get_payload["series"][1]["priceScaleId"] == "overlay-0" assert get_payload["series"][2]["priceScaleId"] == "overlay-1" view_response = client.post("/chart/api/chart-view", json={"view": "daily"}, headers=headers) assert view_response.status_code == 200 view_payload = client.get("/chart/api/chart-data", headers=headers).json() assert view_payload["series"][0]["priceScaleId"] == "right" assert view_payload["series"][1]["priceScaleId"] == "overlay-0" assert view_payload["series"][2]["priceScaleId"] == "overlay-1" assert view_payload["historyBySeries"]["OHLC"]["isComplete"] is True assert view_payload["historyBySeries"]["Line1"]["isComplete"] is True assert view_payload["historyBySeries"]["Line2"]["isComplete"] is True def test_chart_data_remove_keeps_forced_percentage_mode_when_raw_three_candles_remain() -> None: reset_cache_manager() client = TestClient(create_app()) headers = {"X-Session-ID": "chart-force-pct-remove"} source = { "mode": "multi", "series": [ { "id": "SPY", "seriesType": "candlestick", "data": [ {"time": "2026-01-01", "open": 100, "high": 102, "low": 99, "close": 101}, {"time": "2026-01-02", "open": 101, "high": 103, "low": 100, "close": 102}, ], }, { "id": "QQQ", "seriesType": "candlestick", "data": [ {"time": "2026-01-01", "open": 200, "high": 202, "low": 199, "close": 201}, {"time": "2026-01-02", "open": 201, "high": 203, "low": 200, "close": 202}, ], }, { "id": "DIA", "seriesType": "candlestick", "data": [ {"time": "2026-01-01", "open": 300, "high": 302, "low": 299, "close": 301}, {"time": "2026-01-02", "open": 301, "high": 303, "low": 300, "close": 302}, ], }, { "id": "Fear & Greed", "seriesType": "line", "data": [ {"time": "2026-01-01", "value": 40}, {"time": "2026-01-02", "value": 45}, ], }, ], } write_response = client.post("/chart/api/chart-data", json=source, headers=headers) assert write_response.status_code == 200 written = write_response.json() assert written["forcePercentage"] is True remove_response = client.post("/chart/api/chart-series/remove", json={"name": "Fear & Greed"}, headers=headers) assert remove_response.status_code == 200 remove_payload = remove_response.json() assert remove_payload["mutation"]["forcePercentage"] is True chart_payload = client.get("/chart/api/chart-data", headers=headers).json() assert chart_payload["forcePercentage"] is True assert len(chart_payload["series"]) == 3 assert all(series["returnSeries"] is True for series in chart_payload["series"]) def test_chart_view_preserves_two_candlestick_return_layout() -> None: reset_cache_manager() client = TestClient(create_app()) headers = {"X-Session-ID": "chart-two-candle-percent-view"} source = { "mode": "multi", "series": [ { "id": "SPY", "seriesType": "candlestick", "data": [ {"time": "2022-01-03", "open": 100, "high": 102, "low": 99, "close": 101}, {"time": "2023-01-03", "open": 110, "high": 112, "low": 109, "close": 111}, {"time": "2024-01-03", "open": 120, "high": 122, "low": 119, "close": 121}, {"time": "2025-01-03", "open": 130, "high": 132, "low": 129, "close": 131}, ], }, { "id": "QQQ", "seriesType": "candlestick", "data": [ {"time": "2022-01-03", "open": 200, "high": 202, "low": 199, "close": 201}, {"time": "2023-01-03", "open": 210, "high": 212, "low": 209, "close": 211}, {"time": "2024-01-03", "open": 220, "high": 222, "low": 219, "close": 221}, {"time": "2025-01-03", "open": 230, "high": 232, "low": 229, "close": 231}, ], }, ], } write_response = client.post("/chart/api/chart-data", json=source, headers=headers) assert write_response.status_code == 200 initial_payload = write_response.json() assert initial_payload["forcePercentage"] is False assert all(series["seriesType"] == "candlestick" for series in initial_payload["series"]) assert all(series["returnSeries"] is True for series in initial_payload["series"]) view_response = client.post("/chart/api/chart-view", json={"view": "weekly"}, headers=headers) assert view_response.status_code == 200 chart_payload = client.get("/chart/api/chart-data", headers=headers).json() assert chart_payload["forcePercentage"] is False assert len(chart_payload["series"]) == 2 assert all(series["seriesType"] == "candlestick" for series in chart_payload["series"]) assert all(series["returnSeries"] is True for series in chart_payload["series"]) def test_chart_data_write_initializes_standard_history_and_named_series(monkeypatch) -> None: def _fake_recent_history(self, name: str, *, period: str = "3y") -> HistoryChunk: _ = self assert name == "QQQ" assert period == "3y" return HistoryChunk( frame=TimeSeriesDataFrame( pd.DataFrame( { "time": ["2026-01-01", "2026-01-02"], "close": [200.0, 202.0], } ) ), loaded_start="2026-01-01", loaded_end="2026-01-02", requested_period=period, is_complete=False, has_older=True, source_version="test", ) monkeypatch.setattr(DataFactory, "get_recent_history", _fake_recent_history) reset_cache_manager() client = TestClient(create_app()) headers = {"X-Session-ID": "chart-direct-standard"} source = { "mode": "multi", "series": [ { "id": "S&P 500", "seriesType": "line", "data": [ {"time": "2026-01-01", "value": 5000.0}, {"time": "2026-01-02", "value": 5010.0}, ], } ], } write_response = client.post("/chart/api/chart-data", json=source, headers=headers) assert write_response.status_code == 200 write_payload = write_response.json() assert write_payload["historyBySeries"]["S&P 500"]["isComplete"] is True assert write_payload["historyBySeries"]["S&P 500"]["hasOlder"] is False assert write_payload["historyBySeries"]["S&P 500"]["backfillInFlight"] is False add_response = client.post("/chart/api/chart-series/add", json={"name": "QQQ"}, headers=headers) assert add_response.status_code == 200 add_payload = add_response.json() assert add_payload["ok"] is True assert add_payload["mutation"]["entries"] == [ {"name": "S&P 500", "pinned": False}, {"name": "QQQ", "pinned": False}, ] assert add_payload["mutation"]["seriesOrder"] == ["S&P 500", "QQQ"] assert add_payload["historyBySeries"]["S&P 500"]["isComplete"] is True assert add_payload["historyBySeries"]["QQQ"]["loadedStart"] == "2026-01-01" assert add_payload["historyBySeries"]["QQQ"]["backfillInFlight"] is True def test_chart_series_set_returns_snapshot_payload_and_entries(monkeypatch) -> None: def _fake_recent_history(self, name: str, *, period: str = "3y") -> HistoryChunk: _ = self assert name == "spy" assert period == "3y" return HistoryChunk( frame=TimeSeriesDataFrame( pd.DataFrame( { "time": ["2026-01-01", "2026-01-02"], "close": [100.0, 101.0], } ) ), loaded_start="2026-01-01", loaded_end="2026-01-02", requested_period=period, is_complete=False, has_older=True, source_version="test", ) monkeypatch.setattr(DataFactory, "get_recent_history", _fake_recent_history) reset_cache_manager() client = TestClient(create_app()) response = client.post("/chart/api/chart-series/set", json={"name": "spy", "pinned": True}) assert response.status_code == 200 payload = response.json() assert payload["ok"] is True assert payload["mode"] == "multi" assert payload["dataLength"] == 2 assert payload["series"][0]["id"] == "SPY" assert payload["entries"] == [{"name": "SPY", "pinned": True}] assert payload["historyBySeries"]["SPY"]["loadedStart"] == "2026-01-01" assert payload["historyBySeries"]["SPY"]["backfillInFlight"] is True assert payload["requestToken"] def test_chart_series_add_and_remove_return_mutation_patches(monkeypatch) -> None: def _fake_recent_history(self, name: str, *, period: str = "3y") -> HistoryChunk: _ = self series_name = name.upper() if series_name not in {"SPY", "QQQ"}: raise AssertionError(series_name) assert period == "3y" return HistoryChunk( frame=TimeSeriesDataFrame( pd.DataFrame( { "time": ["2026-01-01", "2026-01-02"], "close": [100.0, 101.0] if series_name == "SPY" else [200.0, 202.0], } ) ), loaded_start="2026-01-01", loaded_end="2026-01-02", requested_period=period, is_complete=False, has_older=True, source_version="test", ) monkeypatch.setattr(DataFactory, "get_recent_history", _fake_recent_history) reset_cache_manager() client = TestClient(create_app()) headers = {"X-Session-ID": "chart-series-mutation"} seed = client.post("/chart/api/chart-series/set", json={"name": "spy", "pinned": True}, headers=headers) assert seed.status_code == 200 add_response = client.post("/chart/api/chart-series/add", json={"name": "qqq"}, headers=headers) assert add_response.status_code == 200 add_payload = add_response.json() assert add_payload["ok"] is True assert add_payload["mutation"]["entries"] == [ {"name": "SPY", "pinned": True}, {"name": "QQQ", "pinned": False}, ] assert add_payload["mutation"]["seriesOrder"] == ["SPY", "QQQ"] assert add_payload["historyBySeries"]["QQQ"]["loadedStart"] == "2026-01-01" assert add_payload["historyBySeries"]["QQQ"]["backfillInFlight"] is True assert [series["id"] for series in add_payload["mutation"]["upsertSeries"]] == ["SPY", "QQQ"] remove_response = client.post("/chart/api/chart-series/remove", json={"name": "QQQ"}, headers=headers) assert remove_response.status_code == 200 remove_payload = remove_response.json() assert remove_payload["ok"] is True assert remove_payload["mutation"]["entries"] == [{"name": "SPY", "pinned": True}] assert remove_payload["mutation"]["removedSeriesIds"] == ["QQQ"]