perf: /watchlist/returns 取数 800 → 20 根(MAC 单页,冷路径请求减半)

实测 MAC 服务端 QFQ 复权锚与请求窗口无关(count=11 vs 800 收盘价
完全一致),数学上 max(DEFAULT_WINDOWS)+1=11 根即够,+10 缓冲;
800 根在 MAC 协议上是 2 页请求,纯属浪费。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
awayings
2026-09-11 15:28:48 +08:00
co-authored by Claude Code
parent 5c2d4ff3d1
commit 5ea6914edb
3 changed files with 17 additions and 6 deletions
+1 -1
View File
@@ -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 同款 QFQcount=800)中 date <= D_n 的最后一根 bar。
# 锚点 = 个股日线(/bars 同款 QFQcount 按窗口推导 ≈20 根,MAC 单页请求)中 date <= D_n 的最后一根 bar。
# 只回锚点收盘价:涨跌幅由前端用实时价现算(盘中随 SSE 跳动,无需轮询本接口)。
# 个股日线与日历都走进程内缓存(当日不变、次日失效),同一天重复刷新零行情请求。
curl "http://localhost:8000/api/v1/watchlist/returns"
+13 -3
View File
@@ -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`` 同款 QFQcount=800)中 ``date <= D_n`` 的最后一根 bar。
锚点 = 个股日线(``/bars`` 同款 QFQcount=按窗口推导的 ``_BAR_COUNT``)中
``date <= D_n`` 的最后一根 bar。
容错:今日非交易日 → ``T`` 自动回退;个股锚点日停牌 → 退到最近一根并回实际
``date``;数据不足(次新)→ ``anchors[].close`` 为 ``null``;长期停牌 → 回
+3 -2
View File
@@ -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"}