| 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"] |
|
|