From 591142d2c385e415dcbc8139a7b25ae9461f94d5 Mon Sep 17 00:00:00 2001 From: zhuhao Date: Fri, 9 Oct 2026 19:08:57 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat=EF=BC=9A=E6=96=B0=E5=A2=9E=E8=82=A1?= =?UTF-8?q?=E7=A5=A8=E6=97=A5K=E5=BF=AB=E7=85=A7=E4=B8=8E=E5=90=8C?= =?UTF-8?q?=E8=8A=B1=E9=A1=BA=E6=9D=BF=E5=9D=97=E6=88=90=E5=88=86=E8=82=A1?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=88=86=E9=A1=B5?= =?UTF-8?q?=20limit=20=E9=87=8D=E5=A4=8D=E5=8F=96=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补齐 stock_candlesticks_daily / ths_board_constituents,东方财富板块成分股改为分页 - get_paginated 各页固定 page_size 并按 limit 截断,消除末页缩页导致的窗口重叠 --- docs/API_REFERENCE.md | 50 ++++++++++-- pyproject.toml | 2 +- src/ftshare/apis/stock.py | 41 ++++++++-- src/ftshare/base.py | 53 +++++++++++-- src/ftshare/endpoints/stock.py | 25 +++++- tests/test_client.py | 126 ++++++++++++++++++++++++++++++- tests/test_integration_market.py | 3 +- 7 files changed, 273 insertions(+), 27 deletions(-) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 937d799..7947d36 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -6,13 +6,13 @@ | 指标 | 数量 | |---|---:| -| SDK 方法总数 | 238 | +| SDK 方法总数 | 240 | ## 专题分布 | ftshare-doc 专题 | SDK 方法数 | API mixin 模块 | Endpoint 模块 | |---|---:|---|---| -| 股票数据 | 127 | `ftshare.apis.stock` | `ftshare.endpoints.stock` | +| 股票数据 | 129 | `ftshare.apis.stock` | `ftshare.endpoints.stock` | | 港股数据 | 3 | `ftshare.apis.hk` | `ftshare.endpoints.hk` | | 美股数据 | 2 | `ftshare.apis.us` | `ftshare.endpoints.us` | | 指数专题 | 15 | `ftshare.apis.index` | `ftshare.endpoints.index` | @@ -55,7 +55,7 @@ | [`cashflow_stock_code`](#api-cashflow-stock-code) | 现金流支持股票代码 | `GET` | `api/v2/market/data/finance/cashflow-stock-code` | - | `现金流支持股票代码.md` | | [`company_list`](#api-company-list) | 公司列表 | `GET` | `api/v1/market/data/company-list` | `stock_name`, `stock_code`, `page`, `page_size` | `公司列表.md` | | [`earnings_reports_paginated`](#api-earnings-reports-paginated) | 业绩快报 | `GET` | `api/v1/market/data/finance/stock-performance-express` | `stock_code`, `year`, `report_type`, `page`, `page_size` | `业绩快报.md` | -| [`eastmoney_board_constituents`](#api-eastmoney-board-constituents) | 东方财富板块成份股 | `GET` | `api/v1/market/data/eastmoney-board-constituents` | `board_code` | `东方财富板块成份股.md` | +| [`eastmoney_board_constituents`](#api-eastmoney-board-constituents) | 东方财富板块成分股 | `GET` | `api/v1/market/data/eastmoney-board-constituents` | `board_code`, `date`, `page`, `page_size` | `东方财富板块成分股.md` | | [`eastmoney_board_daily_kline`](#api-eastmoney-board-daily-kline) | 东方财富板块日线OHLC | `GET` | `api/v1/market/data/eastmoney-board-daily-ohlc` | `board_code`, `start_date`, `end_date`, `page`, `page_size` | `东方财富板块日线OHLC.md` | | [`eastmoney_concept_boards`](#api-eastmoney-concept-boards) | 东方财富概念板块 | `GET` | `api/v1/market/data/eastmoney-concept-boards` | - | `东方财富概念板块.md` | | [`eastmoney_dapan_flow`](#api-eastmoney-dapan-flow) | 东方财富大盘资金流 | `GET` | `api/v1/market/data/eastmoney-dapan-flow` | `trade_date`, `start_date`, `end_date`, `page`, `page_size` | `东方财富大盘资金流.md` | @@ -100,6 +100,7 @@ | [`stk_surv`](#api-stk-surv) | 个股严重异常波动 | `GET` | `api/v2/market/data/stk-surv` | `ts_code`, `trade_date`, `start_date`, `end_date`, `page`, `page_size` | `个股严重异常波动.md` | | [`stock_adjust_factor`](#api-stock-adjust-factor) | 股票复权因子 | `GET` | `api/v1/market/data/stock-adjust-factor` | `symbol`, `trade_date`, `start_date`, `end_date`, `page`, `page_size` | `股票复权因子.md` | | [`stock_candlesticks`](#api-stock-candlesticks) | 股票K线 | `GET` | `api/v1/market/data/stock-candlesticks` | `symbol`, `interval_unit`, `adjust_kind`, `since_ts_millis`, `until_ts_millis`, `limit` | `股票K线.md` | +| [`stock_candlesticks_daily`](#api-stock-candlesticks-daily) | 股票日K快照 | `GET` | `api/v1/market/data/stock-candlesticks-daily` | `trade_date`, `page`, `page_size` | `股票日K快照.md` | | [`stock_capital_flows`](#api-stock-capital-flows) | 股票资金流向 | `GET` | `api/v1/market/data/stock-capital-flows` | `date`, `time`, `symbol`, `page`, `page_size` | `股票资金流向.md` | | [`stock_comment_desire_em`](#api-stock-comment-desire-em) | 千股千评意愿度 | `GET` | `api/v1/market/data/stock-comment/desire` | `symbol` | `千股千评意愿度.md` | | [`stock_comment_em`](#api-stock-comment-em) | 千股千评 | `GET` | `api/v1/market/data/stock-comment/index` | `page`, `page_size` | `千股千评.md` | @@ -150,6 +151,7 @@ | [`tdx_board_index`](#api-tdx-board-index) | 通达信板块指数最新快照 | `GET` | `api/v1/market/data/tdx-board-index` | `ts_code`, `board_name`, `board_type`, `board_type_code`, `market`, `page`, `page_size` | `通达信板块指数最新快照.md` | | [`tdx_board_members`](#api-tdx-board-members) | 通达信板块成分股最新快照 | `GET` | `api/v1/market/data/tdx-board-members` | `ts_code`, `board_name`, `board_type`, `board_type_code`, `con_code`, `con_name`, `market`, `page`, `page_size` | `通达信板块成分股最新快照.md` | | [`ths_board_kline`](#api-ths-board-kline) | 同花顺板块K线 | `GET` | `api/v1/market/data/ths-board-kline` | `board_code`, `page`, `page_size` | `同花顺板块K线.md` | +| [`ths_board_constituents`](#api-ths-board-constituents) | 同花顺板块成分股 | `GET` | `api/v1/market/data/ths-board-constituents` | `board_code`, `board_name`, `board_type`, `date`, `page`, `page_size` | `同花顺板块成分股.md` | | [`stock_signal_latest_snapshot`](#api-stock-signal-latest-snapshot) | 信号最新快照 | `GET` | `api/v3/market/data/stock-signal-latest-snapshot` | `signal_type`, `page`, `page_size` | `信号最新快照.md` | | [`ths_stock_daily_flow`](#api-ths-stock-daily-flow) | 同花顺个股资金流日度 | `GET` | `api/v1/market/data/ths-stock-daily-flow` | `start_date`, `end_date`, `code`, `name`, `page`, `page_size` | `同花顺个股资金流日度.md` | | [`ths_concept_daily_flow`](#api-ths-concept-daily-flow) | 同花顺概念板块资金流日度 | `GET` | `api/v1/market/data/ths-concept-daily-flow` | `start_date`, `end_date`, `board_name`, `page`, `page_size` | `同花顺概念板块资金流日度.md` | @@ -697,22 +699,28 @@ Returns:

eastmoney_board_constituents

-- 接口名称:东方财富板块成份股 +- 接口名称:东方财富板块成分股 - HTTP:`GET` - Path:`api/v1/market/data/eastmoney-board-constituents` -- 参数:`board_code` -- 来源文档:`东方财富板块成份股.md` +- 参数:`board_code`, `date`, `page`, `page_size` +- 来源文档:`东方财富板块成分股.md` - 原始接口:`eastmoney_board_constituents` ```text -东方财富板块成份股. +东方财富板块成分股. Endpoint: ``api/v1/market/data/eastmoney-board-constituents``. Method: ``GET``. Documented endpoint: ``eastmoney_board_constituents``. Args: - board_code: 板块代码,如 BK1024 (type: string; required: Y). + board_code: 板块代码,取东方财富板块代码(BK 开头),如 BK0475、BK0490、BK0153 (type: string; required: Y). + date: 查询日期,格式 YYYYMMDD;不传返回当前仍在板块内的成分股 (type: string; required: N). + page: Page number, starting from 1. If omitted, the server default is used unless ``limit`` or ``all_pages`` is set. + page_size: Rows per page. The SDK validates this against the endpoint-specific maximum. + limit: Maximum number of rows to return. The SDK may fetch multiple pages to satisfy this limit. + all_pages: Fetch and combine pages until the server reports the last page. + max_pages: Optional safety cap for ``all_pages``. raw: Return the decoded JSON payload without tabular extraction. fields: Optional field list or comma-separated field string applied after extraction. as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. @@ -1893,6 +1901,19 @@ Returns: payloads when multi-page fetching is used with ``raw=True``. ``` +

stock_candlesticks_daily

+ +- 接口名称:股票日K快照 +- HTTP:`GET` +- Path:`api/v1/market/data/stock-candlesticks-daily` +- 参数:`trade_date`, `page`, `page_size` +- 来源文档:`股票日K快照.md` +- 原始接口:`stock_candlesticks_daily` + +```text +股票日K快照. +``` +

stock_capital_flows

- 接口名称:股票资金流向 @@ -3403,6 +3424,19 @@ Returns: payloads when multi-page fetching is used with ``raw=True``. ``` +

ths_board_constituents

+ +- 接口名称:同花顺板块成分股 +- HTTP:`GET` +- Path:`api/v1/market/data/ths-board-constituents` +- 参数:`board_code`, `board_name`, `board_type`, `date`, `page`, `page_size` +- 来源文档:`同花顺板块成分股.md` +- 原始接口:`ths_board_constituents` + +```text +同花顺板块成分股. +``` +

xueqiu_rank

- 接口名称:雪球股票排名 diff --git a/pyproject.toml b/pyproject.toml index 257f5c0..5a1892f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ftshare" -version = "1.0.10" +version = "1.0.11" description = "Python SDK for FTShare market data APIs." readme = "README.md" requires-python = ">=3.9" diff --git a/src/ftshare/apis/stock.py b/src/ftshare/apis/stock.py index 619572d..f2cc6f7 100644 --- a/src/ftshare/apis/stock.py +++ b/src/ftshare/apis/stock.py @@ -1104,20 +1104,32 @@ def company_list( def eastmoney_board_constituents( self, board_code: Any | None = None, + date: Any | None = None, + page: int | None = None, + page_size: int | None = None, + limit: int | None = None, + all_pages: bool = False, + max_pages: int | None = None, *, raw: bool = False, fields: Sequence[str] | str | None = None, as_dataframe: bool = True, **kwargs: Any, ) -> Any: - """东方财富板块成份股. + """东方财富板块成分股. Endpoint: ``api/v1/market/data/eastmoney-board-constituents``. Method: ``GET``. Documented endpoint: ``eastmoney_board_constituents``. Args: - board_code: 板块代码,如 BK1024 (type: string; required: Y). + board_code: 板块代码,取东方财富板块代码(BK 开头),如 BK0475、BK0490、BK0153 (type: string; required: Y). + date: 查询日期,格式 YYYYMMDD;不传返回当前仍在板块内的成分股 (type: string; required: N). + page: Page number, starting from 1. If omitted, the server default is used unless ``limit`` or ``all_pages`` is set. + page_size: Rows per page. The SDK validates this against the endpoint-specific maximum. + limit: Maximum number of rows to return. The SDK may fetch multiple pages to satisfy this limit. + all_pages: Fetch and combine pages until the server reports the last page. + max_pages: Optional safety cap for ``all_pages``. raw: Return the decoded JSON payload without tabular extraction. fields: Optional field list or comma-separated field string applied after extraction. as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. @@ -1128,10 +1140,17 @@ def eastmoney_board_constituents( ``as_dataframe=False``, raw JSON when ``raw=True``, or raw page payloads when multi-page fetching is used with ``raw=True``. """ - request_params = {'board_code': board_code} + request_params = {'board_code': board_code, 'date': date} request_params.update(kwargs) - return self._call_endpoint( - 'eastmoney_board_constituents', + path = ENDPOINTS['eastmoney_board_constituents'].path + return self.get_paginated( + path, + page=page, + page_size=page_size, + limit=limit, + all_pages=all_pages, + max_pages=max_pages, + max_page_size=ENDPOINTS['eastmoney_board_constituents'].max_page_size, raw=raw, fields=fields, as_dataframe=as_dataframe, @@ -4738,3 +4757,15 @@ def news_reaction_snapshot(self, symbol: Any | None = None, start_date: Any | No params.update(kwargs) path = ENDPOINTS['news_reaction_snapshot'].path return self.get_paginated(path, page=page, page_size=page_size, limit=limit, all_pages=all_pages, max_pages=max_pages, max_page_size=200, raw=raw, fields=fields, as_dataframe=as_dataframe, **params) + + def stock_candlesticks_daily(self, trade_date: Any | None = None, page: int | None = None, page_size: int | None = None, limit: int | None = None, all_pages: bool = False, max_pages: int | None = None, *, raw: bool = False, fields: Sequence[str] | str | None = None, as_dataframe: bool = True, **kwargs: Any) -> Any: + """股票日K快照.""" + params = {'trade_date': trade_date} + params.update(kwargs) + return self.get_paginated(ENDPOINTS['stock_candlesticks_daily'].path, page=page, page_size=page_size, limit=limit, all_pages=all_pages, max_pages=max_pages, max_page_size=500, raw=raw, fields=fields, as_dataframe=as_dataframe, **params) + + def ths_board_constituents(self, board_code: Any | None = None, board_name: Any | None = None, board_type: Any | None = None, date: Any | None = None, page: int | None = None, page_size: int | None = None, limit: int | None = None, all_pages: bool = False, max_pages: int | None = None, *, raw: bool = False, fields: Sequence[str] | str | None = None, as_dataframe: bool = True, **kwargs: Any) -> Any: + """同花顺板块成分股.""" + params = {'board_code': board_code, 'board_name': board_name, 'board_type': board_type, 'date': date} + params.update(kwargs) + return self.get_paginated(ENDPOINTS['ths_board_constituents'].path, page=page, page_size=page_size, limit=limit, all_pages=all_pages, max_pages=max_pages, max_page_size=1000, raw=raw, fields=fields, as_dataframe=as_dataframe, **params) diff --git a/src/ftshare/base.py b/src/ftshare/base.py index b3dd0ed..1a7b697 100644 --- a/src/ftshare/base.py +++ b/src/ftshare/base.py @@ -20,6 +20,29 @@ from .response import extract_tabular, raise_for_api_error, total_pages as extract_total_pages +def _truncate_tabular_rows(payload: Any, keep: int) -> Any: + """Copy a raw payload with its row list truncated to ``keep`` rows. + + Mirrors the container precedence used by :func:`extract_tabular`; unknown + shapes are returned unchanged. + """ + if not isinstance(payload, dict) or keep < 0: + return payload + data = payload.get("data") + if isinstance(data, list): + return {**payload, "data": data[:keep]} + if isinstance(data, dict): + for key in ("records", "items", "index_descriptions"): + value = data.get(key) + if isinstance(value, list): + return {**payload, "data": {**data, key: value[:keep]}} + items = payload.get("items") + if isinstance(items, list): + return {**payload, "items": items[:keep]} + return payload + + + class BaseClient: """Base synchronous client with shared HTTP behavior. @@ -256,7 +279,8 @@ def get_paginated( max_pages: Optional safety cap used with ``all_pages``. max_page_size: Maximum allowed page size for this endpoint. raw: Return raw JSON. When multiple pages are fetched, returns a - list of raw page payloads. + list of raw page payloads; with ``limit`` the final payload's + rows are trimmed so the total matches ``limit``. fields: Optional field list or comma-separated field string. as_dataframe: Return a pandas ``DataFrame`` by default. unwrap_bare_data: When ``True``, an object-shaped ``data`` field @@ -294,16 +318,18 @@ def get_paginated( request_page_size = min(limit, max_page_size) else: request_page_size = max_page_size + if limit is not None: + request_page_size = min(request_page_size, limit) + # Keep the requested page size constant across pages: the server derives + # each page's offset from page * page_size, so shrinking the last page + # would shift its window and duplicate or drop rows. Overshoot is trimmed + # by ``limit`` after the loop instead. while True: - current_page_size = request_page_size - if remaining is not None: - current_page_size = min(current_page_size, remaining) - payload = self.get( path, page=effective_page, - page_size=current_page_size, + page_size=request_page_size, raw=True, **params, ) @@ -324,15 +350,28 @@ def get_paginated( break if page_count is not None and effective_page >= page_count: break - if page_count is None and len(page_rows) < current_page_size: + if page_count is None and len(page_rows) < request_page_size: break if not page_rows: break effective_page += 1 if raw: + if limit is not None: + kept = 0 + for index, payload in enumerate(payloads): + page_rows = extract_tabular(payload) + if not isinstance(page_rows, list): + break + if kept + len(page_rows) <= limit: + kept += len(page_rows) + continue + payloads[index] = _truncate_tabular_rows(payload, limit - kept) + break return payloads[0] if len(payloads) == 1 and not all_pages else payloads + if limit is not None: + rows = rows[:limit] result = self._select_fields(rows, fields) if as_dataframe: return self._to_dataframe(result) diff --git a/src/ftshare/endpoints/stock.py b/src/ftshare/endpoints/stock.py index 4995c6f..1fe68ea 100644 --- a/src/ftshare/endpoints/stock.py +++ b/src/ftshare/endpoints/stock.py @@ -81,10 +81,11 @@ }, 'eastmoney_board_constituents': { 'path': 'api/v1/market/data/eastmoney-board-constituents', - 'title': '东方财富板块成份股', - 'doc_file': '东方财富板块成份股.md', + 'title': '东方财富板块成分股', + 'doc_file': '东方财富板块成分股.md', 'original_api': 'eastmoney_board_constituents', - 'params': ('board_code',), + 'params': ('board_code', 'date', 'page', 'page_size'), + 'max_page_size': 500, }, 'eastmoney_board_daily_kline': { 'path': 'api/v1/market/data/eastmoney-board-daily-ohlc', @@ -962,4 +963,22 @@ 'max_page_size': 200, }, + 'stock_candlesticks_daily': { + 'path': 'api/v1/market/data/stock-candlesticks-daily', + 'title': '股票日K快照', + 'doc_file': '股票日K快照.md', + 'original_api': 'stock_candlesticks_daily', + 'params': ('trade_date', 'page', 'page_size'), + 'max_page_size': 500, + }, + + 'ths_board_constituents': { + 'path': 'api/v1/market/data/ths-board-constituents', + 'title': '同花顺板块成分股', + 'doc_file': '同花顺板块成分股.md', + 'original_api': 'ths_board_constituents', + 'params': ('board_code', 'board_name', 'board_type', 'date', 'page', 'page_size'), + 'max_page_size': 1000, + }, + }) diff --git a/tests/test_client.py b/tests/test_client.py index b446036..7db6bc7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -88,6 +88,35 @@ def paginated_records(records, page=1, pages=1): } +class OffsetPagingSession: + """Fake session mimicking the server's offset = (page - 1) * page_size window.""" + + def __init__(self, rows, pages): + self.rows = list(rows) + self.pages = pages + self.calls = [] + + def get(self, url, params=None, timeout=None, headers=None, stream=False): + self.calls.append({"url": url, "params": params, "headers": headers}) + page = params["page"] + page_size = params["page_size"] + start = (page - 1) * page_size + window = self.rows[start : start + page_size] + payload = { + "code": 0, + "message": "success", + "data": { + "pageNum": page, + "pageSize": page_size, + "total": len(self.rows), + "pages": self.pages, + "records": window, + }, + } + return FakeResponse(payload=payload) + + + def test_new_endpoints_forward_documented_parameters(): session = FakeSession([FakeResponse(payload=paginated_records([{"code": "NVDA"}]))] * 2 + [FakeResponse(payload={"code": 200, "message": "success", "data": {"n": 5}})] * 3) client = FtshareClient(session=session) @@ -346,6 +375,8 @@ def test_requested_endpoint_api_versions(): "ths_concept_daily_flow": "api/v1/market/data/ths-concept-daily-flow", "ths_industry_daily_flow": "api/v1/market/data/ths-industry-daily-flow", "eastmoney_etf_flow": "api/v1/market/data/eastmoney-etf-flow", + "stock_candlesticks_daily": "api/v1/market/data/stock-candlesticks-daily", + "ths_board_constituents": "api/v1/market/data/ths-board-constituents", } assert {name: ENDPOINTS[name].path for name in expected_paths} == expected_paths @@ -984,7 +1015,37 @@ def test_limit_above_single_page_fetches_multiple_pages(): assert session.calls[0]["params"]["page"] == 1 assert session.calls[0]["params"]["page_size"] == 200 assert session.calls[1]["params"]["page"] == 2 - assert session.calls[1]["params"]["page_size"] == 100 + assert session.calls[1]["params"]["page_size"] == 200 + + +def test_limit_does_not_duplicate_rows_across_pages(): + rows = [{"id": i} for i in range(1, 1001)] + session = OffsetPagingSession(rows, pages=5) + client = FtshareClient(session=session) + + df = client.baidu_financial_calendar(start_date="2026-05-01", end_date="2026-05-02", limit=250) + + assert list(df["id"]) == list(range(1, 251)) + assert session.calls[0]["params"]["page_size"] == 200 + assert session.calls[1]["params"]["page_size"] == 200 + assert len(session.calls) == 2 + + +def test_limit_trims_raw_page_payloads(): + rows = [{"id": i} for i in range(1, 1001)] + session = OffsetPagingSession(rows, pages=5) + client = FtshareClient(session=session) + + payloads = client.baidu_financial_calendar( + start_date="2026-05-01", + end_date="2026-05-02", + limit=250, + raw=True, + ) + + assert len(payloads) == 2 + assert [row["id"] for row in payloads[0]["data"]["records"]] == list(range(1, 201)) + assert [row["id"] for row in payloads[1]["data"]["records"]] == list(range(201, 251)) @pytest.mark.parametrize( @@ -1021,7 +1082,7 @@ def test_stk_limit_uses_500_as_default_request_page_size(): assert isinstance(df, pd.DataFrame) assert len(df) == 501 assert session.calls[0]["params"]["page_size"] == 500 - assert session.calls[1]["params"]["page_size"] == 1 + assert session.calls[1]["params"]["page_size"] == 500 def test_stk_limit_allows_500_page_size(): @@ -1628,3 +1689,64 @@ def test_a_share_reference_endpoints_map_to_expected_paths( assert session.calls[0]["url"] == "https://market.ft.tech/gateway/" + expected_path assert session.calls[0]["params"] == expected_params + + +def test_stock_candlesticks_daily_forwards_documented_parameters(): + session = FakeSession([FakeResponse(payload=paginated_records([]))]) + client = FtshareClient(session=session) + + client.stock_candlesticks_daily(trade_date="20261008", page=1, page_size=10, as_dataframe=False) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v1/market/data/stock-candlesticks-daily" + assert session.calls[0]["params"] == {"trade_date": "20261008", "page": 1, "page_size": 10} + + +def test_stock_candlesticks_daily_allows_page_size_up_to_500(): + session = FakeSession([FakeResponse(payload=paginated_records([]))]) + client = FtshareClient(session=session) + + client.stock_candlesticks_daily(trade_date="20261008", page=1, page_size=500, as_dataframe=False) + + assert session.calls[0]["params"] == {"trade_date": "20261008", "page": 1, "page_size": 500} + + with pytest.raises(ValueError, match="page_size must be between 1 and 500"): + client.stock_candlesticks_daily(trade_date="20261008", page_size=501) + + +def test_eastmoney_board_constituents_forwards_date_and_pagination(): + session = FakeSession([FakeResponse(payload=paginated_records([]))]) + client = FtshareClient(session=session) + + client.eastmoney_board_constituents(board_code="BK0475", date="20260930", page=1, page_size=20, as_dataframe=False) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v1/market/data/eastmoney-board-constituents" + assert session.calls[0]["params"] == {"board_code": "BK0475", "date": "20260930", "page": 1, "page_size": 20} + + +def test_ths_board_constituents_forwards_documented_parameters(): + session = FakeSession([FakeResponse(payload=paginated_records([]))]) + client = FtshareClient(session=session) + + client.ths_board_constituents(board_code="300082", board_name="军工", board_type="concept", date="20260915", page=1, page_size=100, as_dataframe=False) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v1/market/data/ths-board-constituents" + assert session.calls[0]["params"] == { + "board_code": "300082", + "board_name": "军工", + "board_type": "concept", + "date": "20260915", + "page": 1, + "page_size": 100, + } + + +def test_ths_board_constituents_allows_page_size_up_to_1000(): + session = FakeSession([FakeResponse(payload=paginated_records([]))]) + client = FtshareClient(session=session) + + client.ths_board_constituents(board_code="300082", page=1, page_size=1000, as_dataframe=False) + + assert session.calls[0]["params"] == {"board_code": "300082", "page": 1, "page_size": 1000} + + with pytest.raises(ValueError, match="page_size must be between 1 and 1000"): + client.ths_board_constituents(board_code="300082", page_size=1001) diff --git a/tests/test_integration_market.py b/tests/test_integration_market.py index 986fca0..2945fbd 100644 --- a/tests/test_integration_market.py +++ b/tests/test_integration_market.py @@ -16,7 +16,8 @@ # Endpoints that return HTTP 200 with an empty payload on the test server for # every parameter combination, or fail before reaching the data layer. Verified -# against http://xz09:10103 on 2026-09-22; re-check these when the server moves. +# against the integration test server on 2026-09-22; re-check these when the +# server moves. KNOWN_SERVER_ISSUES: dict[str, str] = { "ashare_news_sentiment_factors": "HTTP 502 下游服务请求失败 for every parameter combination", "futures_minutes_batch": "HTTP 404 请求的资源不存在 — route absent on the test server", From 9b24239dadb6c046ef61fa9f90b740982ae0a0c4 Mon Sep 17 00:00:00 2001 From: zhuhao Date: Mon, 12 Oct 2026 20:05:19 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat=EF=BC=9A=E6=96=B0=E5=A2=9E=E6=9C=9F?= =?UTF-8?q?=E8=B4=A7=20K=20=E7=BA=BF=E6=89=B9=E9=87=8F/=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E6=97=A5=20K=20=E7=BA=BF=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=9C=9F?= =?UTF-8?q?=E8=B4=A7=E8=A1=8C=E6=83=85=E6=9B=B4=E5=90=8D=20futures=5Fkline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 futures_kline_batch(api/v2 .../futures/kline/batch)与 futures_realtime_day_kline(api/v4 .../futures-realtime-day-kline)端点与方法 - futures_contract_kline 更名为 futures_kline,doc_file/标题同步为 「期货合约历史K线」;futures_minutes 标题改为「期货历史分钟K线」 - futures_minutes 新增 symbols 参数,支持单合约与批量二选一 - API_REFERENCE 计数更新(方法 240→242,期货数据 18→20)及索引/详情条目 - 补充 symbols 列表转发与新增接口的 mock 单测 --- docs/API_REFERENCE.md | 97 +++++++++++++++++++++++----- src/ftshare/apis/futures.py | 106 ++++++++++++++++++++++++++++--- src/ftshare/endpoints/futures.py | 28 ++++++-- tests/endpoint_cases.py | 13 ++-- tests/test_client.py | 56 +++++++++++++++- 5 files changed, 263 insertions(+), 37 deletions(-) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 7947d36..61be43a 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -6,7 +6,7 @@ | 指标 | 数量 | |---|---:| -| SDK 方法总数 | 240 | +| SDK 方法总数 | 242 | ## 专题分布 @@ -18,7 +18,7 @@ | 指数专题 | 15 | `ftshare.apis.index` | `ftshare.endpoints.index` | | ETF专题 | 17 | `ftshare.apis.etf` | `ftshare.endpoints.etf` | | 公募基金 | 18 | `ftshare.apis.fund` | `ftshare.endpoints.fund` | -| 期货数据 | 18 | `ftshare.apis.futures` | `ftshare.endpoints.futures` | +| 期货数据 | 20 | `ftshare.apis.futures` | `ftshare.endpoints.futures` | | 债券专题 | 9 | `ftshare.apis.bond` | `ftshare.endpoints.bond` | | 宏观经济 | 23 | `ftshare.apis.economic` | `ftshare.endpoints.economic` | | 大模型语料 | 5 | `ftshare.apis.llm_corpus` | `ftshare.endpoints.llm_corpus` | @@ -262,9 +262,11 @@ | [`fut_settle`](#api-fut-settle) | 期货每日结算参数 | `GET` | `api/v1/market/data/futures/fut-settle` | `ts_code`, `trade_date`, `start_date`, `end_date`, `exchange`, `page`, `page_size` | `期货每日结算参数.md` | | [`fut_weekly_detail`](#api-fut-weekly-detail) | 期货主要品种交易周报 | `GET` | `api/v1/market/data/futures/fut-weekly-detail` | `week`, `prd`, `start_week`, `end_week`, `exchange`, `page`, `page_size` | `期货主要品种交易周报.md` | | [`fut_wsr`](#api-fut-wsr) | 期货仓单日报 | `GET` | `api/v1/market/data/futures/fut-wsr` | `trade_date`, `start_date`, `end_date`, `symbol`, `exchange`, `page`, `page_size` | `期货仓单日报.md` | -| [`futures_contract_kline`](#api-futures-contract-kline) | 期货行情 | `GET` | `api/v1/market/data/futures/kline` | `symbol`, `interval`, `start`, `end`, `limit` | `期货行情.md` | -| [`futures_minutes`](#api-futures-minutes) | 期货历史分钟行情 | `GET` | `api/v2/market/data/futures_minutes` | `symbol`, `interval`, `start`, `end`, `limit` | `期货历史分钟行情.md` | +| [`futures_kline`](#api-futures-kline) | 期货合约历史K线 | `GET` | `api/v1/market/data/futures/kline` | `symbol`, `interval`, `start`, `end`, `limit` | `期货合约历史K线.md` | +| [`futures_kline_batch`](#api-futures-kline-batch) | 批量期货合约历史K线 | `GET` | `api/v2/market/data/futures/kline/batch` | `symbols`, `interval`, `start`, `end`, `limit` | `批量期货合约历史K线.md` | +| [`futures_minutes`](#api-futures-minutes) | 期货历史分钟K线 | `GET` | `api/v2/market/data/futures_minutes` | `symbol`, `symbols`, `interval`, `start`, `end`, `limit` | `期货历史分钟K线.md` | | [`futures_minutes_realtime`](#api-futures-minutes-realtime) | 期货实时分钟K线 | `GET` | `api/v4/market/data/futures_minutes/realtime` | `symbols` | `期货实时分钟K线.md` | +| [`futures_realtime_day_kline`](#api-futures-realtime-day-kline) | 期货实时日K线 | `GET` | `api/v4/market/data/futures-realtime-day-kline` | `symbols` | `期货实时日K线.md` | | [`futures_nanhua_index_kline`](#api-futures-nanhua-index-kline) | 南华期货指数日K线 | `GET` | `api/v1/market/data/futures/nanhua-index-kline` | `code`, `trade_date`, `start_date`, `end_date`, `page`, `page_size` | `南华期货指数日K线.md` | | [`major_contract`](#api-major-contract) | 重大合同 | `GET` | `api/v1/market/data/corporate/contract` | `start_date`, `end_date` | `重大合同.md` | | [`major_contract_by_symbol`](#api-major-contract-by-symbol) | 重大合同按标的 | `GET` | `api/v1/market/data/corporate/contract/by-symbol` | `symbol`, `page`, `page_size` | `重大合同按标的.md` | @@ -5054,21 +5056,21 @@ Returns: payloads when multi-page fetching is used with ``raw=True``. ``` -

futures_contract_kline

+

futures_kline

-- 接口名称:期货行情 +- 接口名称:期货合约历史K线 - HTTP:`GET` - Path:`api/v1/market/data/futures/kline` - 参数:`symbol`, `interval`, `start`, `end`, `limit` -- 来源文档:`期货行情.md` -- 原始接口:`futures_contract_kline` +- 来源文档:`期货合约历史K线.md` +- 原始接口:`futures_kline` ```text -期货行情. +期货合约历史K线. Endpoint: ``api/v1/market/data/futures/kline``. Method: ``GET``. -Documented endpoint: ``futures_contract_kline``. +Documented endpoint: ``futures_kline``. Args: symbol: 期货合约代码 (type: string; required: Y). @@ -5086,27 +5088,60 @@ Returns: ``as_dataframe=False``, or raw JSON when ``raw=True``. ``` +

futures_kline_batch

+ +- 接口名称:批量期货合约历史K线 +- HTTP:`GET` +- Path:`api/v2/market/data/futures/kline/batch` +- 参数:`symbols`, `interval`, `start`, `end`, `limit` +- 来源文档:`批量期货合约历史K线.md` +- 原始接口:`futures_kline_batch` + +```text +批量期货合约历史K线. + +Endpoint: ``api/v2/market/data/futures/kline/batch``. +Method: ``GET``. +Documented endpoint: ``futures_kline_batch``. + +Args: + symbols: 1~20 个期货合约代码;支持重复参数、逗号分隔或列表 (type: string[]; required: Y). + interval: K 线周期,默认 ``daily`` (type: string; required: N). + start: 起始时间戳,单位毫秒 (type: integer; required: N). + end: 结束时间戳,单位毫秒,不能单独传 (type: integer; required: N). + limit: 每个合约返回条数上限,默认 500 (type: integer; required: N). + raw: Return the decoded JSON payload without tabular extraction. + fields: Optional field list or comma-separated field string applied after extraction. + as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. + **kwargs: Extra request parameters forwarded unchanged. + +Returns: + A pandas ``DataFrame`` by default, Python rows when + ``as_dataframe=False``, or raw JSON when ``raw=True``. +``` +

futures_minutes

-- 接口名称:期货历史分钟行情 +- 接口名称:期货历史分钟K线 - HTTP:`GET` - Path:`api/v2/market/data/futures_minutes` -- 参数:`symbol`, `interval`, `start`, `end`, `limit` -- 来源文档:`期货历史分钟行情.md` +- 参数:`symbol`, `symbols`, `interval`, `start`, `end`, `limit` +- 来源文档:`期货历史分钟K线.md` - 原始接口:`futures_minutes` ```text -期货历史分钟行情. +期货历史分钟K线. Endpoint: ``api/v2/market/data/futures_minutes``. Method: ``GET``. Documented endpoint: ``futures_minutes``. Args: - symbol: 期货合约代码,带交易所短后缀 (type: string; required: Y). + symbol: 单个期货合约代码,带交易所短后缀;与 ``symbols`` 二选一 (type: string; required: N). + symbols: 1~20 个期货合约代码;支持重复参数、逗号分隔或列表;与 ``symbol`` 二选一 (type: string[]; required: N). interval: 分钟周期,默认 ``1min`` (type: string; required: N). - start: 起始时间戳,单位毫秒 (type: integer; required: N). - end: 结束时间戳,单位毫秒,不能单独传 (type: integer; required: N). + start: 起始时间戳,单位毫秒;必填 (type: integer; required: Y). + end: 结束时间戳,单位毫秒;必填,不能单独传 (type: integer; required: Y). limit: 返回条数上限,范围 1~1000 (type: integer; required: N). raw: Return the decoded JSON payload without tabular extraction. fields: Optional field list or comma-separated field string applied after extraction. @@ -5183,6 +5218,34 @@ Returns: payloads when multi-page fetching is used with ``raw=True``. ``` +

futures_realtime_day_kline

+ +- 接口名称:期货实时日K线 +- HTTP:`GET` +- Path:`api/v4/market/data/futures-realtime-day-kline` +- 参数:`symbols` +- 来源文档:`期货实时日K线.md` +- 原始接口:`futures_realtime_day_kline` + +```text +期货实时日K线. + +Endpoint: ``api/v4/market/data/futures-realtime-day-kline``. +Method: ``GET``. +Documented endpoint: ``futures_realtime_day_kline``. + +Args: + symbols: 1~20 个期货合约代码;支持重复参数、逗号分隔或列表 (type: string[]; required: Y). + raw: Return the decoded JSON payload without tabular extraction. + fields: Optional field list or comma-separated field string applied after extraction. + as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. + **kwargs: Extra request parameters forwarded unchanged. + +Returns: + A pandas ``DataFrame`` by default, Python rows when + ``as_dataframe=False``, or raw JSON when ``raw=True``. +``` +

major_contract

- 接口名称:重大合同 diff --git a/src/ftshare/apis/futures.py b/src/ftshare/apis/futures.py index dd6b38b..e53a074 100644 --- a/src/ftshare/apis/futures.py +++ b/src/ftshare/apis/futures.py @@ -298,6 +298,7 @@ def eastmoney_futures_position( def futures_minutes( self, symbol: Any | None = None, + symbols: Any | None = None, interval: Any | None = None, start: Any | None = None, end: Any | None = None, @@ -308,17 +309,18 @@ def futures_minutes( as_dataframe: bool = True, **kwargs: Any, ) -> Any: - """期货历史分钟行情. + """期货历史分钟K线. Endpoint: ``api/v2/market/data/futures_minutes``. Method: ``GET``. Documented endpoint: ``futures_minutes``. Args: - symbol: 期货合约代码,带交易所短后缀 (type: string; required: Y). + symbol: 单个期货合约代码,带交易所短后缀;与 ``symbols`` 二选一 (type: string; required: N). + symbols: 1~20 个期货合约代码;支持重复参数、逗号分隔或列表;与 ``symbol`` 二选一 (type: string[]; required: N). interval: 分钟周期,默认 ``1min`` (type: string; required: N). - start: 起始时间戳,单位毫秒 (type: integer; required: N). - end: 结束时间戳,单位毫秒,不能单独传 (type: integer; required: N). + start: 起始时间戳,单位毫秒;必填 (type: integer; required: Y). + end: 结束时间戳,单位毫秒;必填,不能单独传 (type: integer; required: Y). limit: 返回条数上限,范围 1~1000 (type: integer; required: N). raw: Return the decoded JSON payload without tabular extraction. fields: Optional field list or comma-separated field string applied after extraction. @@ -331,6 +333,7 @@ def futures_minutes( """ request_params = { 'symbol': symbol, + 'symbols': symbols, 'interval': interval, 'start': start, 'end': end, @@ -345,7 +348,7 @@ def futures_minutes( **request_params, ) - def futures_contract_kline( + def futures_kline( self, symbol: Any | None = None, interval: Any | None = None, @@ -358,11 +361,11 @@ def futures_contract_kline( as_dataframe: bool = True, **kwargs: Any, ) -> Any: - """期货行情. + """期货合约历史K线. Endpoint: ``api/v1/market/data/futures/kline``. Method: ``GET``. - Documented endpoint: ``futures_contract_kline``. + Documented endpoint: ``futures_kline``. Args: symbol: 期货合约代码 (type: string; required: Y). @@ -388,7 +391,57 @@ def futures_contract_kline( } request_params.update(kwargs) return self._call_endpoint( - 'futures_contract_kline', + 'futures_kline', + raw=raw, + fields=fields, + as_dataframe=as_dataframe, + **request_params, + ) + + def futures_kline_batch( + self, + symbols: Any | None = None, + interval: Any | None = None, + start: Any | None = None, + end: Any | None = None, + limit: Any | None = None, + *, + raw: bool = False, + fields: Sequence[str] | str | None = None, + as_dataframe: bool = True, + **kwargs: Any, + ) -> Any: + """批量期货合约历史K线. + + Endpoint: ``api/v2/market/data/futures/kline/batch``. + Method: ``GET``. + Documented endpoint: ``futures_kline_batch``. + + Args: + symbols: 1~20 个期货合约代码;支持重复参数、逗号分隔或列表 (type: string[]; required: Y). + interval: K 线周期,默认 ``daily`` (type: string; required: N). + start: 起始时间戳,单位毫秒 (type: integer; required: N). + end: 结束时间戳,单位毫秒,不能单独传 (type: integer; required: N). + limit: 每个合约返回条数上限,默认 500 (type: integer; required: N). + raw: Return the decoded JSON payload without tabular extraction. + fields: Optional field list or comma-separated field string applied after extraction. + as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. + **kwargs: Extra request parameters forwarded unchanged. + + Returns: + A pandas ``DataFrame`` by default, Python rows when + ``as_dataframe=False``, or raw JSON when ``raw=True``. + """ + request_params = { + 'symbols': symbols, + 'interval': interval, + 'start': start, + 'end': end, + 'limit': limit, + } + request_params.update(kwargs) + return self._call_endpoint( + 'futures_kline_batch', raw=raw, fields=fields, as_dataframe=as_dataframe, @@ -430,6 +483,43 @@ def futures_minutes_realtime( as_dataframe=as_dataframe, **request_params, ) + + def futures_realtime_day_kline( + self, + symbols: Any | None = None, + *, + raw: bool = False, + fields: Sequence[str] | str | None = None, + as_dataframe: bool = True, + **kwargs: Any, + ) -> Any: + """期货实时日K线. + + Endpoint: ``api/v4/market/data/futures-realtime-day-kline``. + Method: ``GET``. + Documented endpoint: ``futures_realtime_day_kline``. + + Args: + symbols: 1~20 个期货合约代码;支持重复参数、逗号分隔或列表 (type: string[]; required: Y). + raw: Return the decoded JSON payload without tabular extraction. + fields: Optional field list or comma-separated field string applied after extraction. + as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. + **kwargs: Extra request parameters forwarded unchanged. + + Returns: + A pandas ``DataFrame`` by default, Python rows when + ``as_dataframe=False``, or raw JSON when ``raw=True``. + """ + request_params = {'symbols': symbols} + request_params.update(kwargs) + return self._call_endpoint( + 'futures_realtime_day_kline', + raw=raw, + fields=fields, + as_dataframe=as_dataframe, + **request_params, + ) + def fut_wsr( self, trade_date: Any | None = None, diff --git a/src/ftshare/endpoints/futures.py b/src/ftshare/endpoints/futures.py index 39a810e..02d3437 100644 --- a/src/ftshare/endpoints/futures.py +++ b/src/ftshare/endpoints/futures.py @@ -29,18 +29,25 @@ }, 'futures_minutes': { 'path': 'api/v2/market/data/futures_minutes', - 'title': '期货历史分钟行情', - 'doc_file': '期货历史分钟行情.md', + 'title': '期货历史分钟K线', + 'doc_file': '期货历史分钟K线.md', 'original_api': 'futures_minutes', - 'params': ('symbol', 'interval', 'start', 'end', 'limit'), + 'params': ('symbol', 'symbols', 'interval', 'start', 'end', 'limit'), }, - 'futures_contract_kline': { + 'futures_kline': { 'path': 'api/v1/market/data/futures/kline', - 'title': '期货行情', - 'doc_file': '期货行情.md', - 'original_api': 'futures_contract_kline', + 'title': '期货合约历史K线', + 'doc_file': '期货合约历史K线.md', + 'original_api': 'futures_kline', 'params': ('symbol', 'interval', 'start', 'end', 'limit'), }, + 'futures_kline_batch': { + 'path': 'api/v2/market/data/futures/kline/batch', + 'title': '批量期货合约历史K线', + 'doc_file': '批量期货合约历史K线.md', + 'original_api': 'futures_kline_batch', + 'params': ('symbols', 'interval', 'start', 'end', 'limit'), + }, 'futures_minutes_realtime': { 'path': 'api/v4/market/data/futures_minutes/realtime', 'title': '期货实时分钟K线', @@ -48,6 +55,13 @@ 'original_api': 'futures_minutes_realtime', 'params': ('symbols',), }, + 'futures_realtime_day_kline': { + 'path': 'api/v4/market/data/futures-realtime-day-kline', + 'title': '期货实时日K线', + 'doc_file': '期货实时日K线.md', + 'original_api': 'futures_realtime_day_kline', + 'params': ('symbols',), + }, 'major_contract': { 'path': 'api/v1/market/data/corporate/contract', 'title': '重大合同', diff --git a/tests/endpoint_cases.py b/tests/endpoint_cases.py index f7666d3..c316e85 100644 --- a/tests/endpoint_cases.py +++ b/tests/endpoint_cases.py @@ -189,9 +189,11 @@ "fut_settle": {"ts_code": "A2609.DCE", "trade_date": "20260717", "start_date": None, "end_date": None, "exchange": "DCE", "page": 1, "page_size": 5}, "fut_weekly_detail": {"week": "202629", "prd": "CU,AL", "start_week": None, "end_week": None, "exchange": "DCE", "page": 1, "page_size": 5}, "fut_wsr": {"trade_date": "20260717", "start_date": None, "end_date": None, "symbol": "CU", "exchange": "DCE", "page": 1, "page_size": 5}, - "futures_contract_kline": {"symbol": "A2609.DCE", "interval": "daily", "start": 1784048400000, "end": 1784050200000, "limit": 5}, - "futures_minutes": {"symbol": "A2609.DCE", "interval": "1min", "start": 1784048400000, "end": 1784050200000, "limit": 5}, + "futures_kline": {"symbol": "A2609.DCE", "interval": "daily", "start": 1784048400000, "end": 1784050200000, "limit": 5}, + "futures_kline_batch": {"symbols": "A2609.DCE", "interval": "daily", "start": 1784048400000, "end": 1784050200000, "limit": 5}, + "futures_minutes": {"symbol": "A2609.DCE", "symbols": None, "interval": "1min", "start": 1784048400000, "end": 1784050200000, "limit": 5}, "futures_minutes_realtime": {"symbols": "A2609.DCE"}, + "futures_realtime_day_kline": {"symbols": "A2609.DCE"}, "hk_candlesticks": {"trade_code": "00700.HK", "interval_unit": "day", "until_date": "20260717", "since_date": "20260715", "interval_value": 1, "limit": 5, "adjust_kind": "forward"}, "index_candlesticks": {"symbol": "000300.SH", "interval_unit": "day", "adjust_kind": "forward", "since_ts_millis": 1784048400000, "until_ts_millis": 1784050200000, "limit": 5}, "index_candlesticks_batch": {"symbols": "000300.SH", "interval_unit": "day", "adjust_kind": "forward", "since_ts_millis": 1784048400000, "until_ts_millis": 1784050200000, "limit": 5}, @@ -301,9 +303,12 @@ def _millis(day: str, time: str = "00:00:00") -> int: "fund_share": {"fund_code": "000001", "stati_perd": None, "start_date": None, "end_date": None, "page": 1, "page_size": 5}, "fut_weekly_detail": {"week": "202629", "prd": None, "start_week": None, "end_week": None, "exchange": None, "page": 1, "page_size": 5}, "fut_wsr": {"trade_date": "20260717", "start_date": None, "end_date": None, "symbol": None, "exchange": None, "page": 1, "page_size": 5}, - "futures_contract_kline": {"symbol": "A2609.DCE", "interval": "daily", "start": _millis("2026-05-01"), "end": _millis("2026-07-17", "15:00:00"), "limit": 5}, - "futures_minutes": {"symbol": "A2609.DCE", "interval": "1min", "start": _millis("2026-07-17", "09:30:00"), "end": _millis("2026-07-17", "10:00:00"), "limit": 5}, + "futures_kline": {"symbol": "A2609.DCE", "interval": "daily", "start": _millis("2026-05-01"), "end": _millis("2026-07-17", "15:00:00"), "limit": 5}, + "futures_kline_batch": {"symbols": "A2609.DCE", "interval": "daily", "start": _millis("2026-05-01"), "end": _millis("2026-07-17", "15:00:00"), "limit": 5}, + "futures_minutes": {"symbol": "A2609.DCE", "symbols": None, "interval": "1min", "start": _millis("2026-07-17", "09:30:00"), "end": _millis("2026-07-17", "10:00:00"), "limit": 5}, "futures_minutes_batch": {"symbols": "A2609.DCE", "interval": "1min", "start": _millis("2026-07-17", "09:30:00"), "end": _millis("2026-07-17", "10:00:00"), "limit": 5}, + "futures_minutes_realtime": {"symbols": "A2609.DCE"}, + "futures_realtime_day_kline": {"symbols": "A2609.DCE"}, "futures_nanhua_index_kline": {"code": "AG_NH", "trade_date": None, "start_date": None, "end_date": None, "page": 1, "page_size": 5}, "global_index_daily_kline": {"secid": "100.HSI", "start_date": "20260701", "end_date": "20260717", "limit": 5}, "kline_pattern_annotations": {"date": "20260918", "symbol": None, "pattern": None, "page": 1, "page_size": 5}, diff --git a/tests/test_client.py b/tests/test_client.py index 7db6bc7..fb7c158 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -550,7 +550,7 @@ def test_stock_description_rejects_page_size_above_200(): ("stock_unlock", {"stock_code": "000001"}), ("yzxdr_detail", {"year": 2026, "quarter": 2}), ("futures_minutes", {"symbol": "A2605.DCE", "interval": "1min", "limit": 5}), - ("futures_contract_kline", {"symbol": "A2605.DCE", "interval": "daily", "limit": 5}), + ("futures_kline", {"symbol": "A2605.DCE", "interval": "daily", "limit": 5}), ("company_list", {}), ("wallstreetcn_financial_calendar", {"start_date": "2026-05-01", "end_date": "2026-05-07"}), ("stk_limit", {}), @@ -791,6 +791,60 @@ def test_realtime_kline_methods_serialize_symbols_list_to_json(): assert session.calls[0]["params"] == {"symbols": '["600519.SH"]'} +def test_futures_kline_batch_and_realtime_day_kline_forward_symbols_list(): + session = FakeSession([FakeResponse(payload=[])]) + client = FtshareClient(session=session) + + client.futures_kline_batch( + symbols=["A2611.DCE", "M2611.DCE"], + interval="daily", + start=1790784000000, + end=1791647999000, + limit=5, + as_dataframe=False, + ) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v2/market/data/futures/kline/batch" + assert session.calls[0]["params"] == { + "symbols": ["A2611.DCE", "M2611.DCE"], + "interval": "daily", + "start": 1790784000000, + "end": 1791647999000, + "limit": 5, + } + + session = FakeSession([FakeResponse(payload=[])]) + client = FtshareClient(session=session) + + client.futures_realtime_day_kline(symbols=["A2611.DCE", "CU2611.SHF"], as_dataframe=False) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v4/market/data/futures-realtime-day-kline" + assert session.calls[0]["params"] == {"symbols": ["A2611.DCE", "CU2611.SHF"]} + + +def test_futures_minutes_forwards_symbols_for_batch_queries(): + session = FakeSession([FakeResponse(payload=[])]) + client = FtshareClient(session=session) + + client.futures_minutes( + symbols=["A2611.DCE", "CU2611.SHF"], + interval="5min", + start=1791475200000, + end=1791647999000, + limit=5, + as_dataframe=False, + ) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v2/market/data/futures_minutes" + assert session.calls[0]["params"] == { + "symbols": ["A2611.DCE", "CU2611.SHF"], + "interval": "5min", + "start": 1791475200000, + "end": 1791647999000, + "limit": 5, + } + + def test_etf_pcf_infos_unwraps_bare_object_data(): session = FakeSession([FakeResponse(payload={"code": 200, "message": "success", "data": {"symbol": "510300.SH", "trade_date": 20260909, "creation_redemption_unit": 900000}})]) client = FtshareClient(session=session) From d3a92359580a7ff922f17f1a6fded8acc1212a21 Mon Sep 17 00:00:00 2001 From: zhuhao Date: Tue, 13 Oct 2026 20:35:25 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat=EF=BC=9A=E6=96=B0=E5=A2=9E=E8=82=A1?= =?UTF-8?q?=E7=A5=A8=E6=8A=80=E6=9C=AF=E9=9D=A2=E5=9B=A0=E5=AD=90=E3=80=81?= =?UTF-8?q?A=E8=82=A1=E4=BB=A3=E7=A0=81=E5=8F=98=E6=9B=B4=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E7=BA=BF=E6=8E=A5=E5=8F=A3=EF=BC=8C=E8=82=A1=E6=9C=AC?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=85=A8=E5=B8=82=E5=9C=BA=E5=88=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- docs/API_REFERENCE.md | 79 ++++++++++++++++++++++++-- src/ftshare/apis/stock.py | 100 +++++++++++++++++++++++++++++++-- src/ftshare/endpoints/stock.py | 17 +++++- src/ftshare/response.py | 6 +- tests/endpoint_cases.py | 3 + tests/test_client.py | 59 +++++++++++++++++++ 6 files changed, 251 insertions(+), 13 deletions(-) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 61be43a..0de8594 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -6,13 +6,13 @@ | 指标 | 数量 | |---|---:| -| SDK 方法总数 | 242 | +| SDK 方法总数 | 244 | ## 专题分布 | ftshare-doc 专题 | SDK 方法数 | API mixin 模块 | Endpoint 模块 | |---|---:|---|---| -| 股票数据 | 129 | `ftshare.apis.stock` | `ftshare.endpoints.stock` | +| 股票数据 | 131 | `ftshare.apis.stock` | `ftshare.endpoints.stock` | | 港股数据 | 3 | `ftshare.apis.hk` | `ftshare.endpoints.hk` | | 美股数据 | 2 | `ftshare.apis.us` | `ftshare.endpoints.us` | | 指数专题 | 15 | `ftshare.apis.index` | `ftshare.endpoints.index` | @@ -90,6 +90,7 @@ | [`southbound`](#api-southbound) | 南向资金交易 | `GET` | `api/v1/market/data/southbound` | `date` | `南向资金交易.md` | | [`stk_alert_broker`](#api-stk-alert-broker) | 交易所重点提示证券 | `GET` | `api/v2/market/data/stk-alert-broker` | `ts_code`, `start_date`, `end_date`, `page`, `page_size` | `交易所重点提示证券.md` | | [`stk_code_change`](#api-stk-code-change) | A股代码变更 | `GET` | `api/v1/market/data/stk-code-change` | `trade_code`, `start_date`, `end_date` | `A股代码变更.md` | +| [`stk_code_change_timeline`](#api-stk-code-change-timeline) | A股代码变更时间线 | `GET` | `api/v1/market/data/stk_code_change_timeline` | - | `A股代码变更时间线.md` | | [`stk_status_change`](#api-stk-status-change) | A股状态变更 | `GET` | `api/v1/market/data/stk-status-change` | `trade_code`, `change_date`, `change_type` | `A股状态变更.md` | | [`stk_limit`](#api-stk-limit) | 涨跌停价 | `GET` | `api/v1/market/data/stk-limit` | `instrument_type`, `symbol`, `symbol_id`, `market_id`, `trade_date`, `start_date`, `end_date`, `page`, `page_size` | `涨跌停价.md` | | [`stk_manager_hold`](#api-stk-manager-hold) | 上市公司管理层持股 | `GET` | `api/v1/market/data/stk-manager-hold` | `trade_code`, `end_date` | `上市公司管理层持股.md` | @@ -111,6 +112,7 @@ | [`stock_daec_stocks`](#api-stock-daec-stocks) | A股行情列表 | `GET` | `api/v1/market/data/daec/stocks/{board}` | `board`, `page`, `page_size`, `filter`, `order_by` | `A股行情列表.md` | | [`stock_description`](#api-stock-description) | 股票基础信息 | `GET` | `api/v1/market/data/stock-description` | `symbol_id`, `page`, `page_size` | `股票基础信息.md` | | [`stock_dividends`](#api-stock-dividends) | 股票分红记录 | `GET` | `api/v1/market/data/stock-dividends` | `symbol`, `since_date`, `until_date`, `page`, `page_size` | `股票分红记录.md` | +| [`stock_factor`](#api-stock-factor) | 股票技术面因子 | `GET` | `api/v2/market/data/stock-factor` | `symbol`, `trade_date`, `category` | `股票技术面因子.md` | | [`stock_filter`](#api-stock-filter) | 股票筛选 | `GET` | `api/v2/market/data/stock-list/filter` | `symbol`, `board`, `listing_date_since`, `page`, `page_size` | `股票筛选.md` | | [`stock_float_holders`](#api-stock-float-holders) | 十大流通股东 | `GET` | `api/v1/market/data/holder/stock-holder-ften` | `stock_code`, `is_last`, `page`, `page_size` | `十大流通股东.md` | | [`stock_ggcg_em`](#api-stock-ggcg-em) | 东方财富股东增减持 | `GET` | `api/v1/market/data/holder/stock-ggcg-em` | `symbol`, `page`, `page_size` | `东方财富股东增减持.md` | @@ -136,7 +138,7 @@ | [`stock_realtime_day_kline`](#api-stock-realtime-day-kline) | 股票实时日K线 | `GET` | `api/v4/market/data/stock-realtime-day-kline` | `symbols` | `股票实时日K线.md` | | [`stock_realtime_list`](#api-stock-realtime-list) | A股行情列表 | `GET` | `api/v1/market/data/stock-list/{board}` | `board`, `page`, `page_size` | `A股行情列表.md` | | [`stock_realtime_minute_kline`](#api-stock-realtime-minute-kline) | 股票实时分钟K线 | `GET` | `api/v4/market/data/stock-realtime-minute-kline` | `symbols` | `股票实时分钟K线.md` | -| [`stock_share`](#api-stock-share) | 股本 | `GET` | `api/v2/market/data/share/get-stock-share` | `stock_code`, `date` | `股本.md` | +| [`stock_share`](#api-stock-share) | 股本 | `GET` | `api/v2/market/data/share/get-stock-share` | `stock_code`, `date`, `date_type`, `page`, `page_size` | `股本.md` | | [`stock_share_chg`](#api-stock-share-chg) | 股东增减持 | `GET` | `api/v1/market/data/holder/stock-share-chg` | `stock_code`, `is_last`, `page`, `page_size` | `股东增减持.md` | | [`stock_st_history`](#api-stock-st-history) | 股票历史ST状态 | `GET` | `api/v1/market/data/stock-st-history` | `symbol`, `st_type` | `股票历史ST状态.md` | | [`stock_unlock`](#api-stock-unlock) | 限售解禁 | `GET` | `api/v1/market/data/unlock/stock_unlock` | `stock_code`, `start_date`, `end_date`, `page`, `page_size` | `限售解禁.md` | @@ -2221,6 +2223,38 @@ Returns: payloads when multi-page fetching is used with ``raw=True``. ``` +

stock_factor

+ +- 接口名称:股票技术面因子 +- HTTP:`GET` +- Path:`api/v2/market/data/stock-factor` +- 参数:`symbol`, `trade_date`, `category` +- 来源文档:`股票技术面因子.md` +- 原始接口:`market_stock_factor` + +```text +股票技术面因子. + +Endpoint: ``api/v2/market/data/stock-factor``. +Method: ``GET``. +Documented endpoint: ``market_stock_factor``. + +Args: + symbol: 沪深北 A 股代码,必须带交易所后缀,如 ``000001.SZ`` (type: string; required: Y). + trade_date: 目标日期 YYYYMMDD;不传时以当前日期为目标日期 (type: string; required: N). + category: 返回字段分类:``quote``、``valuation``、``capital``、``trend``、``momentum``、``volatility``、``volume`` 或 ``sentiment``;不传时返回全部字段 (type: string; required: N). + raw: Return the decoded JSON payload without tabular extraction. + fields: Optional field list or comma-separated field string applied after extraction. + as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. + **kwargs: Extra request parameters forwarded unchanged. Useful when the service adds parameters before the SDK is regenerated. + +Returns: + A pandas ``DataFrame`` by default, Python rows when + ``as_dataframe=False``, raw JSON when ``raw=True``。服务端把因子放在 + ``data.values``,SDK 直接返回该对象(``as_dataframe=True`` 时为单行 + ``DataFrame``)。 +``` +

stock_filter

- 接口名称:股票筛选 @@ -2947,20 +2981,26 @@ Returns: - 接口名称:股本 - HTTP:`GET` - Path:`api/v2/market/data/share/get-stock-share` -- 参数:`stock_code`, `date` +- 参数:`stock_code`, `date`, `date_type`, `page`, `page_size` - 来源文档:`股本.md` - 原始接口:`get_stock_share` ```text 股本. -Endpoint: ``api/v1/market/data/share/get-stock-share``. +Endpoint: ``api/v2/market/data/share/get-stock-share``. Method: ``GET``. Documented endpoint: ``get_stock_share``. Args: - stock_code: 股票代码 (type: string; required: Y). + stock_code: 股票代码;不传时查询指定日期的全市场股本 (type: string; required: N). date: 日期 YYYYMMDD (type: string; required: Y). + date_type: 日期条件类型:``publish_date``(发布日期,默认)或 ``end_date``(生效日期) (type: string; required: N). + page: Page number, starting from 1. If omitted, the server default is used unless ``limit`` or ``all_pages`` is set. + page_size: Rows per page. The SDK validates this against the endpoint-specific maximum. + limit: Maximum number of rows to return. The SDK may fetch multiple pages to satisfy this limit. + all_pages: Fetch and combine pages until the server reports the last page. + max_pages: Optional safety cap for ``all_pages``. raw: Return the decoded JSON payload without tabular extraction. fields: Optional field list or comma-separated field string applied after extraction. as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. @@ -3258,6 +3298,33 @@ Returns: payloads when multi-page fetching is used with ``raw=True``. ``` +

stk_code_change_timeline

+ +- 接口名称:A股代码变更时间线 +- HTTP:`GET` +- Path:`api/v1/market/data/stk_code_change_timeline` +- 参数:- +- 来源文档:`A股代码变更时间线.md` +- 原始接口:`stk_code_change_timeline` + +```text +A股代码变更时间线. + +Endpoint: ``api/v1/market/data/stk_code_change_timeline``. +Method: ``GET``. +Documented endpoint: ``stk_code_change_timeline``. + +Args: + raw: Return the decoded JSON payload without tabular extraction. + fields: Optional field list or comma-separated field string applied after extraction. + as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. + **kwargs: Extra request parameters forwarded unchanged. Useful when the service adds parameters before the SDK is regenerated. + +Returns: + A pandas ``DataFrame`` by default, Python rows when + ``as_dataframe=False``, raw JSON when ``raw=True``. +``` +

tdx_board_members

- 接口名称:通达信板块成分股最新快照 diff --git a/src/ftshare/apis/stock.py b/src/ftshare/apis/stock.py index f2cc6f7..5d1d079 100644 --- a/src/ftshare/apis/stock.py +++ b/src/ftshare/apis/stock.py @@ -1714,6 +1714,33 @@ def stk_code_change( request_params.update(kwargs) return self._call_endpoint('stk_code_change', raw=raw, fields=fields, as_dataframe=as_dataframe, **request_params) + def stk_code_change_timeline( + self, + *, + raw: bool = False, + fields: Sequence[str] | str | None = None, + as_dataframe: bool = True, + **kwargs: Any, + ) -> Any: + """A股代码变更时间线. + + Endpoint: ``api/v1/market/data/stk_code_change_timeline``. + Method: ``GET``. + Documented endpoint: ``stk_code_change_timeline``. + + Args: + raw: Return the decoded JSON payload without tabular extraction. + fields: Optional field list or comma-separated field string applied after extraction. + as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. + **kwargs: Extra request parameters forwarded unchanged. Useful when the service adds parameters before the SDK is regenerated. + + Returns: + A pandas ``DataFrame`` by default, Python rows when + ``as_dataframe=False``, raw JSON when ``raw=True``. + """ + request_params = dict(kwargs) + return self._call_endpoint('stk_code_change_timeline', raw=raw, fields=fields, as_dataframe=as_dataframe, **request_params) + def stk_status_change( self, trade_code: Any | None = None, @@ -3900,6 +3927,12 @@ def stock_share( self, stock_code: Any | None = None, date: Any | None = None, + date_type: Any | None = None, + page: int | None = None, + page_size: int | None = None, + limit: int | None = None, + all_pages: bool = False, + max_pages: int | None = None, *, raw: bool = False, fields: Sequence[str] | str | None = None, @@ -3908,13 +3941,19 @@ def stock_share( ) -> Any: """股本. - Endpoint: ``api/v1/market/data/share/get-stock-share``. + Endpoint: ``api/v2/market/data/share/get-stock-share``. Method: ``GET``. Documented endpoint: ``get_stock_share``. Args: - stock_code: 股票代码 (type: string; required: Y). + stock_code: 股票代码;不传时查询指定日期的全市场股本 (type: string; required: N). date: 日期 YYYYMMDD (type: string; required: Y). + date_type: 日期条件类型:``publish_date``(发布日期,默认)或 ``end_date``(生效日期) (type: string; required: N). + page: Page number, starting from 1. If omitted, the server default is used unless ``limit`` or ``all_pages`` is set. + page_size: Rows per page. The SDK validates this against the endpoint-specific maximum. + limit: Maximum number of rows to return. The SDK may fetch multiple pages to satisfy this limit. + all_pages: Fetch and combine pages until the server reports the last page. + max_pages: Optional safety cap for ``all_pages``. raw: Return the decoded JSON payload without tabular extraction. fields: Optional field list or comma-separated field string applied after extraction. as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. @@ -3925,13 +3964,64 @@ def stock_share( ``as_dataframe=False``, raw JSON when ``raw=True``, or raw page payloads when multi-page fetching is used with ``raw=True``. """ - request_params = {'stock_code': stock_code, 'date': date} + request_params = {'stock_code': stock_code, 'date': date, 'date_type': date_type} request_params.update(kwargs) - return self._call_endpoint( - 'stock_share', + endpoint = ENDPOINTS['stock_share'] + return self.get_paginated( + endpoint.path, + page=page, + page_size=page_size, + limit=limit, + all_pages=all_pages, + max_pages=max_pages, + max_page_size=endpoint.max_page_size, + raw=raw, + fields=fields, + as_dataframe=as_dataframe, + **request_params, + ) + + def stock_factor( + self, + symbol: Any | None = None, + trade_date: Any | None = None, + category: Any | None = None, + *, + raw: bool = False, + fields: Sequence[str] | str | None = None, + as_dataframe: bool = True, + **kwargs: Any, + ) -> Any: + """股票技术面因子. + + Endpoint: ``api/v2/market/data/stock-factor``. + Method: ``GET``. + Documented endpoint: ``market_stock_factor``. + + Args: + symbol: 沪深北 A 股代码,必须带交易所后缀,如 ``000001.SZ`` (type: string; required: Y). + trade_date: 目标日期 YYYYMMDD;不传时以当前日期为目标日期 (type: string; required: N). + category: 返回字段分类:``quote``、``valuation``、``capital``、``trend``、``momentum``、``volatility``、``volume`` 或 ``sentiment``;不传时返回全部字段 (type: string; required: N). + raw: Return the decoded JSON payload without tabular extraction. + fields: Optional field list or comma-separated field string applied after extraction. + as_dataframe: Return a pandas ``DataFrame`` by default; set to ``False`` for Python rows. + **kwargs: Extra request parameters forwarded unchanged. Useful when the service adds parameters before the SDK is regenerated. + + Returns: + A pandas ``DataFrame`` by default, Python rows when + ``as_dataframe=False``, raw JSON when ``raw=True``。服务端把因子放在 + ``data.values``,SDK 直接返回该对象(``as_dataframe=True`` 时为单行 + ``DataFrame``)。 + """ + request_params = {'symbol': symbol, 'trade_date': trade_date, 'category': category} + request_params.update(kwargs) + path = ENDPOINTS['stock_factor'].path + return self.get( + path, raw=raw, fields=fields, as_dataframe=as_dataframe, + unwrap_bare_data=True, **request_params, ) diff --git a/src/ftshare/endpoints/stock.py b/src/ftshare/endpoints/stock.py index 1fe68ea..e185bb9 100644 --- a/src/ftshare/endpoints/stock.py +++ b/src/ftshare/endpoints/stock.py @@ -520,7 +520,15 @@ 'title': '股本', 'doc_file': '股本.md', 'original_api': 'get_stock_share', - 'params': ('stock_code', 'date'), + 'params': ('stock_code', 'date', 'date_type', 'page', 'page_size'), + 'max_page_size': 500, + }, + 'stock_factor': { + 'path': 'api/v2/market/data/stock-factor', + 'title': '股票技术面因子', + 'doc_file': '股票技术面因子.md', + 'original_api': 'market_stock_factor', + 'params': ('symbol', 'trade_date', 'category'), }, 'stock_ggcg_em': { 'path': 'api/v1/market/data/holder/stock-ggcg-em', @@ -537,6 +545,13 @@ 'original_api': 'get_stk_code_change', 'params': ('trade_code', 'start_date', 'end_date'), }, + 'stk_code_change_timeline': { + 'path': 'api/v1/market/data/stk_code_change_timeline', + 'title': 'A股代码变更时间线', + 'doc_file': 'A股代码变更时间线.md', + 'original_api': 'stk_code_change_timeline', + 'params': (), + }, 'stk_status_change': { 'path': 'api/v1/market/data/stk-status-change', 'title': 'A股状态变更', diff --git a/src/ftshare/response.py b/src/ftshare/response.py index e316061..fbe499b 100644 --- a/src/ftshare/response.py +++ b/src/ftshare/response.py @@ -31,7 +31,8 @@ def extract_tabular(payload: Any, unwrap_bare_data: bool = False) -> Any: When ``unwrap_bare_data`` is true and ``data`` is an object without ``records``/``items``/``index_descriptions`` rows (single-item query - shapes), the ``data`` object itself is returned instead of the full + shapes), a nested ``data.values`` object is returned when present, + otherwise the ``data`` object itself is returned instead of the full envelope. Any unsupported shape is returned unchanged so callers do not lose data. @@ -48,6 +49,9 @@ def extract_tabular(payload: Any, unwrap_bare_data: bool = False) -> Any: if isinstance(data.get("index_descriptions"), list): return data["index_descriptions"] if unwrap_bare_data: + values = data.get("values") + if isinstance(values, dict): + return values return data if isinstance(payload.get("items"), list): return payload["items"] diff --git a/tests/endpoint_cases.py b/tests/endpoint_cases.py index c316e85..cf14a3a 100644 --- a/tests/endpoint_cases.py +++ b/tests/endpoint_cases.py @@ -322,6 +322,8 @@ def _millis(day: str, time: str = "00:00:00") -> int: "stk_surv": {"ts_code": None, "trade_date": None, "start_date": None, "end_date": None, "page": 1, "page_size": 5}, "stock_dividends": {"symbol": "600519.SH", "since_date": None, "until_date": None, "page": 1, "page_size": 5}, "stock_dividends_effective": {"symbol": "600519.SH", "since_date": None, "until_date": None, "page": 1, "page_size": 5}, + "stock_factor": {"symbol": "000001.SZ", "trade_date": "20261012", "category": "trend"}, + "stock_share": {"stock_code": "920476.BJ", "date": "20261012", "date_type": "end_date", "page": 1, "page_size": 5}, "stock_ggmx": {"stock_code": None, "change_direction": None, "start_date": None, "end_date": None, "page": 1, "page_size": 5}, "stock_minute_seal": {"trade_date": "20260923", "symbol": "000560.SZ"}, "sw_industry_constituent_history": {"industry_code": "801010.SI"}, @@ -355,6 +357,7 @@ def _millis(day: str, time: str = "00:00:00") -> int: "stk_manager_pay": {"trade_code": "000001.SZ", "end_date": None}, "namechange": {"trade_code": "000001.SZ", "start_date": None, "end_date": None}, "stk_code_change": {"trade_code": "001872.SZ", "start_date": None, "end_date": None}, + "stk_code_change_timeline": {}, "stock_st_history": {"symbol": "600735.SH", "st_type": None}, "limit_event_timeline_3s": {"symbol": "000019.SZ", "trade_date": "20260909", "page": 1, "page_size": 5}, "etf_pre_single": {"symbol": "510300.SH", "date": "20260717"}, diff --git a/tests/test_client.py b/tests/test_client.py index fb7c158..49966d3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1491,6 +1491,65 @@ def test_stock_share_chg_forwards_is_last_paging(): assert session.calls[0]["params"]["is_last"] == "true" +def test_stock_share_forwards_date_type_and_full_market_pagination(): + session = FakeSession([FakeResponse(payload={"items": [], "total_pages": 0, "total_items": 0})]) + client = FtshareClient(session=session) + + client.stock_share(date="20261012", date_type="end_date", page=1, page_size=5, as_dataframe=False) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v2/market/data/share/get-stock-share" + assert session.calls[0]["params"] == {"date": "20261012", "date_type": "end_date", "page": 1, "page_size": 5} + + +def test_stock_share_rejects_page_size_above_500(): + client = FtshareClient(session=FakeSession([])) + + with pytest.raises(ValueError, match="page_size must be between 1 and 500"): + client.stock_share(date="20261012", page_size=501) + + +def test_stock_factor_unwraps_values_object(): + session = FakeSession([FakeResponse(payload={"code": 200, "message": "success", "data": {"values": {"ts_code": "000001.SZ", "trade_date": "20261012", "close": 11.73}}})]) + client = FtshareClient(session=session) + + result = client.stock_factor(symbol="000001.SZ", trade_date="20261012", category="quote", as_dataframe=False) + + assert result == {"ts_code": "000001.SZ", "trade_date": "20261012", "close": 11.73} + + +def test_stock_factor_values_object_dataframe_is_single_row(): + session = FakeSession([FakeResponse(payload={"code": 200, "message": "success", "data": {"values": {"ts_code": "000001.SZ", "close": 11.73}}})]) + client = FtshareClient(session=session) + + frame = client.stock_factor(symbol="000001.SZ") + + assert isinstance(frame, pd.DataFrame) + assert len(frame) == 1 + assert frame.loc[0, "ts_code"] == "000001.SZ" + assert frame.loc[0, "close"] == 11.73 + + +def test_stock_factor_forwards_symbol_date_and_category(): + session = FakeSession([FakeResponse(payload={"code": 200, "message": "success", "data": {"values": {}}})]) + client = FtshareClient(session=session) + + client.stock_factor(symbol="000001.SZ", trade_date="20261012", category="trend", as_dataframe=False) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v2/market/data/stock-factor" + assert session.calls[0]["params"] == {"symbol": "000001.SZ", "trade_date": "20261012", "category": "trend"} + + +def test_stk_code_change_timeline_hits_documented_path_and_extracts_data(): + session = FakeSession([FakeResponse(payload={"code": 200, "message": "success", "data": [{"stock_name": "上海医药", "new_code": "601607", "market": "SH"}]})]) + client = FtshareClient(session=session) + + rows = client.stk_code_change_timeline(as_dataframe=False) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v1/market/data/stk_code_change_timeline" + assert session.calls[0]["params"] == {} + assert rows == [{"stock_name": "上海医药", "new_code": "601607", "market": "SH"}] + + def test_ths_industry_constituents_forwards_filters_and_pagination(): session = FakeSession([FakeResponse(payload={"items": [], "total_pages": 0, "total_items": 0})]) client = FtshareClient(session=session)