From fdee661d47bc30c4d3e51bbef61273b9700297e8 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Fri, 10 Jul 2026 15:07:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(watchlist):=20=E4=BF=9D=E7=95=99=E5=86=B7?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E8=87=AA=E9=80=89=E5=8D=A0=E4=BD=8D=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/watchlist.py | 10 +-- backend/tests/test_watchlist_enriched_join.py | 75 +++++++++++++++++-- 2 files changed, 74 insertions(+), 11 deletions(-) diff --git a/backend/app/api/watchlist.py b/backend/app/api/watchlist.py index 4e616d0..ef984e1 100644 --- a/backend/app/api/watchlist.py +++ b/backend/app/api/watchlist.py @@ -126,9 +126,6 @@ def watchlist_enriched( etf_symbols = [s for s in symbols if s in etf_set] df_e, cache_date = repo.get_enriched_latest() - # 保持原契约: 自选含股票但股票 enriched 未就绪 (预热中) → 返回"未就绪"而非部分结果 - if stock_symbols and df_e.is_empty(): - return {"rows": [], "as_of": None, "elapsed_ms": 0} # 以自选列表为主表 LEFT JOIN enriched, 保证自选的每一只都返回一行; # 不在 enriched 缓存里的标的 (新股/冷门股/新用户未同步) 指标为 null, 前端渲染为 "—". @@ -147,12 +144,13 @@ def watchlist_enriched( etf_date = None if etf_symbols: df_etf_all, etf_date = repo.get_enriched_latest_asset("etf") + etf_watchlist_df = pl.DataFrame({"symbol": etf_symbols}) if not df_etf_all.is_empty(): # ETF 同样以自选为主表 LEFT JOIN, 缺失标的指标为 null - etf_watchlist_df = pl.DataFrame({"symbol": etf_symbols}) df_etf = etf_watchlist_df.join(df_etf_all, on="symbol", how="left") - if not df_etf.is_empty(): - df = df_etf if df.is_empty() else pl.concat([df, df_etf], how="diagonal_relaxed") + else: + df_etf = etf_watchlist_df + df = df_etf if df.is_empty() else pl.concat([df, df_etf], how="diagonal_relaxed") # as_of 取两类缓存中较旧者, 避免把旧的 ETF 行标成股票缓存日期 dates = [d for d in (cache_date if stock_symbols else None, etf_date) if d is not None] diff --git a/backend/tests/test_watchlist_enriched_join.py b/backend/tests/test_watchlist_enriched_join.py index 80597fa..ff9925f 100644 --- a/backend/tests/test_watchlist_enriched_join.py +++ b/backend/tests/test_watchlist_enriched_join.py @@ -93,17 +93,19 @@ def test_watchlist_symbol_not_in_enriched_still_returned(monkeypatch): def test_all_watchlist_missing_from_enriched(monkeypatch): - """极端情况: 自选全是 enriched 没覆盖的 (新用户冷启动场景).""" + """股票 enriched 缓存未就绪时, 自选仍返回占位行.""" + syms = ["000001", "000002"] monkeypatch.setattr(wl_api.watchlist, "list_symbols", - lambda: [{"symbol": "000001"}, {"symbol": "000002"}]) + lambda: [{"symbol": s} for s in syms]) repo = _FakeRepo( - enriched_df=pl.DataFrame(schema={"symbol": pl.Utf8}), # 空 schema, 模拟未就绪 + enriched_df=pl.DataFrame(schema={"symbol": pl.Utf8}), enriched_date=None, ) - # 注: 原契约 stock_symbols 非空且 enriched 空 → 返回未就绪. 这是设计, 不变. res = wl_api.watchlist_enriched(_make_request(repo), ext_columns=None) - assert res["rows"] == [] + + assert [r["symbol"] for r in res["rows"]] == syms + assert all(r.get("close") is None for r in res["rows"]) assert res["as_of"] is None @@ -147,3 +149,66 @@ def test_etf_not_in_enriched_still_returned(monkeypatch): row_missing = next(r for r in res["rows"] if r["symbol"] == "599999") assert row_missing["close"] is None + + +def test_all_etf_watchlist_missing_from_enriched(monkeypatch): + """ETF enriched 缓存未就绪时, 自选仍返回占位行.""" + syms = ["510300", "599999"] + monkeypatch.setattr(wl_api.watchlist, "list_symbols", + lambda: [{"symbol": s} for s in syms]) + repo = _FakeRepo( + enriched_df=pl.DataFrame(schema={"symbol": pl.Utf8}), + enriched_date=None, + etf_df=pl.DataFrame(schema={"symbol": pl.Utf8}), + etf_date=None, + etf_set=set(syms), + ) + + res = wl_api.watchlist_enriched(_make_request(repo), ext_columns=None) + + assert [r["symbol"] for r in res["rows"]] == syms + assert all(r.get("close") is None for r in res["rows"]) + assert res["as_of"] is None + + +def test_mixed_watchlist_keeps_pending_stock_rows(monkeypatch): + """股票缓存未就绪不应影响 ETF 行, 且保持自选原始顺序.""" + syms = ["510300", "000001", "510500"] + monkeypatch.setattr(wl_api.watchlist, "list_symbols", + lambda: [{"symbol": s} for s in syms]) + repo = _FakeRepo( + enriched_df=pl.DataFrame(schema={"symbol": pl.Utf8}), + enriched_date=None, + etf_df=_enriched_df([("510300", 4.0, 0.5, 1e8), ("510500", 6.0, -0.2, 2e8)]), + etf_date="2026-07-08", + etf_set={"510300", "510500"}, + ) + + res = wl_api.watchlist_enriched(_make_request(repo), ext_columns=None) + + assert [r["symbol"] for r in res["rows"]] == syms + assert next(r for r in res["rows"] if r["symbol"] == "000001").get("close") is None + assert next(r for r in res["rows"] if r["symbol"] == "510300")["close"] == 4.0 + assert res["as_of"] == "2026-07-08" + + +def test_mixed_watchlist_keeps_pending_etf_rows(monkeypatch): + """ETF 缓存未就绪不应影响股票行, 且保持自选原始顺序.""" + syms = ["510300", "000001", "510500"] + monkeypatch.setattr(wl_api.watchlist, "list_symbols", + lambda: [{"symbol": s} for s in syms]) + repo = _FakeRepo( + enriched_df=_enriched_df([("000001", 15.0, 0.3, 2e9)]), + enriched_date="2026-07-08", + etf_df=pl.DataFrame(schema={"symbol": pl.Utf8}), + etf_date=None, + etf_set={"510300", "510500"}, + ) + + res = wl_api.watchlist_enriched(_make_request(repo), ext_columns=None) + + assert [r["symbol"] for r in res["rows"]] == syms + assert next(r for r in res["rows"] if r["symbol"] == "000001")["close"] == 15.0 + assert all(next(r for r in res["rows"] if r["symbol"] == symbol).get("close") is None + for symbol in ("510300", "510500")) + assert res["as_of"] == "2026-07-08"