diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 3b5f97b..b1f11e8 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -147,6 +147,7 @@ | [`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`, `sector_name`, `page`, `page_size` | `同花顺概念板块资金流日度.md` | | [`ths_industry_daily_flow`](#api-ths-industry-daily-flow) | 同花顺行业板块资金流日度 | `GET` | `api/v1/market/data/ths-industry-daily-flow` | `start_date`, `end_date`, `sector_name`, `page`, `page_size` | `同花顺行业板块资金流日度.md` | +| [`ths_industry_constituents`](#api-ths-industry-constituents) | 同花顺行业成分股列表 | `GET` | `api/v1/market/data/ths-industry-constituents` | `industry_code`, `industry_name`, `stock_code`, `stock_name`, `page`, `page_size` | `同花顺行业成分股列表.md` | | [`ths_hot_list`](#api-ths-hot-list) | 同花顺热榜 | `GET` | `api/v1/market/data/ths-hot-list` | `list_type`, `trade_date`, `page`, `page_size` | `同花顺热榜.md` | | [`trading_calendar`](#api-trading-calendar) | 交易日历 | `GET` | `api/v1/market/data/time/trading-calendar` | `market`, `start_date`, `end_date` | `交易日历.md` | | [`xueqiu_rank`](#api-xueqiu-rank) | 雪球股票排名 | `GET` | `api/v1/market/data/xueqiu-rank` | `rank_group`, `period`, `trade_date`, `page`, `page_size` | `雪球股票排名.md` | @@ -210,8 +211,8 @@ | SDK 方法 | 接口名称 | HTTP | Path | 参数 | 来源文档 | |---|---|---|---|---|---| | [`fund_asset_allocation`](#api-fund-asset-allocation) | 基金资产配置 | `GET` | `api/v1/market/data/fund/fund-asset-allocation` | `fund_code`, `report_date`, `publish_date`, `start_date`, `end_date`, `page`, `page_size` | `基金资产配置.md` | -| [`fund_basicinfo`](#api-fund-basicinfo) | 基金基础信息 | `GET` | `api/v1/market/data/fund/fund-basicinfo` | `institution_code`, `page`, `page_size` | `基金基础信息.md` | -| [`fund_cal_return`](#api-fund-cal-return) | 基金收益 | `GET` | `api/v1/market/data/fund/fund-cal-return` | `institution_code`, `cal-type` | `基金收益.md` | +| [`fund_basicinfo`](#api-fund-basicinfo) | 基金基础信息 | `GET` | `api/v1/market/data/fund/fund-basicinfo` | `fund_code`, `page`, `page_size` | `基金基础信息.md` | +| [`fund_cal_return`](#api-fund-cal-return) | 基金收益 | `GET` | `api/v1/market/data/fund/fund-cal-return` | `fund_code`, `cal-type` | `基金收益.md` | | [`fund_classification`](#api-fund-classification) | 基金分类 | `GET` | `api/v1/market/data/fund/fund-classification` | `fund_code`, `classify_std` | `基金分类.md` | | [`fund_company`](#api-fund-company) | 基金公司 | `GET` | `api/v1/market/data/fund/fund-company` | `fund_company`, `page`, `page_size` | `基金公司.md` | | [`fund_fee`](#api-fund-fee) | 基金费率 | `GET` | `api/v1/market/data/fund/fund-fee` | `fund_code`, `charge_type`, `client_type`, `page`, `page_size` | `基金费率.md` | @@ -3022,6 +3023,16 @@ Documented endpoint: ``ths_concept_daily_flow``. Documented endpoint: ``ths_industry_daily_flow``. +

ths_industry_constituents

+ +- 同花顺行业成分股列表 +- 原始接口:`ths_industry_constituents` +- Path:`api/v1/market/data/ths-industry-constituents` +- Method:`GET` +- 参数:`industry_code`, `industry_name`, `stock_code`, `stock_name`, `page`, `page_size` + +Documented endpoint: ``ths_industry_constituents``. +

ths_board_kline

- 接口名称:同花顺板块K线 @@ -3889,7 +3900,7 @@ Returns: - 接口名称:基金基础信息 - HTTP:`GET` - Path:`api/v1/market/data/fund/fund-basicinfo` -- 参数:`institution_code`, `page`, `page_size` +- 参数:`fund_code`, `page`, `page_size` - 来源文档:`基金基础信息.md` - 原始接口:`get_fund_basicinfo` @@ -3901,7 +3912,7 @@ Method: ``GET``. Documented endpoint: ``get_fund_basicinfo``. Args: - institution_code: 基金代码 (type: string; required: Y). + fund_code: 基金代码;不传时查询全市场数据的默认分页 (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. @@ -3923,7 +3934,7 @@ Returns: - 接口名称:基金收益 - HTTP:`GET` - Path:`api/v1/market/data/fund/fund-cal-return` -- 参数:`institution_code`, `cal-type` +- 参数:`fund_code`, `cal-type` - 来源文档:`基金收益.md` - 原始接口:`get_fund_cal_return` @@ -3935,7 +3946,7 @@ Method: ``GET``. Documented endpoint: ``get_fund_cal_return``. Args: - institution_code: 基金代码(6位数字) (type: string; required: Y). + fund_code: 基金代码(6位数字) (type: string; required: Y). cal_type: 查询区间:1M / 3M / 6M / 1Y / 3Y / 5Y / YTD(请求字段名为 `cal-type`) (type: string; required: Y). Request key: ``cal-type``. raw: Return the decoded JSON payload without tabular extraction. fields: Optional field list or comma-separated field string applied after extraction. diff --git a/src/ftshare/apis/fund.py b/src/ftshare/apis/fund.py index c3c5c86..67517aa 100644 --- a/src/ftshare/apis/fund.py +++ b/src/ftshare/apis/fund.py @@ -13,7 +13,7 @@ class FundApiMixin: def fund_basicinfo( self, - institution_code: Any | None = None, + fund_code: Any | None = None, page: int | None = None, page_size: int | None = None, limit: int | None = None, @@ -32,7 +32,7 @@ def fund_basicinfo( Documented endpoint: ``get_fund_basicinfo``. Args: - institution_code: 基金代码 (type: string; required: Y). + fund_code: 基金代码;不传时查询全市场数据的默认分页 (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. @@ -48,16 +48,17 @@ def fund_basicinfo( ``as_dataframe=False``, raw JSON when ``raw=True``, or raw page payloads when multi-page fetching is used with ``raw=True``. """ - request_params = {'institution_code': institution_code} + request_params = {'fund_code': fund_code} request_params.update(kwargs) - path = ENDPOINTS['fund_basicinfo'].path + endpoint = ENDPOINTS['fund_basicinfo'] return self.get_paginated( - path, + 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, @@ -66,7 +67,7 @@ def fund_basicinfo( def fund_cal_return( self, - institution_code: Any | None = None, + fund_code: Any | None = None, cal_type: Any | None = None, *, raw: bool = False, @@ -81,7 +82,7 @@ def fund_cal_return( Documented endpoint: ``get_fund_cal_return``. Args: - institution_code: 基金代码(6位数字) (type: string; required: Y). + fund_code: 基金代码(6位数字) (type: string; required: Y). cal_type: 查询区间:1M / 3M / 6M / 1Y / 3Y / 5Y / YTD(请求字段名为 `cal-type`) (type: string; required: Y). Request key: ``cal-type``. raw: Return the decoded JSON payload without tabular extraction. fields: Optional field list or comma-separated field string applied after extraction. @@ -93,7 +94,7 @@ def fund_cal_return( ``as_dataframe=False``, raw JSON when ``raw=True``, or raw page payloads when multi-page fetching is used with ``raw=True``. """ - request_params = {'institution_code': institution_code, 'cal-type': cal_type} + request_params = {'fund_code': fund_code, 'cal-type': cal_type} request_params.update(kwargs) return self._call_endpoint( 'fund_cal_return', diff --git a/src/ftshare/apis/stock.py b/src/ftshare/apis/stock.py index c69e7c4..2de859e 100644 --- a/src/ftshare/apis/stock.py +++ b/src/ftshare/apis/stock.py @@ -4100,6 +4100,14 @@ def ths_industry_daily_flow(self, start_date: Any | None = None, end_date: Any | return self.get_paginated(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) + def ths_industry_constituents(self, industry_code: Any | None = None, industry_name: Any | None = None, stock_code: Any | None = None, stock_name: 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 = {'industry_code': industry_code, 'industry_name': industry_name, 'stock_code': stock_code, 'stock_name': stock_name} + params.update(kwargs) + path = ENDPOINTS['ths_industry_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=1000, raw=raw, fields=fields, as_dataframe=as_dataframe, **params) + + def stock_realtime_minute_kline(self, symbols: Any | None = None, *, raw: bool = False, fields: Sequence[str] | str | None = None, as_dataframe: bool = True, **kwargs: Any) -> Any: """股票实时分钟K线.""" params = {'symbols': symbols} diff --git a/src/ftshare/endpoints/fund.py b/src/ftshare/endpoints/fund.py index d77a3a1..745b267 100644 --- a/src/ftshare/endpoints/fund.py +++ b/src/ftshare/endpoints/fund.py @@ -11,14 +11,15 @@ 'title': '基金基础信息', 'doc_file': '基金基础信息.md', 'original_api': 'get_fund_basicinfo', - 'params': ('institution_code', 'page', 'page_size'), + 'params': ('fund_code', 'page', 'page_size'), + 'max_page_size': 500, }, 'fund_cal_return': { 'path': 'api/v1/market/data/fund/fund-cal-return', 'title': '基金收益', 'doc_file': '基金收益.md', 'original_api': 'get_fund_cal_return', - 'params': ('institution_code', 'cal-type'), + 'params': ('fund_code', 'cal-type'), }, 'fund_overview': { 'path': 'api/v1/market/data/fund/fund-overview', diff --git a/src/ftshare/endpoints/stock.py b/src/ftshare/endpoints/stock.py index d58287d..dbd1fa4 100644 --- a/src/ftshare/endpoints/stock.py +++ b/src/ftshare/endpoints/stock.py @@ -813,6 +813,15 @@ 'max_page_size': 1000, }, + 'ths_industry_constituents': { + 'path': 'api/v1/market/data/ths-industry-constituents', + 'title': '同花顺行业成分股列表', + 'doc_file': '同花顺行业成分股列表.md', + 'original_api': 'ths_industry_constituents', + 'params': ('industry_code', 'industry_name', 'stock_code', 'stock_name', 'page', 'page_size'), + 'max_page_size': 1000, + }, + 'stock_realtime_minute_kline': { 'path': 'api/v4/market/data/stock-realtime-minute-kline', 'title': '股票实时分钟K线', diff --git a/src/ftshare/response.py b/src/ftshare/response.py index dbe4b22..e17cead 100644 --- a/src/ftshare/response.py +++ b/src/ftshare/response.py @@ -24,12 +24,16 @@ def extract_tabular(payload: Any) -> Any: Supported envelopes: - ``{"data": {"records": [...]}}`` + - ``{"data": {"items": [...]}}`` + - ``{"data": [...]}`` - ``{"items": [...]}`` Any unsupported shape is returned unchanged so callers do not lose data. """ if isinstance(payload, dict): data = payload.get("data") + if isinstance(data, list): + return data if isinstance(data, dict) and isinstance(data.get("records"), list): return data["records"] if isinstance(data, dict) and isinstance(data.get("items"), list): diff --git a/tests/endpoint_cases.py b/tests/endpoint_cases.py index e9b9edb..1c71656 100644 --- a/tests/endpoint_cases.py +++ b/tests/endpoint_cases.py @@ -56,7 +56,6 @@ "index_slug": "HSI", "industry_code": "801010", "inst_type": "all_inst", - "institution_code": "000001", "institution_id": "1001", "industry_name": "银行", "instrument_id": "A2609", diff --git a/tests/test_client.py b/tests/test_client.py index 38663c7..aadc6d3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -860,6 +860,66 @@ def test_stock_share_chg_forwards_is_last_paging(): assert session.calls[0]["params"]["is_last"] == "true" +def test_ths_industry_constituents_forwards_filters_and_pagination(): + session = FakeSession([FakeResponse(payload={"items": [], "total_pages": 0, "total_items": 0})]) + client = FtshareClient(session=session) + + client.ths_industry_constituents(industry_name="证券", page=1, page_size=1000, as_dataframe=False) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v1/market/data/ths-industry-constituents" + assert session.calls[0]["params"] == {"industry_name": "证券", "page": 1, "page_size": 1000} + + +def test_fund_basicinfo_paginated_with_fund_code(): + session = FakeSession([FakeResponse(payload={ + "code": 200, + "message": "success", + "data": {"items": [{"fund_code": "110011", "fund_name": "易方达优质精选混合(QDII)"}], + "page_num": 1, "page_size": 500, "total": 1, "pages": 1}, + })]) + client = FtshareClient(session=session) + + rows = client.fund_basicinfo(fund_code="110011", page=1, page_size=500, as_dataframe=False) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v1/market/data/fund/fund-basicinfo" + assert session.calls[0]["params"] == {"fund_code": "110011", "page": 1, "page_size": 500} + assert rows == [{"fund_code": "110011", "fund_name": "易方达优质精选混合(QDII)"}] + + +def test_fund_basicinfo_rejects_page_size_over_max(): + session = FakeSession([]) + client = FtshareClient(session=session) + + with pytest.raises(ValueError): + client.fund_basicinfo(page=1, page_size=501, as_dataframe=False) + + assert session.calls == [] + + +def test_fund_cal_return_forwards_fund_code_and_cal_type(): + session = FakeSession([FakeResponse(payload=[])]) + client = FtshareClient(session=session) + + client.fund_cal_return(fund_code="110011", cal_type="1M", as_dataframe=False) + + assert session.calls[0]["url"] == "https://market.ft.tech/gateway/api/v1/market/data/fund/fund-cal-return" + assert session.calls[0]["params"] == {"fund_code": "110011", "cal-type": "1M"} + + +def test_fund_cal_return_extracts_bare_array_data(): + session = FakeSession([FakeResponse(payload={ + "code": 200, + "message": "success", + "data": [{"date": 20260522, "return": 0.0}, {"date": 20260525, "return": 0.0016}], + })]) + client = FtshareClient(session=session) + + df = client.fund_cal_return(fund_code="110011", cal_type="1M") + + assert list(df.columns) == ["date", "return"] + assert len(df) == 2 + + def test_fund_share_forwards_paginated_params(): session = FakeSession([FakeResponse(payload={"items": [], "total_pages": 0, "total_items": 0})]) client = FtshareClient(session=session)