From 5ea6914edb6047d5a7992ea8c580953722ed67ed Mon Sep 17 00:00:00 2001 From: awayings Date: Fri, 11 Sep 2026 15:28:48 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20/watchlist/returns=20=E5=8F=96=E6=95=B0?= =?UTF-8?q?=20800=20=E2=86=92=2020=20=E6=A0=B9=EF=BC=88MAC=20=E5=8D=95?= =?UTF-8?q?=E9=A1=B5=EF=BC=8C=E5=86=B7=E8=B7=AF=E5=BE=84=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=87=8F=E5=8D=8A=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实测 MAC 服务端 QFQ 复权锚与请求窗口无关(count=11 vs 800 收盘价 完全一致),数学上 max(DEFAULT_WINDOWS)+1=11 根即够,+10 缓冲; 800 根在 MAC 协议上是 2 页请求,纯属浪费。 Co-Authored-By: Claude Code --- docs/web-api.md | 2 +- src/easy_tdx/web/routers/watchlist.py | 16 +++++++++++++--- tests/unit/test_watchlist_and_streamer.py | 5 +++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/web-api.md b/docs/web-api.md index b3f6872..c14e108 100644 --- a/docs/web-api.md +++ b/docs/web-api.md @@ -157,7 +157,7 @@ curl -X POST "http://localhost:8000/api/v1/watchlist" \ # 自选「近 3 日 / 近 1 周 / 近 2 周」涨跌幅锚点(窗口固定为 3,5,10;交易日偏移口径) # T = 上证指数日线(交易日历)中 <= 今天的最后一天;D_n = T 往前 n 个交易日; -# 锚点 = 个股日线(/bars 同款 QFQ,count=800)中 date <= D_n 的最后一根 bar。 +# 锚点 = 个股日线(/bars 同款 QFQ,count 按窗口推导 ≈20 根,MAC 单页请求)中 date <= D_n 的最后一根 bar。 # 只回锚点收盘价:涨跌幅由前端用实时价现算(盘中随 SSE 跳动,无需轮询本接口)。 # 个股日线与日历都走进程内缓存(当日不变、次日失效),同一天重复刷新零行情请求。 curl "http://localhost:8000/api/v1/watchlist/returns" diff --git a/src/easy_tdx/web/routers/watchlist.py b/src/easy_tdx/web/routers/watchlist.py index 97b4261..2d96c85 100644 --- a/src/easy_tdx/web/routers/watchlist.py +++ b/src/easy_tdx/web/routers/watchlist.py @@ -30,7 +30,12 @@ from easy_tdx.models.enums import KlineCategory, Market from easy_tdx.realtime.session import SHANGHAI_TZ, is_trading_time from easy_tdx.web.convert import market_from_str, market_value_from_str from easy_tdx.web.deps import get_client, get_mac_client_optional -from easy_tdx.web.returns import StockReturns, compute_stock_returns, resolve_trade_date +from easy_tdx.web.returns import ( + DEFAULT_WINDOWS, + StockReturns, + compute_stock_returns, + resolve_trade_date, +) from easy_tdx.web.watchlist_store import WatchItem, get_watchlist_store _logger = logging.getLogger(__name__) @@ -44,7 +49,11 @@ _CODE_PATTERN = r"^\d{6}$" _CALENDAR_MARKET = Market.SH # 交易日历 = 上证指数(个股停牌会缺日期,不能当日历) _CALENDAR_CODE = "000001" -_BAR_COUNT = 800 # 日线一次覆盖 3 年+(与 /bars 默认同值),锚点与 last_close 同一次请求 +# 取数根数 = 最大窗口 + 缓冲。数学上 max(DEFAULT_WINDOWS)+1=11 根就够(D_10 之后最多 +# 10 个交易日,第 11 根必然 <= D_10),+10 缓冲吸收序列端部意外;20 根仍是 MAC 单页 +# (700 根/页)。实测 MAC 服务端 QFQ 复权锚与请求窗口无关(count=11 vs 800 收盘价 +# 完全一致),缩小窗口不改变锚点值,800 根的 2 页请求纯属浪费。 +_BAR_COUNT = max(DEFAULT_WINDOWS) + 10 _CONCURRENCY = 4 # TDX 防封红线:并发 ≤ 4 # 日历"未确认"(缺今天)时的重取间隔,详见 _calendar_stale _CALENDAR_RETRY_SECONDS = 60.0 @@ -294,7 +303,8 @@ async def watchlist_returns( 锚定算法(详见 :mod:`easy_tdx.web.returns`):``T`` = 上证指数日线(交易日历) 中 ``<=`` 今天的最后一个交易日;``D_n`` = 日历中 ``T`` 往前 n 个交易日的日期; - 锚点 = 个股日线(``/bars`` 同款 QFQ,count=800)中 ``date <= D_n`` 的最后一根 bar。 + 锚点 = 个股日线(``/bars`` 同款 QFQ,count=按窗口推导的 ``_BAR_COUNT``)中 + ``date <= D_n`` 的最后一根 bar。 容错:今日非交易日 → ``T`` 自动回退;个股锚点日停牌 → 退到最近一根并回实际 ``date``;数据不足(次新)→ ``anchors[].close`` 为 ``null``;长期停牌 → 回 diff --git a/tests/unit/test_watchlist_and_streamer.py b/tests/unit/test_watchlist_and_streamer.py index b8fd0de..0702daf 100644 --- a/tests/unit/test_watchlist_and_streamer.py +++ b/tests/unit/test_watchlist_and_streamer.py @@ -363,9 +363,10 @@ def test_watchlist_returns_ok(monkeypatch, tmp_path): assert item["anchors"][0]["close"] == pytest.approx(10.0 + _IDX_D3) assert item["anchors"][1]["close"] == pytest.approx(10.0 + _IDX_D5) assert item["anchors"][2]["close"] == pytest.approx(10.0 + _IDX_D10) - # /bars 同款语义:MAC + QFQ + count=800 + # /bars 同款语义:MAC + QFQ + 按窗口推导的 count(单页请求,不取 800 根) assert {k["adjust"] for k in mac.kwargs} == {Adjust.QFQ} - assert {k["count"] for k in mac.kwargs} == {800} + assert {k["count"] for k in mac.kwargs} == {mod._BAR_COUNT} + assert mod._BAR_COUNT < 700 # MAC 单页上限内,1 次请求 assert set(mac.calls) == {"000001", "600519", "002594"}