diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md
index b1f11e8..7561b6f 100644
--- a/docs/API_REFERENCE.md
+++ b/docs/API_REFERENCE.md
@@ -6,17 +6,17 @@
| 指标 | 数量 |
|---|---:|
-| SDK 方法总数 | 219 |
+| SDK 方法总数 | 227 |
## 专题分布
| ftshare-doc 专题 | SDK 方法数 | API mixin 模块 | Endpoint 模块 |
|---|---:|---|---|
-| 股票数据 | 118 | `ftshare.apis.stock` | `ftshare.endpoints.stock` |
+| 股票数据 | 119 | `ftshare.apis.stock` | `ftshare.endpoints.stock` |
| 港股数据 | 3 | `ftshare.apis.hk` | `ftshare.endpoints.hk` |
| 美股数据 | 2 | `ftshare.apis.us` | `ftshare.endpoints.us` |
-| 指数专题 | 14 | `ftshare.apis.index` | `ftshare.endpoints.index` |
-| ETF专题 | 11 | `ftshare.apis.etf` | `ftshare.endpoints.etf` |
+| 指数专题 | 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` |
| 债券专题 | 6 | `ftshare.apis.bond` | `ftshare.endpoints.bond` |
@@ -33,6 +33,7 @@
| [`report_announcement_list`](#api-report-announcement-list) | 报告公告列表 | `GET` | `api/v1/market/data/report-announcements/list` | `date`, `sec_code`, `page`, `page_size` | `报告公告列表.md` |
| [`report_announcement_summary`](#api-report-announcement-summary) | 报告公告摘要 | `GET` | `api/v1/market/data/report-announcements/summary` | `announcement_id` | `报告公告摘要.md` |
| [`stock_candlesticks_batch`](#api-stock-candlesticks-batch) | 批量股票K线 | `GET` | `api/v1/market/data/stock-candlesticks/batch` | `symbols`, `interval_unit`, `interval_value`, `adjust_kind`, `since_ts_millis`, `until_ts_millis`, `limit` | `批量股票K线.md` |
+| [`stock_dividends_effective`](#api-stock-dividends-effective) | 股票有效分红记录 | `GET` | `api/v2/market/data/stock-dividends-effective` | `symbol`, `since_date`, `until_date`, `page`, `page_size` | `股票有效分红记录.md` |
| [`stock_ggmx`](#api-stock-ggmx) | 董监高持股变动 | `GET` | `api/v1/market/data/holder/stock-ggmx` | `stock_code`, `change_direction`, `start_date`, `end_date`, `page`, `page_size` | `董监高持股变动.md` |
| [`stock_intraday_auction_volume_symbol`](#api-stock-intraday-auction-volume-symbol) | 单标的连续竞价成交量 | `GET` | `api/v1/market/data/intraday-auction-volume/symbol` | `symbol`, `trade_date`, `page`, `page_size` | `单标的连续竞价成交量.md` |
| [`stock_minutes_batch`](#api-stock-minutes-batch) | 批量股票历史分钟行情 | `GET` | `api/v2/market/data/stock_minutes/batch` | `symbols`, `interval_value`, `adjust_kind`, `since_ts_millis`, `until_ts_millis`, `limit` | `批量股票历史分钟行情.md` |
@@ -60,7 +61,7 @@
| [`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` |
| [`eastmoney_market_valuation`](#api-eastmoney-market-valuation) | 东方财富市场估值 | `GET` | `api/v1/market/data/eastmoney-market-valuation` | `market_code`, `trade_date`, `start_date`, `end_date`, `page`, `page_size` | `东方财富市场估值.md` |
| [`eastmoney_rank`](#api-eastmoney-rank) | 东方财富股票排名 | `GET` | `api/v1/market/data/eastmoney-rank` | `rank_group`, `market`, `trade_date` | `东方财富股票排名.md` |
-| [`eastmoney_sector_flow`](#api-eastmoney-sector-flow) | 东方财富板块资金流 | `GET` | `api/v1/market/data/eastmoney-sector-flow` | `sector_code`, `sector_type`, `trade_date`, `start_date`, `end_date`, `page`, `page_size` | `东方财富板块资金流.md` |
+| [`eastmoney_sector_flow`](#api-eastmoney-sector-flow) | 东方财富板块资金流 | `GET` | `api/v1/market/data/eastmoney-sector-flow` | `board_code`, `board_type`, `board_level`, `trade_date`, `start_date`, `end_date`, `page`, `page_size` | `东方财富板块资金流.md` |
| [`eastmoney_stock_flow`](#api-eastmoney-stock-flow) | 东方财富个股资金流 | `GET` | `api/v1/market/data/eastmoney-stock-flow` | `symbol`, `trade_date`, `start_date`, `end_date`, `page`, `page_size` | `东方财富个股资金流.md` |
| [`eastmoney_stock_valuation`](#api-eastmoney-stock-valuation) | 东方财富个股估值 | `GET` | `api/v1/market/data/eastmoney-stock-valuation` | `symbol`, `trade_date`, `start_date`, `end_date`, `page`, `page_size` | `东方财富个股估值.md` |
| [`exchange_margin_summaries`](#api-exchange-margin-summaries) | 交易所融资融券汇总日度 | `GET` | `api/v1/market/data/exchange-margin-summaries` | `start_date`, `end_date`, `exchange`, `page`, `page_size` | `交易所融资融券汇总日度.md` |
@@ -75,7 +76,7 @@
| [`limit_list`](#api-limit-list) | 涨跌停池 | `GET` | `api/v1/market/data/limit-list` | `limit_type`, `trade_date` | `涨跌停池.md` |
| [`limit_up_briefs`](#api-limit-up-briefs) | 涨停简报 | `GET` | `api/v3/market/data/limit-up-reports/briefs` | `date` | `涨停简报.md` |
| [`limit_up_public_report`](#api-limit-up-public-report) | 涨停对外归因报告 | `GET` | `api/v3/market/data/limit-up-reports/public-report` | `date`, `security_code` | `涨停对外归因报告.md` |
-| [`margin_trading_details`](#api-margin-trading-details) | 融资融券明细 | `GET` | `api/v1/market/data/margin-trading-details` | `date`, `page`, `page_size` | `融资融券明细.md` |
+| [`margin_trading_details`](#api-margin-trading-details) | 融资融券明细 | `GET` | `api/v1/market/data/margin-trading-details` | `date`, `start_date`, `end_date`, `stock`, `page`, `page_size` | `融资融券明细.md` |
| [`namechange`](#api-namechange) | 股票曾用名 | `GET` | `api/v1/market/data/namechange` | `trade_code`, `start_date`, `end_date` | `股票曾用名.md` |
| [`nth_trade_date`](#api-nth-trade-date) | 第N个交易日 | `GET` | `api/v1/market/data/time/get-nth-trade-date` | `n` | `第N个交易日.md` |
| [`northbound`](#api-northbound) | 北向资金交易 | `GET` | `api/v1/market/data/northbound` | `date` | `北向资金交易.md` |
@@ -145,8 +146,8 @@
| [`ths_board_kline`](#api-ths-board-kline) | 同花顺板块K线 | `GET` | `api/v1/market/data/ths-board-kline` | `board_code`, `page`, `page_size` | `同花顺板块K线.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`, `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_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` |
+| [`ths_industry_daily_flow`](#api-ths-industry-daily-flow) | 同花顺行业板块资金流日度 | `GET` | `api/v1/market/data/ths-industry-daily-flow` | `start_date`, `end_date`, `board_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` |
@@ -172,6 +173,7 @@
### 指数专题
| [`index_minutes_batch`](#api-index-minutes-batch) | 批量指数历史分钟行情 | `GET` | `api/v2/market/data/index_minutes/batch` | `symbols`, `interval_value`, `since_ts_millis`, `until_ts_millis`, `limit` | `批量指数历史分钟行情.md` |
+| [`index_candlesticks_batch`](#api-index-candlesticks-batch) | 批量指数K线 | `GET` | `api/v2/market/data/index-candlesticks/batch` | `symbols`, `interval_unit`, `adjust_kind`, `since_ts_millis`, `until_ts_millis`, `limit` | `批量指数K线.md` |
| [`sw_index_history_minutes`](#api-sw-index-history-minutes) | 申万指数历史分钟K线 | `GET` | `api/v1/market/data/sw-index/history-minutes` | `index_code`, `start_date`, `end_date`, `page`, `page_size` | `申万指数历史分钟K线.md` |
| SDK 方法 | 接口名称 | HTTP | Path | 参数 | 来源文档 |
@@ -192,6 +194,12 @@
### ETF专题
| [`etf_minutes_batch`](#api-etf-minutes-batch) | 批量ETF历史分钟行情 | `GET` | `api/v2/market/data/etf_minutes/batch` | `symbols`, `interval_value`, `adjust_kind`, `since_ts_millis`, `until_ts_millis`, `limit` | `批量ETF历史分钟行情.md` |
+| [`etf_announcements`](#api-etf-announcements) | ETF公告列表 | `GET` | `api/v2/market/data/announcements/etf-announcements` | `etf_code`, `start_date`, `end_date`, `page`, `page_size` | `ETF公告列表.md` |
+| [`etf_candlesticks_batch`](#api-etf-candlesticks-batch) | 批量ETFK线 | `GET` | `api/v2/market/data/etf-candlesticks/batch` | `symbols`, `interval_unit`, `adjust_kind`, `since_ts_millis`, `until_ts_millis`, `limit` | `批量ETFK线.md` |
+| [`etf_component_details`](#api-etf-component-details) | ETF成分证券明细 | `GET` | `api/v2/market/data/etf-component-details` | `symbol`, `trade_date` | `ETF成分证券明细.md` |
+| [`etf_net_value`](#api-etf-net-value) | ETF净值 | `GET` | `api/v2/market/data/etf-net-value` | `etf_code`, `nav_date`, `start_date`, `end_date`, `page`, `page_size` | `ETF净值.md` |
+| [`etf_pcf_infos`](#api-etf-pcf-infos) | ETF申赎清单 | `GET` | `api/v2/market/data/etf-pcf/etf-pcf-infos` | `symbol`, `trade_date`, `start_date`, `end_date`, `page`, `page_size` | `ETF-PCF信息.md` |
+| [`etf_share`](#api-etf-share) | ETF份额 | `GET` | `api/v2/market/data/etf-share` | `etf_code`, `stati_perd`, `start_date`, `end_date`, `page`, `page_size` | `ETF份额.md` |
| SDK 方法 | 接口名称 | HTTP | Path | 参数 | 来源文档 |
|---|---|---|---|---|---|
@@ -878,7 +886,7 @@ Returns:
- 接口名称:东方财富板块资金流
- HTTP:`GET`
- Path:`api/v1/market/data/eastmoney-sector-flow`
-- 参数:`sector_code`, `sector_type`, `trade_date`, `start_date`, `end_date`, `page`, `page_size`
+- 参数:`board_code`, `board_type`, `board_level`, `trade_date`, `start_date`, `end_date`, `page`, `page_size`
- 来源文档:`东方财富板块资金流.md`
- 原始接口:`get_eastmoney_sector_flow`
@@ -890,11 +898,12 @@ Method: ``GET``.
Documented endpoint: ``get_eastmoney_sector_flow``.
Args:
- sector_code: 板块代码,如 BK0488 (type: string; required: N).
- sector_type: 板块类型:industry / concept / regional (type: string; required: N).
- trade_date: 交易日 YYYYMMDD;与 start_date/end_date 互斥 (type: string; required: N).
- start_date: 区间起始日 YYYYMMDD;需与 end_date 同时提供 (type: string; required: N).
- end_date: 区间结束日 YYYYMMDD;需与 start_date 同时提供 (type: string; required: N).
+ board_code: 板块代码,如 BK0488 (type: string; required: N).
+ board_type: 板块类型:industry / concept / regional (type: string; required: N).
+ board_level: 行业层级:1=一级、2=二级、3=三级;不传返回全部层级,仅匹配 industry (type: integer; required: N).
+ trade_date: 交易日 YYYYMMDD (type: string; required: N).
+ start_date: 区间起始日 YYYYMMDD (type: string; required: N).
+ end_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.
@@ -1285,7 +1294,7 @@ Returns:
- 接口名称:融资融券明细
- HTTP:`GET`
- Path:`api/v1/market/data/margin-trading-details`
-- 参数:`date`, `page`, `page_size`
+- 参数:`date`, `start_date`, `end_date`, `stock`, `page`, `page_size`
- 来源文档:`融资融券明细.md`
- 原始接口:`margin_trading_details`
@@ -1297,7 +1306,10 @@ Method: ``GET``.
Documented endpoint: ``margin_trading_details``.
Args:
- date: 查询日期 YYYYMMDD;不传则使用当前内存快照 (type: string; required: N).
+ date: 查询日期 YYYYMMDD,必须为交易日;不传则使用前一交易日快照;不能与 start_date/end_date 同时使用 (type: string; required: N).
+ start_date: 区间查询开始日期 YYYYMMDD;须与 end_date、stock 同时提供,间隔不能超过 3 年 (type: string; required: N).
+ end_date: 区间查询结束日期 YYYYMMDD;须与 start_date、stock 同时提供 (type: string; required: N).
+ stock: 股票代码过滤条件 (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.
@@ -2690,6 +2702,7 @@ Returns:
- HTTP:`GET`
- Path:`api/v4/market/data/stock-realtime-day-kline`
- 参数:`symbols`
+- 说明:`symbols` 支持 list/tuple(SDK 自动序列化为 JSON 字符串数组)或 JSON 字符串。
- 来源文档:`股票实时日K线.md`
- 原始接口:`stock_realtime_day_kline`
@@ -2737,6 +2750,7 @@ Returns:
- HTTP:`GET`
- Path:`api/v4/market/data/stock-realtime-minute-kline`
- 参数:`symbols`
+- 说明:`symbols` 支持 list/tuple(SDK 自动序列化为 JSON 字符串数组)或 JSON 字符串。
- 来源文档:`股票实时分钟K线.md`
- 原始接口:`stock_realtime_minute_kline`
@@ -3009,7 +3023,7 @@ Documented endpoint: ``ths_stock_daily_flow``.
- 原始接口:`ths_concept_daily_flow`
- Path:`api/v1/market/data/ths-concept-daily-flow`
- Method:`GET`
-- 参数:`start_date`, `end_date`, `sector_name`, `page`, `page_size
+- 参数:`start_date`, `end_date`, `board_name`, `page`, `page_size
Documented endpoint: ``ths_concept_daily_flow``.
@@ -3019,7 +3033,7 @@ Documented endpoint: ``ths_concept_daily_flow``.
- 原始接口:`ths_industry_daily_flow`
- Path:`api/v1/market/data/ths-industry-daily-flow`
- Method:`GET`
-- 参数:`start_date`, `end_date`, `sector_name`, `page`, `page_size
+- 参数:`start_date`, `end_date`, `board_name`, `page`, `page_size
Documented endpoint: ``ths_industry_daily_flow``.
@@ -3405,6 +3419,7 @@ Returns:
- HTTP:`GET`
- Path:`api/v4/market/data/index-realtime-day-kline`
- 参数:`symbols`
+- 说明:`symbols` 支持 list/tuple(SDK 自动序列化为 JSON 字符串数组)或 JSON 字符串。
- 来源文档:`指数实时日K线.md`
- 原始接口:`index_realtime_day_kline`
@@ -3418,6 +3433,7 @@ Returns:
- HTTP:`GET`
- Path:`api/v4/market/data/index-realtime-minute-kline`
- 参数:`symbols`
+- 说明:`symbols` 支持 list/tuple(SDK 自动序列化为 JSON 字符串数组)或 JSON 字符串。
- 来源文档:`指数实时分钟K线.md`
- 原始接口:`index_realtime_minute_kline`
@@ -3835,6 +3851,7 @@ Returns:
- HTTP:`GET`
- Path:`api/v4/market/data/etf-realtime-day-kline`
- 参数:`symbols`
+- 说明:`symbols` 支持 list/tuple(SDK 自动序列化为 JSON 字符串数组)或 JSON 字符串。
- 来源文档:`ETF实时日K线.md`
- 原始接口:`etf_realtime_day_kline`
@@ -3848,6 +3865,7 @@ ETF实时日K线.
- HTTP:`GET`
- Path:`api/v4/market/data/etf-realtime-minute-kline`
- 参数:`symbols`
+- 说明:`symbols` 支持 list/tuple(SDK 自动序列化为 JSON 字符串数组)或 JSON 字符串。
- 来源文档:`ETF实时分钟K线.md`
- 原始接口:`etf_realtime_minute_kline`
@@ -5778,3 +5796,52 @@ Returns:
- Path:`api/v1/market/data/cb/cb-lists`
- 参数:`-`
- 来源文档:`可转债列表.md`
+
+
etf_announcements
+
+- Path:`api/v2/market/data/announcements/etf-announcements`
+- 参数:`etf_code, start_date, end_date, page, page_size`
+- 来源文档:`ETF公告列表.md`
+
+etf_candlesticks_batch
+
+- Path:`api/v2/market/data/etf-candlesticks/batch`
+- 参数:`symbols, interval_unit, adjust_kind, since_ts_millis, until_ts_millis, limit`
+- 来源文档:`批量ETFK线.md`
+
+etf_component_details
+
+- Path:`api/v2/market/data/etf-component-details`
+- 参数:`symbol, trade_date`
+- 来源文档:`ETF成分证券明细.md`
+
+etf_net_value
+
+- Path:`api/v2/market/data/etf-net-value`
+- 参数:`etf_code, nav_date, start_date, end_date, page, page_size`
+- 来源文档:`ETF净值.md`
+
+etf_pcf_infos
+
+- Path:`api/v2/market/data/etf-pcf/etf-pcf-infos`
+- 参数:`symbol, trade_date, start_date, end_date, page, page_size`
+- 来源文档:`ETF-PCF信息.md`
+- 说明:单标的单日查询时服务端返回裸对象,SDK 直接返回 `data` 对象(默认为单行 DataFrame);区间/多行查询仍返回行列表。
+
+etf_share
+
+- Path:`api/v2/market/data/etf-share`
+- 参数:`etf_code, stati_perd, start_date, end_date, page, page_size`
+- 来源文档:`ETF份额.md`
+
+index_candlesticks_batch
+
+- Path:`api/v2/market/data/index-candlesticks/batch`
+- 参数:`symbols, interval_unit, adjust_kind, since_ts_millis, until_ts_millis, limit`
+- 来源文档:`批量指数K线.md`
+
+stock_dividends_effective
+
+- Path:`api/v2/market/data/stock-dividends-effective`
+- 参数:`symbol, since_date, until_date, page, page_size`
+- 来源文档:`股票有效分红记录.md`
diff --git a/pyproject.toml b/pyproject.toml
index 0acad77..f47c979 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "ftshare"
-version = "1.0.4"
+version = "1.0.5"
description = "Python SDK for FTShare market data APIs."
readme = "README.md"
requires-python = ">=3.9"
diff --git a/src/ftshare/apis/etf.py b/src/ftshare/apis/etf.py
index 4b42ec1..e365b68 100644
--- a/src/ftshare/apis/etf.py
+++ b/src/ftshare/apis/etf.py
@@ -6,6 +6,7 @@
from typing import Any
from ..endpoints import ENDPOINTS
+from ..params import symbols_to_json_string
class EtfApiMixin:
@@ -317,14 +318,14 @@ def etf_minutes(self, symbol: Any | None = None, interval_value: Any | None = No
def etf_realtime_minute_kline(self, symbols: Any | None = None, *, raw: bool = False, fields: Sequence[str] | str | None = None, as_dataframe: bool = True, **kwargs: Any) -> Any:
"""ETF实时分钟K线."""
- params = {'symbols': symbols}
+ params = {'symbols': symbols_to_json_string(symbols)}
params.update(kwargs)
return self._call_endpoint('etf_realtime_minute_kline', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
def etf_realtime_day_kline(self, symbols: Any | None = None, *, raw: bool = False, fields: Sequence[str] | str | None = None, as_dataframe: bool = True, **kwargs: Any) -> Any:
"""ETF实时日K线."""
- params = {'symbols': symbols}
+ params = {'symbols': symbols_to_json_string(symbols)}
params.update(kwargs)
return self._call_endpoint('etf_realtime_day_kline', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
@@ -333,3 +334,255 @@ def etf_minutes_batch(self, symbols: Any | None = None, interval_value: Any | No
params = {'symbols': symbols, 'interval_value': interval_value, 'adjust_kind': adjust_kind, 'since_ts_millis': since_ts_millis, 'until_ts_millis': until_ts_millis, 'limit': limit}
params.update(kwargs)
return self._call_endpoint('etf_minutes_batch', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
+
+ def etf_announcements(
+ self,
+ etf_code: Any | None = None,
+ start_date: Any | None = None,
+ end_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:
+ """ETF公告列表.
+
+ Endpoint: ``api/v2/market/data/announcements/etf-announcements``.
+ Method: ``GET``.
+ Documented endpoint: ``etf_announcements``.
+
+ Args:
+ etf_code: ETF 代码(按标的查询时必填),支持裸代码/短后缀/长后缀 (type: string; required: N).
+ start_date: 日期 YYYYMMDD(按日期查询时必填,单日);与 etf_code 二选一 (type: string; required: N).
+ end_date: 日期 YYYYMMDD,不填默认等于 start_date(必须等于 start_date) (type: string; required: N).
+ page: 页码。
+ page_size: 每页条数。
+ 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.
+ **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``, or raw page
+ payloads when multi-page fetching is used with ``raw=True``.
+ """
+ request_params = {'etf_code': etf_code, 'start_date': start_date, 'end_date': end_date}
+ request_params.update(kwargs)
+ path = ENDPOINTS['etf_announcements'].path
+ return self.get_paginated(
+ path,
+ page=page,
+ page_size=page_size,
+ limit=limit,
+ all_pages=all_pages,
+ max_pages=max_pages,
+ raw=raw,
+ fields=fields,
+ as_dataframe=as_dataframe,
+ **request_params,
+ )
+
+ def etf_candlesticks_batch(self, symbols: Any | None = None, interval_unit: Any | None = None, adjust_kind: Any | None = None, since_ts_millis: Any | None = None, until_ts_millis: Any | None = None, limit: Any | None = None, *, raw: bool = False, fields: Sequence[str] | str | None = None, as_dataframe: bool = True, **kwargs: Any) -> Any:
+ """批量ETFK线."""
+ params = {'symbols': symbols, 'interval_unit': interval_unit, 'adjust_kind': adjust_kind, 'since_ts_millis': since_ts_millis, 'until_ts_millis': until_ts_millis, 'limit': limit}
+ params.update(kwargs)
+ return self._call_endpoint('etf_candlesticks_batch', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
+
+ def etf_component_details(self, symbol: Any | None = None, trade_date: Any | None = None, *, raw: bool = False, fields: Sequence[str] | str | None = None, as_dataframe: bool = True, **kwargs: Any) -> Any:
+ """ETF成分证券明细."""
+ params = {'symbol': symbol, 'trade_date': trade_date}
+ params.update(kwargs)
+ return self._call_endpoint('etf_component_details', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
+
+ def etf_net_value(
+ self,
+ etf_code: Any | None = None,
+ nav_date: Any | None = None,
+ start_date: Any | None = None,
+ end_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:
+ """ETF净值.
+
+ Endpoint: ``api/v2/market/data/etf-net-value``.
+ Method: ``GET``.
+ Documented endpoint: ``etf_net_value``.
+
+ Args:
+ etf_code: ETF 代码,如 510300;兼容参数名 fund_code (type: string; required: Y).
+ nav_date: 净值日期 YYYYMMDD;与日期区间参数互斥 (type: integer; required: N).
+ start_date: 净值开始日期 YYYYMMDD;须与 end_date 同时提供 (type: integer; required: N).
+ end_date: 净值结束日期 YYYYMMDD;须与 start_date 同时提供 (type: integer; 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.
+ **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``, or raw page
+ payloads when multi-page fetching is used with ``raw=True``.
+ """
+ request_params = {'etf_code': etf_code, 'nav_date': nav_date, 'start_date': start_date, 'end_date': end_date}
+ request_params.update(kwargs)
+ path = ENDPOINTS['etf_net_value'].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['etf_net_value'].max_page_size,
+ raw=raw,
+ fields=fields,
+ as_dataframe=as_dataframe,
+ **request_params,
+ )
+
+ def etf_pcf_infos(
+ self,
+ symbol: Any | None = None,
+ trade_date: Any | None = None,
+ start_date: Any | None = None,
+ end_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:
+ """ETF申赎清单.
+
+ Endpoint: ``api/v2/market/data/etf-pcf/etf-pcf-infos``.
+ Method: ``GET``.
+ Documented endpoint: ``etf_pcf_infos``.
+
+ Args:
+ symbol: ETF 代码;单标的单日或区间查询时必填,如 510300.SH (type: string; required: N).
+ trade_date: 交易日 YYYYMMDD;单日查询时必填,不能与 start_date/end_date 同时使用 (type: integer; required: N).
+ start_date: 区间开始日期 YYYYMMDD;须与 end_date、symbol 同时提供 (type: integer; required: N).
+ end_date: 区间结束日期 YYYYMMDD;须与 start_date、symbol 同时提供 (type: integer; 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.
+ **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``, or raw page
+ payloads when multi-page fetching is used with ``raw=True``.
+ 单标的单日查询时服务端返回裸对象,SDK 直接返回该 ``data`` 对象
+ (``as_dataframe=True`` 时为单行 DataFrame)。
+ """
+ request_params = {'symbol': symbol, 'trade_date': trade_date, 'start_date': start_date, 'end_date': end_date}
+ request_params.update(kwargs)
+ path = ENDPOINTS['etf_pcf_infos'].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['etf_pcf_infos'].max_page_size,
+ raw=raw,
+ fields=fields,
+ as_dataframe=as_dataframe,
+ unwrap_bare_data=True,
+ **request_params,
+ )
+
+ def etf_share(
+ self,
+ etf_code: Any | None = None,
+ stati_perd: Any | None = None,
+ start_date: Any | None = None,
+ end_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:
+ """ETF份额.
+
+ Endpoint: ``api/v2/market/data/etf-share``.
+ Method: ``GET``.
+ Documented endpoint: ``etf_share``.
+
+ Args:
+ etf_code: ETF 代码,如 510300;兼容参数名 fund_code (type: string; required: Y).
+ stati_perd: 统计周期:日/季度/年度/截止时点/半年/全部,默认全部 (type: string; required: N).
+ start_date: 开始日期 YYYYMMDD,按 trade_date 过滤 (type: integer; required: N).
+ end_date: 结束日期 YYYYMMDD,按 trade_date 过滤 (type: integer; 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.
+ **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``, or raw page
+ payloads when multi-page fetching is used with ``raw=True``.
+ """
+ request_params = {'etf_code': etf_code, 'stati_perd': stati_perd, 'start_date': start_date, 'end_date': end_date}
+ request_params.update(kwargs)
+ path = ENDPOINTS['etf_share'].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['etf_share'].max_page_size,
+ raw=raw,
+ fields=fields,
+ as_dataframe=as_dataframe,
+ **request_params,
+ )
diff --git a/src/ftshare/apis/index.py b/src/ftshare/apis/index.py
index 917168e..2145748 100644
--- a/src/ftshare/apis/index.py
+++ b/src/ftshare/apis/index.py
@@ -6,6 +6,7 @@
from typing import Any
from ..endpoints import ENDPOINTS
+from ..params import symbols_to_json_string
class IndexApiMixin:
@@ -470,14 +471,14 @@ def index_minutes(self, symbol: Any | None = None, interval_value: Any | None =
def index_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}
+ params = {'symbols': symbols_to_json_string(symbols)}
params.update(kwargs)
return self._call_endpoint('index_realtime_minute_kline', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
def index_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线."""
- params = {'symbols': symbols}
+ params = {'symbols': symbols_to_json_string(symbols)}
params.update(kwargs)
return self._call_endpoint('index_realtime_day_kline', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
@@ -492,3 +493,9 @@ def index_minutes_batch(self, symbols: Any | None = None, interval_value: Any |
params = {'symbols': symbols, 'interval_value': interval_value, 'since_ts_millis': since_ts_millis, 'until_ts_millis': until_ts_millis, 'limit': limit}
params.update(kwargs)
return self._call_endpoint('index_minutes_batch', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
+
+ def index_candlesticks_batch(self, symbols: Any | None = None, interval_unit: Any | None = None, adjust_kind: Any | None = None, since_ts_millis: Any | None = None, until_ts_millis: Any | None = None, limit: Any | None = None, *, raw: bool = False, fields: Sequence[str] | str | None = None, as_dataframe: bool = True, **kwargs: Any) -> Any:
+ """批量指数K线."""
+ params = {'symbols': symbols, 'interval_unit': interval_unit, 'adjust_kind': adjust_kind, 'since_ts_millis': since_ts_millis, 'until_ts_millis': until_ts_millis, 'limit': limit}
+ params.update(kwargs)
+ return self._call_endpoint('index_candlesticks_batch', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
diff --git a/src/ftshare/apis/stock.py b/src/ftshare/apis/stock.py
index 2de859e..136a68d 100644
--- a/src/ftshare/apis/stock.py
+++ b/src/ftshare/apis/stock.py
@@ -6,6 +6,7 @@
from typing import Any
from ..endpoints import ENDPOINTS
+from ..params import symbols_to_json_string
def _present_params(params: dict[str, Any]) -> list[str]:
@@ -1414,8 +1415,9 @@ def eastmoney_rank(
def eastmoney_sector_flow(
self,
- sector_code: Any | None = None,
- sector_type: Any | None = None,
+ board_code: Any | None = None,
+ board_type: Any | None = None,
+ board_level: Any | None = None,
trade_date: Any | None = None,
start_date: Any | None = None,
end_date: Any | None = None,
@@ -1437,11 +1439,12 @@ def eastmoney_sector_flow(
Documented endpoint: ``get_eastmoney_sector_flow``.
Args:
- sector_code: 板块代码,如 BK0488 (type: string; required: N).
- sector_type: 板块类型:industry / concept / regional (type: string; required: N).
- trade_date: 交易日 YYYYMMDD;与 start_date/end_date 互斥 (type: string; required: N).
- start_date: 区间起始日 YYYYMMDD;需与 end_date 同时提供 (type: string; required: N).
- end_date: 区间结束日 YYYYMMDD;需与 start_date 同时提供 (type: string; required: N).
+ board_code: 板块代码,如 BK0488 (type: string; required: N).
+ board_type: 板块类型:industry / concept / regional (type: string; required: N).
+ board_level: 行业层级:1=一级、2=二级、3=三级;不传返回全部层级,仅匹配 industry (type: integer; required: N).
+ trade_date: 交易日 YYYYMMDD (type: string; required: N).
+ start_date: 区间起始日 YYYYMMDD (type: string; required: N).
+ end_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.
@@ -1457,7 +1460,7 @@ def eastmoney_sector_flow(
``as_dataframe=False``, raw JSON when ``raw=True``, or raw page
payloads when multi-page fetching is used with ``raw=True``.
"""
- request_params = {'sector_code': sector_code, 'sector_type': sector_type, 'trade_date': trade_date, 'start_date': start_date, 'end_date': end_date}
+ request_params = {'board_code': board_code, 'board_type': board_type, 'board_level': board_level, 'trade_date': trade_date, 'start_date': start_date, 'end_date': end_date}
request_params.update(kwargs)
path = ENDPOINTS['eastmoney_sector_flow'].path
return self.get_paginated(
@@ -2428,6 +2431,9 @@ def limit_event_timeline_3s(
def margin_trading_details(
self,
date: Any | None = None,
+ start_date: Any | None = None,
+ end_date: Any | None = None,
+ stock: Any | None = None,
page: int | None = None,
page_size: int | None = None,
limit: int | None = None,
@@ -2446,7 +2452,10 @@ def margin_trading_details(
Documented endpoint: ``margin_trading_details``.
Args:
- date: 查询日期 YYYYMMDD;不传则使用当前内存快照 (type: string; required: N).
+ date: 查询日期 YYYYMMDD,必须为交易日;不传则使用前一交易日快照;不能与 start_date/end_date 同时使用 (type: string; required: N).
+ start_date: 区间查询开始日期 YYYYMMDD;须与 end_date、stock 同时提供,间隔不能超过 3 年 (type: string; required: N).
+ end_date: 区间查询结束日期 YYYYMMDD;须与 start_date、stock 同时提供 (type: string; required: N).
+ stock: 股票代码过滤条件 (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.
@@ -2462,7 +2471,7 @@ def margin_trading_details(
``as_dataframe=False``, raw JSON when ``raw=True``, or raw page
payloads when multi-page fetching is used with ``raw=True``.
"""
- request_params = {'date': date}
+ request_params = {'date': date, 'start_date': start_date, 'end_date': end_date, 'stock': stock}
request_params.update(kwargs)
path = ENDPOINTS['margin_trading_details'].path
return self.get_paginated(
@@ -3920,6 +3929,14 @@ def stock_dividends(
return self._call_endpoint('stock_dividends', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
+ def stock_dividends_effective(self, symbol: Any | None = None, since_date: Any | None = None, until_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 = {'symbol': symbol, 'since_date': since_date, 'until_date': until_date}
+ params.update(kwargs)
+ path = ENDPOINTS['stock_dividends_effective'].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_history_list(self, trade_date: Any | None = None, code: Any | None = None, page: int | None = None, page_size: int | None = None, *, raw: bool = False, fields: Sequence[str] | str | None = None, as_dataframe: bool = True, **kwargs: Any) -> Any:
"""股票历史列表."""
params = {'trade_date': trade_date, 'code': code, 'page': page, 'page_size': page_size}
@@ -4084,17 +4101,17 @@ def ths_stock_daily_flow(self, start_date: Any | None = None, end_date: Any | No
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_concept_daily_flow(self, start_date: Any | None = None, end_date: Any | None = None, sector_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:
+ def ths_concept_daily_flow(self, start_date: Any | None = None, end_date: Any | None = None, board_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 = {'start_date': start_date, 'end_date': end_date, 'sector_name': sector_name}
+ params = {'start_date': start_date, 'end_date': end_date, 'board_name': board_name}
params.update(kwargs)
path = ENDPOINTS['ths_concept_daily_flow'].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 ths_industry_daily_flow(self, start_date: Any | None = None, end_date: Any | None = None, sector_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:
+ def ths_industry_daily_flow(self, start_date: Any | None = None, end_date: Any | None = None, board_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 = {'start_date': start_date, 'end_date': end_date, 'sector_name': sector_name}
+ params = {'start_date': start_date, 'end_date': end_date, 'board_name': board_name}
params.update(kwargs)
path = ENDPOINTS['ths_industry_daily_flow'].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)
@@ -4110,14 +4127,14 @@ def ths_industry_constituents(self, industry_code: Any | None = None, industry_n
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}
+ params = {'symbols': symbols_to_json_string(symbols)}
params.update(kwargs)
return self._call_endpoint('stock_realtime_minute_kline', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
def stock_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线."""
- params = {'symbols': symbols}
+ params = {'symbols': symbols_to_json_string(symbols)}
params.update(kwargs)
return self._call_endpoint('stock_realtime_day_kline', raw=raw, fields=fields, as_dataframe=as_dataframe, **params)
diff --git a/src/ftshare/base.py b/src/ftshare/base.py
index ed70e23..abc230c 100644
--- a/src/ftshare/base.py
+++ b/src/ftshare/base.py
@@ -68,6 +68,7 @@ def get(
raw: bool = False,
fields: Sequence[str] | str | None = None,
as_dataframe: bool = True,
+ unwrap_bare_data: bool = False,
**params: Any,
) -> Any:
"""Send a GET request and normalize the response.
@@ -82,6 +83,10 @@ def get(
selection is applied after tabular extraction.
as_dataframe: Return a pandas ``DataFrame`` by default. Set to
``False`` to return Python rows such as ``list[dict]``.
+ unwrap_bare_data: When ``True``, an object-shaped ``data`` field
+ is returned directly instead of the full envelope. Intended
+ for single-item query endpoints documented to answer with a
+ bare object.
**params: Query parameters. Values set to ``None`` are omitted.
Returns:
@@ -99,6 +104,7 @@ def get(
raw=raw,
fields=fields,
as_dataframe=as_dataframe,
+ unwrap_bare_data=unwrap_bare_data,
**params,
)
@@ -128,6 +134,7 @@ def _request(
raw: bool = False,
fields: Sequence[str] | str | None = None,
as_dataframe: bool = True,
+ unwrap_bare_data: bool = False,
**params: Any,
) -> Any:
"""Send an HTTP request and normalize the response."""
@@ -159,7 +166,7 @@ def _request(
if raw:
return payload
- result = self._extract_tabular(payload)
+ result = self._extract_tabular(payload, unwrap_bare_data=unwrap_bare_data)
result = self._select_fields(result, fields)
if as_dataframe:
return self._to_dataframe(result)
@@ -195,6 +202,7 @@ def get_paginated(
raw: bool = False,
fields: Sequence[str] | str | None = None,
as_dataframe: bool = True,
+ unwrap_bare_data: bool = False,
**params: Any,
) -> Any:
"""Send a request to an endpoint that supports page/page_size.
@@ -214,6 +222,9 @@ def get_paginated(
list of raw page payloads.
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
+ is returned directly instead of the full envelope. Only used
+ on the single-request path (no ``limit``/``all_pages``).
**params: Query parameters. Values set to ``None`` are omitted.
Returns:
@@ -232,6 +243,7 @@ def get_paginated(
raw=raw,
fields=fields,
as_dataframe=as_dataframe,
+ unwrap_bare_data=unwrap_bare_data,
**params,
)
@@ -404,9 +416,9 @@ def _raise_for_api_error(payload: Any) -> None:
raise_for_api_error(payload)
@classmethod
- def _extract_tabular(cls, payload: Any) -> Any:
+ def _extract_tabular(cls, payload: Any, unwrap_bare_data: bool = False) -> Any:
"""Extract common row containers from FTShare response shapes."""
- return extract_tabular(payload)
+ return extract_tabular(payload, unwrap_bare_data=unwrap_bare_data)
@staticmethod
def _total_pages(payload: Any) -> int | None:
diff --git a/src/ftshare/endpoints/etf.py b/src/ftshare/endpoints/etf.py
index 5466e58..65f57f3 100644
--- a/src/ftshare/endpoints/etf.py
+++ b/src/ftshare/endpoints/etf.py
@@ -88,4 +88,55 @@
'params': ('symbols', 'interval_value', 'adjust_kind', 'since_ts_millis', 'until_ts_millis', 'limit'),
},
+ 'etf_announcements': {
+ 'path': 'api/v2/market/data/announcements/etf-announcements',
+ 'title': 'ETF公告列表',
+ 'doc_file': 'ETF公告列表.md',
+ 'original_api': 'etf_announcements',
+ 'params': ('etf_code', 'start_date', 'end_date', 'page', 'page_size'),
+ },
+
+ 'etf_candlesticks_batch': {
+ 'path': 'api/v2/market/data/etf-candlesticks/batch',
+ 'title': '批量ETFK线',
+ 'doc_file': '批量ETFK线.md',
+ 'original_api': 'etf_candlesticks_batch',
+ 'params': ('symbols', 'interval_unit', 'adjust_kind', 'since_ts_millis', 'until_ts_millis', 'limit'),
+ },
+
+ 'etf_component_details': {
+ 'path': 'api/v2/market/data/etf-component-details',
+ 'title': 'ETF成分证券明细',
+ 'doc_file': 'ETF成分证券明细.md',
+ 'original_api': 'etf_component_details',
+ 'params': ('symbol', 'trade_date'),
+ },
+
+ 'etf_net_value': {
+ 'path': 'api/v2/market/data/etf-net-value',
+ 'title': 'ETF净值',
+ 'doc_file': 'ETF净值.md',
+ 'original_api': 'etf_net_value',
+ 'params': ('etf_code', 'nav_date', 'start_date', 'end_date', 'page', 'page_size'),
+ 'max_page_size': 200,
+ },
+
+ 'etf_pcf_infos': {
+ 'path': 'api/v2/market/data/etf-pcf/etf-pcf-infos',
+ 'title': 'ETF申赎清单',
+ 'doc_file': 'ETF-PCF信息.md',
+ 'original_api': 'etf_pcf_infos',
+ 'params': ('symbol', 'trade_date', 'start_date', 'end_date', 'page', 'page_size'),
+ 'max_page_size': 500,
+ },
+
+ 'etf_share': {
+ 'path': 'api/v2/market/data/etf-share',
+ 'title': 'ETF份额',
+ 'doc_file': 'ETF份额.md',
+ 'original_api': 'etf_share',
+ 'params': ('etf_code', 'stati_perd', 'start_date', 'end_date', 'page', 'page_size'),
+ 'max_page_size': 200,
+ },
+
})
diff --git a/src/ftshare/endpoints/index.py b/src/ftshare/endpoints/index.py
index 6af055b..3b36d0a 100644
--- a/src/ftshare/endpoints/index.py
+++ b/src/ftshare/endpoints/index.py
@@ -112,4 +112,12 @@
'params': ('symbols', 'interval_value', 'since_ts_millis', 'until_ts_millis', 'limit'),
},
+ 'index_candlesticks_batch': {
+ 'path': 'api/v2/market/data/index-candlesticks/batch',
+ 'title': '批量指数K线',
+ 'doc_file': '批量指数K线.md',
+ 'original_api': 'index_candlesticks_batch',
+ 'params': ('symbols', 'interval_unit', 'adjust_kind', 'since_ts_millis', 'until_ts_millis', 'limit'),
+ },
+
})
diff --git a/src/ftshare/endpoints/stock.py b/src/ftshare/endpoints/stock.py
index dbd1fa4..224e5c6 100644
--- a/src/ftshare/endpoints/stock.py
+++ b/src/ftshare/endpoints/stock.py
@@ -124,7 +124,7 @@
'title': '东方财富板块资金流',
'doc_file': '东方财富板块资金流.md',
'original_api': 'get_eastmoney_sector_flow',
- 'params': ('sector_code', 'sector_type', 'trade_date', 'start_date', 'end_date', 'page', 'page_size'),
+ 'params': ('board_code', 'board_type', 'board_level', 'trade_date', 'start_date', 'end_date', 'page', 'page_size'),
},
'eastmoney_stock_flow': {
'path': 'api/v1/market/data/eastmoney-stock-flow',
@@ -201,7 +201,7 @@
'title': '融资融券明细',
'doc_file': '融资融券明细.md',
'original_api': 'margin_trading_details',
- 'params': ('date', 'page', 'page_size'),
+ 'params': ('date', 'start_date', 'end_date', 'stock', 'page', 'page_size'),
},
'northbound': {
'path': 'api/v1/market/data/northbound',
@@ -800,7 +800,7 @@
'title': '同花顺概念板块资金流日度',
'doc_file': '同花顺概念板块资金流日度.md',
'original_api': 'ths_concept_daily_flow',
- 'params': ('start_date', 'end_date', 'sector_name', 'page', 'page_size'),
+ 'params': ('start_date', 'end_date', 'board_name', 'page', 'page_size'),
'max_page_size': 1000,
},
@@ -809,7 +809,7 @@
'title': '同花顺行业板块资金流日度',
'doc_file': '同花顺行业板块资金流日度.md',
'original_api': 'ths_industry_daily_flow',
- 'params': ('start_date', 'end_date', 'sector_name', 'page', 'page_size'),
+ 'params': ('start_date', 'end_date', 'board_name', 'page', 'page_size'),
'max_page_size': 1000,
},
@@ -901,4 +901,12 @@
'params': ('symbols', 'interval_value', 'adjust_kind', 'since_ts_millis', 'until_ts_millis', 'limit'),
},
+ 'stock_dividends_effective': {
+ 'path': 'api/v2/market/data/stock-dividends-effective',
+ 'title': '股票有效分红记录',
+ 'doc_file': '股票有效分红记录.md',
+ 'original_api': 'stock_dividends_effective',
+ 'params': ('symbol', 'since_date', 'until_date', 'page', 'page_size'),
+ },
+
})
diff --git a/src/ftshare/params.py b/src/ftshare/params.py
new file mode 100644
index 0000000..49e64da
--- /dev/null
+++ b/src/ftshare/params.py
@@ -0,0 +1,13 @@
+"""Request parameter normalization helpers."""
+
+from __future__ import annotations
+
+import json
+from typing import Any
+
+
+def symbols_to_json_string(symbols: Any) -> Any:
+ """Serialize list/tuple/set symbols into the JSON string realtime kline endpoints require."""
+ if isinstance(symbols, (list, tuple, set)):
+ return json.dumps(list(symbols))
+ return symbols
diff --git a/src/ftshare/response.py b/src/ftshare/response.py
index e17cead..307c953 100644
--- a/src/ftshare/response.py
+++ b/src/ftshare/response.py
@@ -19,7 +19,7 @@ def raise_for_api_error(payload: Any) -> None:
raise FtshareAPIError(payload.get("code"), str(message) if message is not None else None, payload)
-def extract_tabular(payload: Any) -> Any:
+def extract_tabular(payload: Any, unwrap_bare_data: bool = False) -> Any:
"""Extract common row containers from FTShare response shapes.
Supported envelopes:
@@ -28,16 +28,23 @@ def extract_tabular(payload: Any) -> Any:
- ``{"data": [...]}``
- ``{"items": [...]}``
+ When ``unwrap_bare_data`` is true and ``data`` is an object without
+ ``records``/``items`` rows (single-item query shapes), the ``data``
+ object itself is returned instead of the full envelope.
+
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):
- return data["items"]
+ if isinstance(data, dict):
+ if isinstance(data.get("records"), list):
+ return data["records"]
+ if isinstance(data.get("items"), list):
+ return data["items"]
+ if unwrap_bare_data:
+ return data
if isinstance(payload.get("items"), list):
return payload["items"]
return payload
diff --git a/tests/endpoint_cases.py b/tests/endpoint_cases.py
index 1c71656..c62e86b 100644
--- a/tests/endpoint_cases.py
+++ b/tests/endpoint_cases.py
@@ -12,6 +12,9 @@
"begin_date": "20260101",
"board": "all",
"board_code": "BK1024",
+ "board_level": 2,
+ "board_name": "人工智能",
+ "board_type": "industry",
"cal-type": "1Y",
"candi_date": "20260101",
"category": "economic",
@@ -41,6 +44,7 @@
"end_date": "20260717",
"end_week": "202630",
"end_time": "15:00:00",
+ "etf_code": "510300",
"exchange": "DCE",
"filter": "close > 10",
"fund_code": "000001",
@@ -102,10 +106,7 @@
"scope": "ChinaStock",
"sec_code": "000001",
"security_code": "113000.SZ",
- "sector_name": "人工智能",
"secid": "1.600000",
- "sector_code": "BK1024",
- "sector_type": "concept",
"signal_type": "new_high_60d",
"since": "20260701",
"since_date": "20260701",
@@ -120,6 +121,7 @@
"stati_perd": "日",
"stock_code": "000001.SZ",
"stock_name": "平安银行",
+ "stock": "600000.SH",
"subindustry_name": "动力电池",
"symbol": "600000.SH",
"symbol_code": "110070.SH",
@@ -165,6 +167,12 @@
"etf_minutes": {"symbol": "510300.SH", "interval_value": 1, "adjust_kind": "none", "since_ts_millis": 1784048400000, "until_ts_millis": 1784050200000, "limit": 5},
"etf_realtime_day_kline": {"symbols": '["510300.SH"]'},
"etf_realtime_minute_kline": {"symbols": '["510300.SH"]'},
+ "etf_announcements": {"etf_code": "159915", "start_date": "20260831", "end_date": None, "page": 1, "page_size": 5},
+ "etf_candlesticks_batch": {"symbols": "510300.SH", "interval_unit": "day", "adjust_kind": "forward", "since_ts_millis": 1784048400000, "until_ts_millis": 1784050200000, "limit": 5},
+ "etf_component_details": {"symbol": "510300.SH", "trade_date": None},
+ "etf_net_value": {"etf_code": "510300", "nav_date": 20260909, "start_date": None, "end_date": None, "page": 1, "page_size": 5},
+ "etf_pcf_infos": {"symbol": "510300.SH", "trade_date": "20260909", "start_date": None, "end_date": None, "page": 1, "page_size": 5},
+ "etf_share": {"etf_code": "510300", "stati_perd": "日", "start_date": None, "end_date": None, "page": 1, "page_size": 5},
"ft_limit": {"ts_code": "A2609.DCE", "trade_date": "20260717", "start_date": None, "end_date": None, "cont": "A", "exchange": "DCE", "page": 1, "page_size": 5},
"fund_index_fund": {"index_code": "000300", "scope": "all"},
"fund_asset_allocation": {"fund_code": "000001", "report_date": 20260331, "publish_date": None, "start_date": None, "end_date": None, "page": 1, "page_size": 5},
@@ -181,12 +189,14 @@
"futures_minutes_realtime": {"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", "interval_value": 1, "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},
"index_minutes": {"symbol": "000300.SH", "interval_value": 1, "since_ts_millis": 1784048400000, "until_ts_millis": 1784050200000, "limit": 5},
"index_realtime_day_kline": {"symbols": '["000300.SH"]'},
"index_realtime_minute_kline": {"symbols": '["000300.SH"]'},
"kline_pattern_annotations": {"date": "20260817", "symbol": "000001", "pattern": "双重顶", "page": 1, "page_size": 5},
"limit_list": {"limit_type": "up", "trade_date": "20260717"},
"limit_up_public_report": {"date": "20260724", "security_code": "603976.SH"},
+ "margin_trading_details": {"date": "20260717", "start_date": None, "end_date": None, "stock": None, "page": 1, "page_size": 5},
"major_contract": {"start_date": "20260715", "end_date": "20260717"},
"price_change": {"stock_code": "000001.SZ", "base_date": "20260717", "n": 5, "direction": "forward"},
"semantic_search_news": {"query": "人工智能", "limit": 3, "year": 2026, "start_time": None, "end_time": None},
@@ -202,6 +212,7 @@
"stock_candlesticks": {"symbol": "600519.SH", "interval_unit": "day", "interval_value": 1, "adjust_kind": "forward", "since_ts_millis": 1784048400000, "until_ts_millis": 1784050200000, "limit": 5},
"stock_connect_members": {"direction": "north", "channel": "SH", "page": 1, "page_size": 5},
"stock_dividends": {"symbol": "600519.SH", "since_date": "2026-07-01", "until_date": "2026-07-17", "page": 1, "page_size": 5},
+ "stock_dividends_effective": {"symbol": "600519.SH", "since_date": "2026-07-01", "until_date": "2026-07-17", "page": 1, "page_size": 5},
"stock_ggcg_em": {"symbol": "全部", "page": 1, "page_size": 5},
"stock_ggmx_buy_ranking": {"time_range": "1y", "page": 1, "page_size": 5},
"stock_ggmx_sell_ranking": {"time_range": "1y", "page": 1, "page_size": 5},
diff --git a/tests/test_client.py b/tests/test_client.py
index aadc6d3..560f663 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -231,6 +231,14 @@ def test_requested_endpoint_api_versions():
"sw_index_history_minutes": "api/v1/market/data/sw-index/history-minutes",
"index_minutes_batch": "api/v2/market/data/index_minutes/batch",
"etf_minutes_batch": "api/v2/market/data/etf_minutes/batch",
+ "etf_announcements": "api/v2/market/data/announcements/etf-announcements",
+ "etf_candlesticks_batch": "api/v2/market/data/etf-candlesticks/batch",
+ "etf_component_details": "api/v2/market/data/etf-component-details",
+ "etf_net_value": "api/v2/market/data/etf-net-value",
+ "etf_pcf_infos": "api/v2/market/data/etf-pcf/etf-pcf-infos",
+ "etf_share": "api/v2/market/data/etf-share",
+ "index_candlesticks_batch": "api/v2/market/data/index-candlesticks/batch",
+ "stock_dividends_effective": "api/v2/market/data/stock-dividends-effective",
"eastmoney_dapan_flow": "api/v1/market/data/eastmoney-dapan-flow",
"search": "api/v1/market/security/search/",
"eastmoney_rank": "api/v1/market/data/eastmoney-rank",
@@ -260,6 +268,28 @@ def test_new_batch_endpoints_forward_symbols_and_documented_parameters():
"limit": 5,
},
),
+ (
+ "etf_candlesticks_batch",
+ {
+ "symbols": '["510300.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,
+ },
+ ),
(
"stock_minutes_batch",
{
@@ -410,13 +440,154 @@ def test_new_paginated_endpoints_forward_parameters():
client.stock_signal_latest_snapshot(signal_type="new_high_60d", page=2, page_size=5)
client.ths_stock_daily_flow(start_date="20260805", end_date="20260805", code="600000", page=1, page_size=1000)
- client.ths_concept_daily_flow(start_date="20260805", end_date="20260805", sector_name="机器人概念", page=1, page_size=1000)
- client.ths_industry_daily_flow(start_date="20260805", end_date="20260805", sector_name="证券", page=1, page_size=1000)
+ client.ths_concept_daily_flow(start_date="20260805", end_date="20260805", board_name="机器人概念", page=1, page_size=1000)
+ client.ths_industry_daily_flow(start_date="20260805", end_date="20260805", board_name="证券", page=1, page_size=1000)
assert session.calls[0]["params"] == {"signal_type": "new_high_60d", "page": 2, "page_size": 5}
assert session.calls[1]["params"] == {"start_date": "20260805", "end_date": "20260805", "code": "600000", "page": 1, "page_size": 1000}
- assert session.calls[2]["params"] == {"start_date": "20260805", "end_date": "20260805", "sector_name": "机器人概念", "page": 1, "page_size": 1000}
- assert session.calls[3]["params"] == {"start_date": "20260805", "end_date": "20260805", "sector_name": "证券", "page": 1, "page_size": 1000}
+ assert session.calls[2]["params"] == {"start_date": "20260805", "end_date": "20260805", "board_name": "机器人概念", "page": 1, "page_size": 1000}
+ assert session.calls[3]["params"] == {"start_date": "20260805", "end_date": "20260805", "board_name": "证券", "page": 1, "page_size": 1000}
+
+
+def test_eastmoney_sector_flow_forwards_board_parameters():
+ session = FakeSession([FakeResponse(payload=paginated_records([]))])
+ client = FtshareClient(session=session)
+
+ client.eastmoney_sector_flow(board_code="BK0488", board_type="industry", board_level=2, page=1, page_size=5)
+
+ assert session.calls[0]["url"] == "https://market.ft.tech/gateway/" + ENDPOINTS["eastmoney_sector_flow"].path
+ assert session.calls[0]["params"] == {"board_code": "BK0488", "board_type": "industry", "board_level": 2, "page": 1, "page_size": 5}
+
+
+def test_margin_trading_details_forwards_range_query():
+ session = FakeSession([FakeResponse(payload=paginated_records([{"symbol": "600000.SH"}]))])
+ client = FtshareClient(session=session)
+
+ rows = client.margin_trading_details(start_date="20260601", end_date="20260623", stock="600000.SH", page=1, page_size=5, as_dataframe=False)
+
+ assert rows == [{"symbol": "600000.SH"}]
+ assert session.calls[0]["url"] == "https://market.ft.tech/gateway/" + ENDPOINTS["margin_trading_details"].path
+ assert session.calls[0]["params"] == {"start_date": "20260601", "end_date": "20260623", "stock": "600000.SH", "page": 1, "page_size": 5}
+
+ session = FakeSession([FakeResponse(payload=paginated_records([{"symbol": "000001.SZ"}]))])
+ client = FtshareClient(session=session)
+
+ rows = client.margin_trading_details(date="20260717", as_dataframe=False)
+
+ assert rows == [{"symbol": "000001.SZ"}]
+ assert session.calls[0]["params"] == {"date": "20260717"}
+
+
+def test_new_etf_document_endpoints_forward_documented_parameters():
+ cases = [
+ (
+ "etf_pcf_infos",
+ {"symbol": "510300.SH", "trade_date": "20260909", "page": 1, "page_size": 5},
+ {"symbol": "510300.SH", "trade_date": "20260909", "page": 1, "page_size": 5},
+ ),
+ (
+ "etf_share",
+ {"etf_code": "510300", "stati_perd": "日", "page": 1, "page_size": 5},
+ {"etf_code": "510300", "stati_perd": "日", "page": 1, "page_size": 5},
+ ),
+ (
+ "etf_net_value",
+ {"etf_code": "510300", "nav_date": 20260909, "page": 1, "page_size": 5},
+ {"etf_code": "510300", "nav_date": 20260909, "page": 1, "page_size": 5},
+ ),
+ (
+ "etf_announcements",
+ {"etf_code": "159915", "page": 1, "page_size": 5},
+ {"etf_code": "159915", "page": 1, "page_size": 5},
+ ),
+ (
+ "etf_component_details",
+ {"symbol": "510300.SH", "trade_date": 20260908},
+ {"symbol": "510300.SH", "trade_date": 20260908},
+ ),
+ ]
+
+ for method_name, kwargs, expected_params in cases:
+ session = FakeSession([FakeResponse(payload={"code": 200, "message": "success", "data": {"records": [], "pages": 1}})])
+ client = FtshareClient(session=session)
+
+ getattr(client, method_name)(as_dataframe=False, **kwargs)
+
+ assert session.calls[0]["url"] == "https://market.ft.tech/gateway/" + ENDPOINTS[method_name].path
+ assert session.calls[0]["params"] == expected_params
+
+
+def test_etf_share_and_net_value_reject_page_size_above_200():
+ client = FtshareClient(session=FakeSession([]))
+
+ with pytest.raises(ValueError, match="page_size must be between 1 and 200"):
+ client.etf_share(etf_code="510300", page_size=201)
+ with pytest.raises(ValueError, match="page_size must be between 1 and 200"):
+ client.etf_net_value(etf_code="510300", page_size=201)
+
+
+def test_realtime_kline_methods_serialize_symbols_list_to_json():
+ realtime_payload = {"code": 200, "message": "success", "data": [{"symbol": "600519.SH", "items": [], "total": 0}]}
+ methods = [
+ "stock_realtime_day_kline",
+ "stock_realtime_minute_kline",
+ "etf_realtime_day_kline",
+ "etf_realtime_minute_kline",
+ "index_realtime_day_kline",
+ "index_realtime_minute_kline",
+ ]
+
+ for method_name in methods:
+ session = FakeSession([FakeResponse(payload=realtime_payload)])
+ client = FtshareClient(session=session)
+
+ getattr(client, method_name)(symbols=["600519.SH", "000001.SZ"], as_dataframe=False)
+
+ assert session.calls[0]["params"] == {"symbols": '["600519.SH", "000001.SZ"]'}
+
+ session = FakeSession([FakeResponse(payload=realtime_payload)])
+ client = FtshareClient(session=session)
+
+ client.stock_realtime_day_kline(symbols='["600519.SH"]', as_dataframe=False)
+
+ assert session.calls[0]["params"] == {"symbols": '["600519.SH"]'}
+
+
+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)
+
+ result = client.etf_pcf_infos(symbol="510300.SH", trade_date="20260909", as_dataframe=False)
+
+ assert result == {"symbol": "510300.SH", "trade_date": 20260909, "creation_redemption_unit": 900000}
+
+
+def test_etf_pcf_infos_bare_object_dataframe_is_single_row():
+ session = FakeSession([FakeResponse(payload={"code": 200, "message": "success", "data": {"symbol": "510300.SH", "trade_date": 20260909, "creation_redemption_unit": 900000}})])
+ client = FtshareClient(session=session)
+
+ frame = client.etf_pcf_infos(symbol="510300.SH", trade_date="20260909")
+
+ assert isinstance(frame, pd.DataFrame)
+ assert len(frame) == 1
+ assert frame.loc[0, "symbol"] == "510300.SH"
+ assert frame.loc[0, "creation_redemption_unit"] == 900000
+
+
+def test_etf_pcf_infos_keeps_records_shape_and_envelope_passthrough():
+ session = FakeSession([FakeResponse(payload=paginated_records([{"symbol": "510300.SH"}]))])
+ client = FtshareClient(session=session)
+
+ rows = client.etf_pcf_infos(symbol="510300.SH", start_date="20260901", end_date="20260909", as_dataframe=False)
+
+ assert rows == [{"symbol": "510300.SH"}]
+
+ session = FakeSession([FakeResponse(payload={"code": 200, "message": "success", "data": {"symbol": "510300.SH", "trade_date": 20260909}})])
+ client = FtshareClient(session=session)
+
+ result = client.get(ENDPOINTS["etf_pcf_infos"].path, raw=False, as_dataframe=False)
+
+ assert result == {"code": 200, "message": "success", "data": {"symbol": "510300.SH", "trade_date": 20260909}}
def test_new_flow_endpoints_reject_page_size_above_1000():