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():