From fbcb54d23fff5dc0b54dc1ae582a4b936d68647b Mon Sep 17 00:00:00 2001 From: Lxy Date: Sun, 15 Mar 2026 10:07:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=86=85=E5=A4=96=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=8B=86=E5=88=86=EF=BC=8C=E7=BB=9F=E4=B8=80=E5=B0=81=E8=A3=85?= =?UTF-8?q?sdk=E6=8E=A5=E5=8F=A3=EF=BC=9B=E6=9C=AA=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AMAZINGDATA_ADAPTER_INTERFACES.md | 120 ++++ AMAZINGDATA_ADAPTER_INTERFACES_V2.md | 222 ++++++++ AMAZINGDATA_ADAPTER_INTERFACES_V3.md | 239 ++++++++ GET_HISTORY_STOCK_STATUS.md | 123 ++++ INTERNAL_INTERFACES.md | 148 +++++ .../amazingdata_adapter.cpython-311.pyc | Bin 83612 -> 85938 bytes .../internal_data_service.cpython-311.pyc | Bin 0 -> 22060 bytes app/adapters/amazingdata_adapter.py | 127 +++-- app/adapters/internal_data_service.py | 534 ++++++++++++++++++ scripts/replace_sdk_calls.py | 56 ++ todo_20260311.md | 11 + 11 files changed, 1540 insertions(+), 40 deletions(-) create mode 100644 AMAZINGDATA_ADAPTER_INTERFACES.md create mode 100644 AMAZINGDATA_ADAPTER_INTERFACES_V2.md create mode 100644 AMAZINGDATA_ADAPTER_INTERFACES_V3.md create mode 100644 GET_HISTORY_STOCK_STATUS.md create mode 100644 INTERNAL_INTERFACES.md create mode 100644 app/adapters/__pycache__/internal_data_service.cpython-311.pyc create mode 100644 app/adapters/internal_data_service.py create mode 100644 scripts/replace_sdk_calls.py diff --git a/AMAZINGDATA_ADAPTER_INTERFACES.md b/AMAZINGDATA_ADAPTER_INTERFACES.md new file mode 100644 index 0000000..931c41e --- /dev/null +++ b/AMAZINGDATA_ADAPTER_INTERFACES.md @@ -0,0 +1,120 @@ +# AmazingData Adapter 接口调用关系 + +## 一、核心适配器接口 (DataSourceAdapter 实现) + +| 序号 | 公共接口 | 同步方法 | SDK 调用 | 说明 | +|------|----------|----------|----------|------| +| 1 | `connect` | - | `_market_data.login()` | 连接数据源 | +| 2 | `close` | - | - | 关闭连接 | +| 3 | `subscribe_ticks` | - | - | 订阅实时Tick | +| 4 | `fetch_klines` | `_fetch_klines_sync` | `_market_data.query_kline()` | 获取K线数据 | +| 5 | `fetch_symbols` | `_fetch_symbols_sync` | `_base_data.get_code_list()`
`_base_data.get_code_info()`
`_base_data.get_future_code_list()`
`_base_data.get_future_info()` | 获取标的列表 | +| 6 | `fetch_trading_calendar` | `_fetch_calendar_sync` | `_base_data.get_calendar()` | 获取交易日历 | +| 7 | `health_check` | - | `_market_data.is_login()` | 健康检查 | + +## 二、数据获取接口 (基础数据) + +| 序号 | 公共接口 | 同步方法 | SDK 调用 | 说明 | +|------|----------|----------|----------|------| +| 8 | `get_adj_factor` | - | `_base_data.get_adj_factor()` | 获取复权因子 | +| 9 | `get_backward_factor` | - | `_base_data.get_backward_factor()` | 获取后复权因子 | +| 10 | `get_code_info` | - | `_base_data.get_code_info()` | 获取代码信息 | +| 11 | `get_trading_calendar` | - | `_base_data.get_calendar()` | 获取交易日历(通用) | + +## 三、数据获取接口 (财务/股东数据) + +| 序号 | 公共接口 | 同步方法 | SDK 调用 | 说明 | +|------|----------|----------|----------|------| +| 12 | `get_balance_sheet` | - | `_info_data.get_balance_sheet()` | 资产负债表 | +| 13 | `get_cash_flow` | - | `_info_data.get_cash_flow()` | 现金流量表 | +| 14 | `get_income_statement` | - | `_info_data.get_income_statement()` | 利润表 | +| 15 | `get_profit_express` | - | `_info_data.get_profit_express()` | 业绩预告 | +| 16 | `get_profit_notice` | - | `_info_data.get_profit_notice()` | 业绩快报 | +| 17 | `get_top10_shareholders` | - | `_info_data.get_share_holder()` | 前十大股东 | +| 18 | `get_shareholder_count` | - | `_info_data.get_holder_num()` | 股东户数 | +| 19 | `get_equity_structure` | - | `_info_data.get_equity_structure()` | 股本结构 | + +## 四、数据获取接口 (市场数据) + +| 序号 | 公共接口 | 同步方法 | SDK 调用 | 说明 | +|------|----------|----------|----------|------| +| 20 | `get_snapshot` | - | `_market_data.query_kline()` | 获取快照数据 | +| 21 | `get_index_constituents` | - | `_info_data.get_index_constituent()` | 指数成分股 | +| 22 | `get_index_weights` | - | `_info_data.get_index_weight()` | 指数权重 | +| 23 | `get_margin_summary` | - | `_info_data.get_margin_summary()` | 融资融券汇总 | +| 24 | `get_margin_detail` | - | `_info_data.get_margin_detail()` | 融资融券明细 | +| 25 | `get_longhu_bang` | - | `_info_data.get_long_hu_bang()` | 龙虎榜 | +| 26 | `get_block_trading` | - | `_info_data.get_block_trading()` | 大宗交易 | + +## 五、数据获取接口 (基金/可转债) + +| 序号 | 公共接口 | 同步方法 | SDK 调用 | 说明 | +|------|----------|----------|----------|------| +| 27 | `get_etf_pcf` | - | `_base_data.get_etf_pcf()` | ETF申购赎回清单 | +| 28 | `get_fund_share` | - | `_info_data.get_fund_share()` | 基金份额 | +| 29 | `get_kzz_issuance` | - | `_info_data.get_kzz_issuance()` | 可转债发行 | + +## 六、新增分表数据接口 + +| 序号 | 公共接口 | 同步方法 | SDK 调用 | 说明 | +|------|----------|----------|----------|------| +| 30 | `fetch_kline_base` | `_fetch_kline_base_sync` | `_market_data.query_kline()` | 获取基础K线数据 | +| 31 | `fetch_kline_quote` | `_fetch_kline_quote_sync` | `_market_data.query_kline()`
`_base_data.get_code_info()` | 获取行情指标数据 | +| 32 | `fetch_kline_finance` | `_fetch_kline_finance_sync` | `_market_data.query_kline()`
`_info_data.get_equity_structure()` | 获取财务数据 | +| 33 | `fetch_stock_basic_info` | `_fetch_stock_basic_info_sync` | `_base_data.get_code_list()`
`_base_data.get_code_info()`
`_info_data.get_equity_structure()` | 获取股票基础信息 | + +## 七、组合调用详细说明 + +### 7.1 多次调用同一接口的方法 + +| 方法 | SDK 调用次数 | 调用详情 | +|------|-------------|----------| +| `fetch_symbols` (stock) | 2次 | 1. `get_code_list()`
2. `get_code_info()` | +| `fetch_symbols` (futures) | 2次 | 1. `get_future_code_list()`
2. `get_future_info()` | +| `fetch_kline_quote` | 2次 | 1. `query_kline()` - 获取K线
2. `get_code_info()` - 获取涨跌停价 | +| `fetch_kline_finance` | 2次 | 1. `get_equity_structure()` - 获取股本
2. `query_kline()` - 获取价格计算市值 | +| `fetch_stock_basic_info` | 3次 | 1. `get_code_list()` - 获取代码列表
2. `get_code_info()` - 获取名称
3. `get_equity_structure()` - 获取上市日期 | + +### 7.2 调用链示例 + +``` +fetch_stock_basic_info (async) +└── _fetch_stock_basic_info_sync (sync) + ├── _base_data.get_code_list() [第1次SDK调用] + ├── _base_data.get_code_info() [第2次SDK调用] + └── _info_data.get_equity_structure() [第3次SDK调用, 循环每个股票] +``` + +``` +fetch_kline_quote (async) +└── _fetch_kline_quote_sync (sync) + ├── _market_data.query_kline() [第1次SDK调用, 扩展日期范围] + └── _base_data.get_code_info() [第2次SDK调用, 获取涨跌停价] +``` + +``` +fetch_kline_finance (async) +└── _fetch_kline_finance_sync (sync) + ├── _info_data.get_equity_structure() [第1次SDK调用] + └── _market_data.query_kline() [第2次SDK调用] +``` + +## 八、SDK 对象汇总 + +| SDK 对象 | 说明 | 主要方法 | +|----------|------|----------| +| `_market_data` | 市场数据 | `login()`, `query_kline()`, `is_login()` | +| `_base_data` | 基础数据 | `get_code_list()`, `get_code_info()`, `get_calendar()`, `get_adj_factor()`, `get_future_code_list()`, `get_etf_pcf()` | +| `_info_data` | 信息数据 | `get_equity_structure()`, `get_share_holder()`, `get_income_statement()`, `get_balance_sheet()`, `get_cash_flow()`, `get_profit_express()`, `get_profit_notice()`, `get_holder_num()`, `get_margin_summary()`, `get_margin_detail()`, `get_long_hu_bang()`, `get_block_trading()`, `get_index_constituent()`, `get_index_weight()`, `get_fund_share()`, `get_kzz_issuance()` | + +## 九、接口统计 + +| 类别 | 接口数量 | +|------|----------| +| 核心适配器接口 | 7 | +| 基础数据接口 | 4 | +| 财务/股东数据接口 | 8 | +| 市场数据接口 | 7 | +| 基金/可转债接口 | 3 | +| 新增分表数据接口 | 4 | +| **总计** | **33** | diff --git a/AMAZINGDATA_ADAPTER_INTERFACES_V2.md b/AMAZINGDATA_ADAPTER_INTERFACES_V2.md new file mode 100644 index 0000000..80324b8 --- /dev/null +++ b/AMAZINGDATA_ADAPTER_INTERFACES_V2.md @@ -0,0 +1,222 @@ +# AmazingData Adapter 接口调用关系 (完整版) + +## 一、核心适配器接口 + +### 1.1 fetch_klines / _fetch_klines_sync (最复杂) + +``` +fetch_klines (async) +└── _fetch_klines_sync (sync) + ├── _market_data.query_kline() [第1次SDK调用] + ├── _base_data.get_code_info() [第2次SDK调用 - 涨跌停价] + ├── _info_data.get_equity_structure() [第3次SDK调用 - 股本结构] + ├── _base_data.get_calendar() [第4次SDK调用 - 交易日历] + └── _get_list_date() [内部方法] + ├── _base_data.get_code_info() [可能第5次SDK调用] + └── _base_data.get_hist_code_list() [可能第6次SDK调用] +``` + +| 调用层次 | 方法 | SDK调用次数 | 说明 | +|---------|------|------------|------| +| 外层 | `fetch_klines` | 1 | async入口 | +| 内层 | `_fetch_klines_sync` | 4-6次 | 包含K线+扩展字段获取 | + +**总计 SDK 调用**: 4-6 次(取决于 `_get_list_date` 的执行路径) + +--- + +### 1.2 其他核心接口 + +| 序号 | 公共接口 | 同步方法 | SDK 调用 | 次数 | 说明 | +|------|----------|----------|----------|------|------| +| 1 | `connect` | - | `_market_data.login()` | 1 | 连接数据源 | +| 2 | `close` | - | - | 0 | 关闭连接 | +| 3 | `subscribe_ticks` | - | - | 0 | 订阅实时Tick | +| 4 | `fetch_symbols` | `_fetch_symbols_sync` | `_base_data.get_code_list()`
`_base_data.get_code_info()` | 2 | 股票标的列表 | +| 4b| `fetch_symbols` | `_fetch_symbols_sync` | `_base_data.get_future_code_list()`
`_base_data.get_future_info()` | 2 | 期货标的列表 | +| 5 | `fetch_trading_calendar` | `_fetch_calendar_sync` | `_base_data.get_calendar()` | 1 | 交易日历 | +| 6 | `health_check` | - | `_market_data.is_login()` | 1 | 健康检查 | + +--- + +## 二、新增分表数据接口 (详细调用链) + +### 2.1 fetch_kline_base / _fetch_kline_base_sync + +``` +fetch_kline_base (async) +└── _fetch_kline_base_sync (sync) + └── _market_data.query_kline() [第1次SDK调用] +``` + +| 调用层次 | 方法 | SDK调用次数 | 说明 | +|---------|------|------------|------| +| 外层 | `fetch_kline_base` | 1 | async入口 | +| 内层 | `_fetch_kline_base_sync` | 1 | 仅基础K线 | + +**总计 SDK 调用**: 1 次 + +--- + +### 2.2 fetch_kline_quote / _fetch_kline_quote_sync + +``` +fetch_kline_quote (async) +└── _fetch_kline_quote_sync (sync) + ├── _market_data.query_kline() [第1次SDK调用 - 扩展日期范围] + └── _base_data.get_code_info() [第2次SDK调用 - 涨跌停价] +``` + +| 调用层次 | 方法 | SDK调用次数 | 说明 | +|---------|------|------------|------| +| 外层 | `fetch_kline_quote` | 2 | async入口 | +| 内层 | `_fetch_kline_quote_sync` | 2 | K线+涨跌停价 | + +**总计 SDK 调用**: 2 次 + +--- + +### 2.3 fetch_kline_finance / _fetch_kline_finance_sync + +``` +fetch_kline_finance (async) +└── _fetch_kline_finance_sync (sync) + ├── _info_data.get_equity_structure() [第1次SDK调用 - 股本结构] + └── _market_data.query_kline() [第2次SDK调用 - 价格数据] +``` + +| 调用层次 | 方法 | SDK调用次数 | 说明 | +|---------|------|------------|------| +| 外层 | `fetch_kline_finance` | 2 | async入口 | +| 内层 | `_fetch_kline_finance_sync` | 2 | 股本+价格计算市值 | + +**总计 SDK 调用**: 2 次 + +--- + +### 2.4 fetch_stock_basic_info / _fetch_stock_basic_info_sync + +``` +fetch_stock_basic_info (async) +└── _fetch_stock_basic_info_sync (sync) + ├── _base_data.get_code_list() [第1次SDK调用 - 代码列表] + ├── _base_data.get_code_info() [第2次SDK调用 - 名称/交易所] + └── 循环每个股票: + └── _info_data.get_equity_structure() [第3次SDK调用 - 上市日期] +``` + +| 调用层次 | 方法 | SDK调用次数 | 说明 | +|---------|------|------------|------| +| 外层 | `fetch_stock_basic_info` | 2+N | async入口,N为股票数量 | +| 内层 | `_fetch_stock_basic_info_sync` | 2+N | 基础信息+上市日期 | + +**总计 SDK 调用**: 2 + N 次(N = 股票数量,每个股票调用一次 get_equity_structure) + +--- + +## 三、财务/股东数据接口 + +| 序号 | 公共接口 | SDK 调用 | 次数 | 说明 | +|------|----------|----------|------|------| +| 7 | `get_balance_sheet` | `_info_data.get_balance_sheet()` | 1 | 资产负债表 | +| 8 | `get_cash_flow` | `_info_data.get_cash_flow()` | 1 | 现金流量表 | +| 9 | `get_income_statement` | `_info_data.get_income_statement()` | 1 | 利润表 | +| 10 | `get_profit_express` | `_info_data.get_profit_express()` | 1 | 业绩预告 | +| 11 | `get_profit_notice` | `_info_data.get_profit_notice()` | 1 | 业绩快报 | +| 12 | `get_top10_shareholders` | `_info_data.get_share_holder()` | 1 | 前十大股东 | +| 13 | `get_shareholder_count` | `_info_data.get_holder_num()` | 1 | 股东户数 | +| 14 | `get_equity_structure` | `_info_data.get_equity_structure()` | 1 | 股本结构 | + +--- + +## 四、市场数据接口 + +| 序号 | 公共接口 | SDK 调用 | 次数 | 说明 | +|------|----------|----------|------|------| +| 15 | `get_snapshot` | `_market_data.query_kline()` | 1 | 快照数据 | +| 16 | `get_adj_factor` | `_base_data.get_adj_factor()` | 1 | 复权因子 | +| 17 | `get_backward_factor` | `_base_data.get_backward_factor()` | 1 | 后复权因子 | +| 18 | `get_index_constituents` | `_info_data.get_index_constituent()` | 1 | 指数成分股 | +| 19 | `get_index_weights` | `_info_data.get_index_weight()` | 1 | 指数权重 | +| 20 | `get_margin_summary` | `_info_data.get_margin_summary()` | 1 | 融资融券汇总 | +| 21 | `get_margin_detail` | `_info_data.get_margin_detail()` | 1 | 融资融券明细 | +| 22 | `get_longhu_bang` | `_info_data.get_long_hu_bang()` | 1 | 龙虎榜 | +| 23 | `get_block_trading` | `_info_data.get_block_trading()` | 1 | 大宗交易 | + +--- + +## 五、基金/可转债接口 + +| 序号 | 公共接口 | SDK 调用 | 次数 | 说明 | +|------|----------|----------|------|------| +| 24 | `get_etf_pcf` | `_base_data.get_etf_pcf()` | 1 | ETF申购赎回清单 | +| 25 | `get_fund_share` | `_info_data.get_fund_share()` | 1 | 基金份额 | +| 26 | `get_kzz_issuance` | `_info_data.get_kzz_issuance()` | 1 | 可转债发行 | + +--- + +## 六、接口调用次数汇总表 + +| 接口方法 | SDK 调用次数 | 调用的 SDK 方法 | +|----------|-------------|----------------| +| `fetch_klines` | **4-6次** | `query_kline` + `get_code_info` (1-2次) + `get_equity_structure` + `get_calendar` + `get_hist_code_list` (可选) | +| `fetch_symbols` (stock) | **2次** | `get_code_list` + `get_code_info` | +| `fetch_symbols` (futures) | **2次** | `get_future_code_list` + `get_future_info` | +| `fetch_trading_calendar` | **1次** | `get_calendar` | +| `fetch_kline_base` | **1次** | `query_kline` | +| `fetch_kline_quote` | **2次** | `query_kline` + `get_code_info` | +| `fetch_kline_finance` | **2次** | `get_equity_structure` + `query_kline` | +| `fetch_stock_basic_info` | **2+N次** | `get_code_list` + `get_code_info` + N×`get_equity_structure` | +| 其他单个数据接口 | **1次** | 对应单个 SDK 方法 | + +--- + +## 七、SDK 对象与方法汇总 + +### _market_data (市场数据) +| 方法 | 调用次数 | 使用场景 | +|------|---------|----------| +| `login()` | 1 | connect | +| `query_kline()` | 多次 | K线数据获取 | +| `is_login()` | 1 | health_check | + +### _base_data (基础数据) +| 方法 | 调用次数 | 使用场景 | +|------|---------|----------| +| `get_code_list()` | 多次 | 股票代码列表 | +| `get_code_info()` | 最频繁 | 代码信息、涨跌停价、上市日期 | +| `get_calendar()` | 多次 | 交易日历 | +| `get_adj_factor()` | 按需 | 复权因子 | +| `get_future_code_list()` | 按需 | 期货代码列表 | +| `get_etf_pcf()` | 按需 | ETF数据 | +| `get_hist_code_list()` | 可选 | 历史代码列表(备选) | + +### _info_data (信息数据) +| 方法 | 调用次数 | 使用场景 | +|------|---------|----------| +| `get_equity_structure()` | 最频繁 | 股本结构、上市日期推断 | +| `get_income_statement()` | 按需 | 利润表 | +| `get_balance_sheet()` | 按需 | 资产负债表 | +| `get_cash_flow()` | 按需 | 现金流量表 | +| `get_share_holder()` | 按需 | 股东数据 | +| `get_margin_summary/detail()` | 按需 | 融资融券 | +| `get_index_constituent/weight()` | 按需 | 指数数据 | + +--- + +## 八、接口统计 + +| 类别 | 接口数量 | 说明 | +|------|----------|------| +| 核心适配器接口 | 6 | connect/close/subscribe_ticks/fetch_klines/fetch_symbols/fetch_trading_calendar/health_check | +| 财务/股东数据接口 | 8 | 各类财务报表和股东数据 | +| 市场数据接口 | 9 | K线、指数、融资融券等 | +| 基金/可转债接口 | 3 | ETF、基金、可转债 | +| 新增分表数据接口 | 4 | 拆分表专用接口 | +| **总计** | **30** | 公共接口方法 | + +**调用复杂度分级**: +- ⭐ 简单 (1次SDK调用): fetch_kline_base, get_balance_sheet, get_cash_flow 等 +- ⭐⭐ 中等 (2次SDK调用): fetch_symbols, fetch_kline_quote, fetch_kline_finance 等 +- ⭐⭐⭐ 复杂 (4-6次SDK调用): **fetch_klines** (原K线获取接口) +- ⭐⭐⭐⭐ 最复杂 (2+N次SDK调用): **fetch_stock_basic_info** (N=股票数量) diff --git a/AMAZINGDATA_ADAPTER_INTERFACES_V3.md b/AMAZINGDATA_ADAPTER_INTERFACES_V3.md new file mode 100644 index 0000000..1d36b7e --- /dev/null +++ b/AMAZINGDATA_ADAPTER_INTERFACES_V3.md @@ -0,0 +1,239 @@ +# AmazingData Adapter 接口调用关系 (完整修正版) + +## 一、接口调用次数汇总表 + +| 序号 | 接口方法 | SDK 调用次数 | 调用的 SDK 方法 | 复杂度 | +|------|----------|-------------|----------------|--------| +| 1 | `fetch_klines` | **4-6次** | `query_kline` + `get_code_info` + `get_equity_structure` + `get_calendar` + `get_list_date`(内部) | ⭐⭐⭐⭐ | +| 2 | `fetch_stock_basic_info` | **2+N次** | `get_code_list` + `get_code_info` + N×`get_equity_structure` | ⭐⭐⭐⭐⭐ | +| 3 | `fetch_symbols` (stock) | **2次** | `get_code_list` + `get_code_info` | ⭐⭐ | +| 4 | `fetch_symbols` (futures) | **1次** | `get_future_code_list` | ⭐ | +| 5 | `fetch_trading_calendar` | **1次** | `get_calendar` | ⭐ | +| 6 | `fetch_kline_quote` | **2次** | `query_kline` + `get_code_info` | ⭐⭐ | +| 7 | `fetch_kline_finance` | **2次** | `get_equity_structure` + `query_kline` | ⭐⭐ | +| 8 | `fetch_kline_base` | **1次** | `query_kline` | ⭐ | +| 9 | 其他所有数据接口 | **1次** | 对应单个 SDK 方法 | ⭐ | + +--- + +## 二、复杂接口详细调用链 + +### 2.1 fetch_klines (4-6次SDK调用) + +```python +fetch_klines (async) +└── _fetch_klines_sync (sync) + ├── _market_data.query_kline() # [第1次] 获取K线数据 + ├── _base_data.get_code_info() # [第2次] 获取涨跌停价 + ├── _info_data.get_equity_structure() # [第3次] 获取股本结构 + ├── _base_data.get_calendar() # [第4次] 获取交易日历 + └── _get_list_date() # [内部方法] + ├── _base_data.get_code_info() # [第5次,可选] 尝试获取上市日期 + └── _base_data.get_hist_code_list() # [第6次,可选] 备选方案 +``` + +**实际调用**: 最少4次,最多6次(取决于 `_get_list_date` 的执行路径) + +--- + +### 2.2 fetch_stock_basic_info (2+N次SDK调用) + +```python +fetch_stock_basic_info (async) +└── _fetch_stock_basic_info_sync (sync) + ├── _base_data.get_code_list() # [第1次] 获取所有股票代码 + ├── _base_data.get_code_info() # [第2次] 获取股票名称 + └── for code in codes: # [循环N次,N=股票数量] + └── _info_data.get_equity_structure() # [第3~2+N次] 每个股票调用一次! +``` + +**⚠️ 警告**: 如果获取全市场5000+只股票,将触发 2+5000 = **5002次** SDK 调用! + +**建议**: 使用 `codes` 参数限制股票数量,避免全量查询。 + +--- + +### 2.3 fetch_symbols (股票 - 2次SDK调用) + +```python +fetch_symbols (async) +└── _fetch_symbols_sync (sync) + ├── _base_data.get_code_list() # [第1次] 获取代码列表 + └── _base_data.get_code_info() # [第2次] 获取代码信息(名称) +``` + +--- + +### 2.4 fetch_symbols (期货 - 1次SDK调用) + +```python +fetch_symbols (async) +└── _fetch_symbols_sync (sync) + └── _base_data.get_future_code_list() # [第1次] 获取期货代码 + # 注意: 期货没有调用 get_future_info,交易所通过代码解析 +``` + +**修正**: 之前文档错误地写为2次调用,实际只有1次。 + +--- + +## 三、新增分表接口调用链 + +### 3.1 fetch_kline_base (1次SDK调用) + +```python +fetch_kline_base (async) +└── _fetch_kline_base_sync (sync) + └── _market_data.query_kline() # [第1次] 仅基础K线 +``` + +--- + +### 3.2 fetch_kline_quote (2次SDK调用) + +```python +fetch_kline_quote (async) +└── _fetch_kline_quote_sync (sync) + ├── _market_data.query_kline() # [第1次] 扩展日期范围 + └── _base_data.get_code_info() # [第2次] 涨跌停价 +``` + +--- + +### 3.3 fetch_kline_finance (2次SDK调用) + +```python +fetch_kline_finance (async) +└── _fetch_kline_finance_sync (sync) + ├── _info_data.get_equity_structure() # [第1次] 股本结构 + └── _market_data.query_kline() # [第2次] 价格数据 +``` + +--- + +## 四、单次SDK调用的接口 + +以下接口都只进行 **1次** SDK 调用: + +| 接口 | SDK 方法 | +|------|----------| +| `connect` | `_market_data.login()` | +| `health_check` | `_market_data.is_login()` | +| `fetch_trading_calendar` | `_base_data.get_calendar()` | +| `get_adj_factor` | `_base_data.get_adj_factor()` | +| `get_backward_factor` | `_base_data.get_backward_factor()` | +| `get_snapshot` | `_market_data.query_snapshot()` | +| `get_balance_sheet` | `_info_data.get_balance_sheet()` | +| `get_cash_flow` | `_info_data.get_cash_flow()` | +| `get_income_statement` | `_info_data.get_income()` | +| `get_profit_express` | `_info_data.get_profit_express()` | +| `get_profit_notice` | `_info_data.get_profit_notice()` | +| `get_top10_shareholders` | `_info_data.get_share_holder()` | +| `get_shareholder_count` | `_info_data.get_holder_num()` | +| `get_equity_structure` | `_info_data.get_equity_structure()` | +| `get_index_constituents` | `_info_data.get_index_constituent()` | +| `get_index_weights` | `_info_data.get_index_weight()` | +| `get_margin_summary` | `_info_data.get_margin_summary()` | +| `get_margin_detail` | `_info_data.get_margin_detail()` | +| `get_longhu_bang` | `_info_data.get_long_hu_bang()` | +| `get_block_trading` | `_info_data.get_block_trading()` | +| `get_etf_pcf` | `_base_data.get_etf_pcf()` | +| `get_fund_share` | `_info_data.get_fund_share()` | +| `get_kzz_issuance` | `_info_data.get_kzz_issuance()` | +| `get_history_stock_status` | `_info_data.get_history_stock_status()` | +| `get_code_info` | `_base_data.get_code_info()` | +| `get_trading_calendar` | `_base_data.get_calendar()` | + +--- + +## 五、SDK 对象方法汇总 + +### _market_data (市场数据) +| 方法 | 使用场景 | 调用次数 | +|------|----------|----------| +| `login()` | connect | 1 | +| `query_kline()` | K线数据 | 频繁 | +| `query_snapshot()` | get_snapshot | 按需 | +| `is_login()` | health_check | 1 | + +### _base_data (基础数据) +| 方法 | 使用场景 | 调用次数 | +|------|----------|----------| +| `get_code_list()` | fetch_symbols(stock) | 频繁 | +| `get_future_code_list()` | fetch_symbols(futures) | 按需 | +| `get_code_info()` | 多个接口 | **最频繁** | +| `get_calendar()` | 交易日历 | 多次 | +| `get_adj_factor()` | 复权因子 | 按需 | +| `get_backward_factor()` | 后复权因子 | 按需 | +| `get_etf_pcf()` | ETF数据 | 按需 | +| `get_hist_code_list()` | 备选上市日期 | 可选 | + +### _info_data (信息数据) +| 方法 | 使用场景 | 调用次数 | +|------|----------|----------| +| `get_equity_structure()` | 股本/上市日期 | **最频繁** | +| `get_share_holder()` | 股东数据 | 按需 | +| `get_income()` | 利润表 | 按需 | +| `get_balance_sheet()` | 资产负债表 | 按需 | +| `get_cash_flow()` | 现金流量表 | 按需 | +| `get_profit_express()` | 业绩预告 | 按需 | +| `get_profit_notice()` | 业绩快报 | 按需 | +| `get_holder_num()` | 股东户数 | 按需 | +| `get_margin_summary()` | 融资融券汇总 | 按需 | +| `get_margin_detail()` | 融资融券明细 | 按需 | +| `get_long_hu_bang()` | 龙虎榜 | 按需 | +| `get_block_trading()` | 大宗交易 | 按需 | +| `get_index_constituent()` | 指数成分股 | 按需 | +| `get_index_weight()` | 指数权重 | 按需 | +| `get_fund_share()` | 基金份额 | 按需 | +| `get_kzz_issuance()` | 可转债发行 | 按需 | +| `get_history_stock_status()` | 历史股票状态(涨停/跌停/ST/停牌) | 按需 | + +--- + +## 六、修正说明 + +### 6.1 与V2版文档的差异 + +| 接口 | V2版文档 | V3版文档(修正) | 差异说明 | +|------|----------|----------------|----------| +| `fetch_symbols` (futures) | 2次调用 | **1次调用** | 期货实际只调用`get_future_code_list`,没有调用`get_future_info` | +| `fetch_stock_basic_info` | 2+N次 | **2+N次** | 确认正确,N=股票数量 | +| `fetch_klines` | 4-6次 | **4-6次** | 确认正确 | + +### 6.2 风险提示 + +**⚠️ 高危接口**: `fetch_stock_basic_info` +- 获取全市场股票时会产生 **5000+** 次 SDK 调用 +- 可能导致性能问题或触发限流 +- **建议**: 始终使用 `codes` 参数限制查询范围 + +```python +# ❌ 不推荐: 获取全市场 +all_stocks = await adapter.fetch_stock_basic_info() + +# ✅ 推荐: 只查询指定股票 +specific_stocks = await adapter.fetch_stock_basic_info( + codes=["000001.SZ", "600519.SH"] +) +``` + +--- + +## 七、接口统计 + +| 类别 | 接口数量 | +|------|----------| +| 核心适配器接口 | 6 | +| 基础数据接口 | 4 | +| 财务/股东数据接口 | 8 | +| 市场数据接口 | 9 | +| 基金/可转债接口 | 3 | +| 新增分表数据接口 | 4 | +| **总计** | **34** | + +**复杂度分级**: +- ⭐ 简单 (1次): 26个接口 +- ⭐⭐ 中等 (2次): 3个接口 (`fetch_symbols` stock, `fetch_kline_quote`, `fetch_kline_finance`) +- ⭐⭐⭐⭐ 复杂 (4-6次): 1个接口 (`fetch_klines`) +- ⭐⭐⭐⭐⭐ 极复杂 (2+N次): 1个接口 (`fetch_stock_basic_info`) diff --git a/GET_HISTORY_STOCK_STATUS.md b/GET_HISTORY_STOCK_STATUS.md new file mode 100644 index 0000000..666de5d --- /dev/null +++ b/GET_HISTORY_STOCK_STATUS.md @@ -0,0 +1,123 @@ +# get_history_stock_status 接口实现 + +## 接口说明 + +`get_history_stock_status` 是 AmazingData SDK 提供的历史股票状态查询接口,用于获取股票的历史涨停/跌停价、ST状态、停牌状态等信息。 + +## 实现位置 + +### 1. 内部接口层 +**文件**: `app/adapters/internal_data_service.py` + +```python +class _InfoDataInternal: + def get_history_stock_status( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取历史股票状态数据 + + 包含字段: + - TRADE_DATE: 交易日期 + - UP_LIMIT: 涨停价 + - DOWN_LIMIT: 跌停价 + - MAX_UP_DOWN: 最大涨跌幅限制 + - IS_ST: 是否ST + - IS_SUSPEND: 是否停牌 + """ +``` + +### 2. 对外接口层 +**文件**: `app/adapters/amazingdata_adapter.py` + +```python +class AmazingDataAdapter(DataSourceAdapter): + async def get_history_stock_status( + self, + codes: List[str], + start_date: Optional[str] = None, + end_date: Optional[str] = None, + is_local: Optional[bool] = None + ) -> Dict[str, pd.DataFrame]: + """获取历史股票状态数据""" +``` + +## 返回字段说明 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| TRADE_DATE | int | 交易日期 (YYYYMMDD) | +| UP_LIMIT | float | 涨停价 | +| DOWN_LIMIT | float | 跌停价 | +| MAX_UP_DOWN | float | 最大涨跌幅限制(%) | +| IS_ST | bool/int | 是否ST股 | +| IS_SUSPEND | bool/int | 是否停牌 | + +## 使用示例 + +```python +from app.adapters.amazingdata_adapter import AmazingDataAdapter + +adapter = AmazingDataAdapter() +await adapter.connect({ + "username": "your_username", + "password": "your_password", + "host": "your_host", + "port": 8600 +}) + +# 获取股票历史状态 +result = await adapter.get_history_stock_status( + codes=["000001.SZ", "600519.SH"], + start_date="20240301", + end_date="20240310" +) + +# 处理结果 +for symbol, df in result.items(): + print(f"股票: {symbol}") + print(df[["TRADE_DATE", "UP_LIMIT", "DOWN_LIMIT", "IS_ST"]]) +``` + +## 调用链 + +``` +AmazingDataAdapter.get_history_stock_status (async) +└── _InfoDataInternal.get_history_stock_status (sync) + └── SDK: _info_data.get_history_stock_status() +``` + +## SDK 调用统计 + +| 层次 | 调用次数 | 说明 | +|------|----------|------| +| 对外接口 | 1 | async 包装 | +| 内部接口 | 1 | 错误处理 | +| SDK | 1 | 实际数据获取 | + +**总计**: 1 次 SDK 调用(单次查询) + +## 应用场景 + +1. **涨跌停分析** - 获取历史涨停/跌停价格 +2. **ST股票跟踪** - 监控股票ST状态变化 +3. **停牌监控** - 获取股票停牌状态 +4. **策略回测** - 基于历史涨跌停限制进行回测 + +## 与其他接口的关系 + +| 接口 | 用途 | 区别 | +|------|------|------| +| `get_history_stock_status` | 获取历史状态(涨停/跌停/ST/停牌) | 返回每日状态数据 | +| `get_code_info` | 获取当前代码信息 | 返回最新静态信息 | +| `fetch_klines` | 获取K线数据 | 包含涨跌停判断逻辑 | + +## 文档更新 + +以下文档已更新: +- `INTERNAL_INTERFACES.md` - 内部接口层文档 +- `AMAZINGDATA_ADAPTER_INTERFACES_V3.md` - 接口调用关系文档 diff --git a/INTERNAL_INTERFACES.md b/INTERNAL_INTERFACES.md new file mode 100644 index 0000000..bd51adf --- /dev/null +++ b/INTERNAL_INTERFACES.md @@ -0,0 +1,148 @@ +# 内部接口层文档 + +## 架构设计 + +``` +对外接口 (AmazingDataAdapter) + │ + ├── 内部接口层 (InternalDataService) + │ ├── _MarketDataInternal (市场数据) + │ ├── _BaseDataInternal (基础数据) + │ └── _InfoDataInternal (信息数据) + │ + └── AmazingData SDK + ├── _market_data + ├── _base_data + └── _info_data +``` + +## 设计原则 + +1. **对外接口不直接调用 SDK** - 所有 SDK 调用都通过内部接口层 +2. **统一错误处理** - 内部接口层统一处理 SDK 异常 +3. **日志记录** - 内部接口层统一记录调用日志 +4. **便于测试** - 可以 mock 内部接口进行单元测试 + +## 内部接口类 + +### _MarketDataInternal + +封装 `_market_data` SDK 对象的方法: + +| 方法 | SDK 方法 | 说明 | +|------|----------|------| +| `login()` | `_market_data.login()` | 登录 | +| `is_login()` | `_market_data.is_login()` | 检查登录状态 | +| `query_kline()` | `_market_data.query_kline()` | 查询K线 | +| `query_snapshot()` | `_market_data.query_snapshot()` | 查询快照 | + +### _BaseDataInternal + +封装 `_base_data` SDK 对象的方法: + +| 方法 | SDK 方法 | 说明 | +|------|----------|------| +| `get_code_list()` | `_base_data.get_code_list()` | 获取股票代码列表 | +| `get_future_code_list()` | `_base_data.get_future_code_list()` | 获取期货代码列表 | +| `get_code_info()` | `_base_data.get_code_info()` | 获取代码信息 | +| `get_calendar()` | `_base_data.get_calendar()` | 获取交易日历 | +| `get_adj_factor()` | `_base_data.get_adj_factor()` | 获取复权因子 | +| `get_backward_factor()` | `_base_data.get_backward_factor()` | 获取后复权因子 | +| `get_etf_pcf()` | `_base_data.get_etf_pcf()` | 获取ETF申赎数据 | +| `get_hist_code_list()` | `_base_data.get_hist_code_list()` | 获取历史代码列表 | + +### _InfoDataInternal + +封装 `_info_data` SDK 对象的方法: + +| 方法 | SDK 方法 | 说明 | +|------|----------|------| +| `get_equity_structure()` | `_info_data.get_equity_structure()` | 股本结构 | +| `get_share_holder()` | `_info_data.get_share_holder()` | 股东数据 | +| `get_holder_num()` | `_info_data.get_holder_num()` | 股东户数 | +| `get_income()` | `_info_data.get_income()` | 利润表 | +| `get_balance_sheet()` | `_info_data.get_balance_sheet()` | 资产负债表 | +| `get_cash_flow()` | `_info_data.get_cash_flow()` | 现金流量表 | +| `get_profit_express()` | `_info_data.get_profit_express()` | 业绩预告 | +| `get_profit_notice()` | `_info_data.get_profit_notice()` | 业绩快报 | +| `get_margin_summary()` | `_info_data.get_margin_summary()` | 融资融券汇总 | +| `get_margin_detail()` | `_info_data.get_margin_detail()` | 融资融券明细 | +| `get_long_hu_bang()` | `_info_data.get_long_hu_bang()` | 龙虎榜 | +| `get_block_trading()` | `_info_data.get_block_trading()` | 大宗交易 | +| `get_index_constituent()` | `_info_data.get_index_constituent()` | 指数成分股 | +| `get_index_weight()` | `_info_data.get_index_weight()` | 指数权重 | +| `get_fund_share()` | `_info_data.get_fund_share()` | 基金份额 | +| `get_kzz_issuance()` | `_info_data.get_kzz_issuance()` | 可转债发行 | +| `get_history_stock_status()` | `_info_data.get_history_stock_status()` | 历史股票状态(涨停/跌停/ST/停牌) | + +## 使用方式 + +### 在 AmazingDataAdapter 中 + +```python +class AmazingDataAdapter(DataSourceAdapter): + def __init__(self): + # ... 其他初始化 ... + self._internal: Optional[InternalDataService] = None + + async def connect(self, config: dict) -> None: + # ... 登录和初始化 SDK ... + + # 初始化内部数据服务层 + self._internal = InternalDataService(self) + + def _fetch_klines_sync(self, ...): + # 使用内部接口,而不是直接调用 SDK + kline_dict = self._internal.market.query_kline(...) + code_info = self._internal.base.get_code_info(...) + equity = self._internal.info.get_equity_structure(...) +``` + +### 错误处理 + +内部接口统一处理 SDK 异常,返回空数据或默认值: + +```python +class _BaseDataInternal: + def get_code_info(self, security_type: str) -> pd.DataFrame: + try: + return self._base_data.get_code_info(security_type=security_type) + except Exception as e: + error(f"[_BaseDataInternal] Get code info failed: {e}") + return pd.DataFrame() # 返回空数据而不是抛出异常 +``` + +## 文件结构 + +``` +app/adapters/ +├── __init__.py +├── base.py # 适配器基类 +├── amazingdata_adapter.py # 对外适配器(使用内部接口) +└── internal_data_service.py # 内部接口层 +``` + +## 迁移说明 + +### 之前的调用方式(已废弃) +```python +# 直接调用 SDK +code_info = self._base_data.get_code_info(security_type=...) +kline = self._market_data.query_kline(...) +equity = self._info_data.get_equity_structure(...) +``` + +### 现在的调用方式 +```python +# 通过内部接口调用 +code_info = self._internal.base.get_code_info(security_type=...) +kline = self._internal.market.query_kline(...) +equity = self._internal.info.get_equity_structure(...) +``` + +## 优势 + +1. **解耦** - 对外接口与 SDK 解耦,便于更换数据源 +2. **可测试性** - 可以 mock 内部接口进行单元测试 +3. **错误处理** - 统一的错误处理和日志记录 +4. **扩展性** - 可以在内部接口层添加缓存、限流等功能 diff --git a/app/adapters/__pycache__/amazingdata_adapter.cpython-311.pyc b/app/adapters/__pycache__/amazingdata_adapter.cpython-311.pyc index ec0b1d5cbe1d7d59553a273defc4de726a28b699..cc15fd8c8a17091ad9f6ba57940209b074d0a772 100644 GIT binary patch delta 16389 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 delta 15447 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 RV4r-F6@7h{3hZ?D{{bB_55@oh diff --git a/app/adapters/__pycache__/internal_data_service.cpython-311.pyc b/app/adapters/__pycache__/internal_data_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..782601282ea2daae71471af92192e3c0c5f10397 GIT binary patch literal 22060 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 JIvIp`{|kl$q}>1j literal 0 HcmV?d00001 diff --git a/app/adapters/amazingdata_adapter.py b/app/adapters/amazingdata_adapter.py index ce6c494..ab0f8bb 100644 --- a/app/adapters/amazingdata_adapter.py +++ b/app/adapters/amazingdata_adapter.py @@ -17,6 +17,7 @@ from app.adapters.base import ( TradeCalData, TickCallback ) from app.core.logger import info, error, warning +from app.adapters.internal_data_service import InternalDataService class SecurityType(Enum): """证券类型枚举""" @@ -65,6 +66,7 @@ class AmazingDataAdapter(DataSourceAdapter): self._calendar = None self._is_logged_in = False self._connected = False + self._internal: Optional[InternalDataService] = None # 内部数据服务 def _check_login(self): """检查是否已登录""" @@ -90,7 +92,7 @@ class AmazingDataAdapter(DataSourceAdapter): """ try: # 方法1:尝试从代码信息中获取 - code_info_df = self._base_data.get_code_info(security_type=SecurityType.STOCK_A.value) + code_info_df = self._internal.base.get_code_info(security_type=SecurityType.STOCK_A.value) if symbol in code_info_df.index: # 尝试不同的字段名 for field in ['list_date', 'LIST_DATE', 'listDate', 'founded_date']: @@ -107,7 +109,7 @@ class AmazingDataAdapter(DataSourceAdapter): # 方法2:尝试从历史代码列表获取 try: - hist_codes = self._base_data.get_hist_code_list(security_type=SecurityType.STOCK_A.value) + hist_codes = self._internal.base.get_hist_code_list(security_type=SecurityType.STOCK_A.value) if symbol in hist_codes.index and 'list_date' in hist_codes.columns: list_date_val = hist_codes.loc[symbol, 'list_date'] if pd.notna(list_date_val): @@ -182,8 +184,11 @@ class AmazingDataAdapter(DataSourceAdapter): # 初始化数据类 self._base_data = self._ad.BaseData() self._info_data = self._ad.InfoData() - self._calendar = self._base_data.get_calendar() + self._calendar = self._internal.base.get_calendar() self._market_data = self._ad.MarketData(self._calendar) + + # 初始化内部数据服务层 + self._internal = InternalDataService(self) self._is_logged_in = True print("[amazingdata_adapter]登录成功") @@ -258,8 +263,8 @@ class AmazingDataAdapter(DataSourceAdapter): end_int = self._format_date(end_date) print(f"[amazingdata_adapter _fetch_klines_sync]正在获取K线数据: 代码={codes}, 日期范围={start_date}~{end_date}, 周期={period_value}") - # 获取K线数据 - 将周期值转换为 SDK 的常量 - kline_dict = self._market_data.query_kline( + # 获取K线数据 - 使用内部接口 + kline_dict = self._internal.market.query_kline( code_list=codes, begin_date=start_int, end_date=end_int, @@ -278,7 +283,7 @@ class AmazingDataAdapter(DataSourceAdapter): # ============================================ print(f"[amazingdata_adapter _fetch_klines_sync]正在获取证券基本信息...") try: - code_info_df = self._base_data.get_code_info(security_type=SecurityType.STOCK_A.value) + code_info_df = self._internal.base.get_code_info(security_type=SecurityType.STOCK_A.value) # 提取当前股票的涨停价和跌停价 if symbol in code_info_df.index: high_limited = float(code_info_df.loc[symbol, 'high_limited']) if 'high_limited' in code_info_df.columns else None @@ -298,7 +303,7 @@ class AmazingDataAdapter(DataSourceAdapter): # ============================================ print(f"[amazingdata_adapter _fetch_klines_sync]正在获取股本结构...") try: - equity_dict = self._info_data.get_equity_structure( + equity_dict = self._internal.info.get_equity_structure( code_list=codes, local_path=self.config.local_path, is_local=self.config.use_local_cache @@ -341,7 +346,7 @@ class AmazingDataAdapter(DataSourceAdapter): print(f"[amazingdata_adapter _fetch_klines_sync]正在获取交易日历...") try: # 获取交易日历 - calendar = self._base_data.get_calendar(market=Market.SH.value) + calendar = self._internal.base.get_calendar(market=Market.SH.value) # 获取股票上市日期 list_date = self._get_list_date(symbol) @@ -453,11 +458,11 @@ class AmazingDataAdapter(DataSourceAdapter): if asset_type == "stock": # 获取A股代码列表 - codes = self._base_data.get_code_list( + codes = self._internal.base.get_code_list( security_type=SecurityType.STOCK_A.value ) # 获取代码信息 - info_df = self._base_data.get_code_info( + info_df = self._internal.base.get_code_info( security_type=SecurityType.STOCK_A.value ) @@ -487,7 +492,7 @@ class AmazingDataAdapter(DataSourceAdapter): elif asset_type == "futures": # 获取期货代码列表 - codes = self._base_data.get_future_code_list( + codes = self._internal.base.get_future_code_list( security_type=SecurityType.FUTURE.value ) @@ -556,7 +561,7 @@ class AmazingDataAdapter(DataSourceAdapter): """同步获取交易日历""" # 获取交易日历 market = Market.SH if exchange in ["SH", "SSE"] else Market.SZ - calendar = self._base_data.get_calendar(market=market.value) + calendar = self._internal.base.get_calendar(market=market.value) start_int = self._format_date(start) end_int = self._format_date(end) @@ -582,7 +587,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() await loop.run_in_executor( None, - lambda: self._base_data.get_code_list( + lambda: self._internal.base.get_code_list( security_type=SecurityType.STOCK_A.value ) ) @@ -613,7 +618,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._base_data.get_adj_factor( + lambda: self._internal.base.get_adj_factor( code_list=codes, local_path=self.config.local_path, is_local=is_local @@ -632,7 +637,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._base_data.get_backward_factor( + lambda: self._internal.base.get_backward_factor( code_list=codes, local_path=self.config.local_path, is_local=is_local @@ -658,7 +663,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_index_constituent( + lambda: self._internal.info.get_index_constituent( code_list=codes, local_path=self.config.local_path, is_local=is_local @@ -690,7 +695,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_index_weight( + lambda: self._internal.info.get_index_weight( code_list=codes, local_path=self.config.local_path, is_local=is_local, @@ -714,7 +719,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._market_data.query_snapshot( + lambda: self._internal.market.query_snapshot( code_list=codes, begin_date=start_int, end_date=end_int @@ -811,7 +816,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._base_data.get_code_info(security_type=security_type.value) + lambda: self._internal.base.get_code_info(security_type=security_type.value) ) async def get_trading_calendar( @@ -831,7 +836,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._base_data.get_calendar(market=market.value) + lambda: self._internal.base.get_calendar(market=market.value) ) # ==================== 业绩数据接口 ==================== @@ -871,7 +876,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_profit_express( + lambda: self._internal.info.get_profit_express( code_list=codes, local_path=self.config.local_path, is_local=is_local, @@ -913,7 +918,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_profit_notice( + lambda: self._internal.info.get_profit_notice( code_list=codes, local_path=self.config.local_path, is_local=is_local, @@ -956,7 +961,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_share_holder( + lambda: self._internal.info.get_share_holder( code_list=codes, local_path=self.config.local_path, is_local=is_local, @@ -994,7 +999,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_holder_num( + lambda: self._internal.info.get_holder_num( code_list=codes, local_path=self.config.local_path, is_local=is_local, @@ -1035,7 +1040,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_equity_structure( + lambda: self._internal.info.get_equity_structure( code_list=codes, local_path=self.config.local_path, is_local=is_local, @@ -1078,7 +1083,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_margin_summary( + lambda: self._internal.info.get_margin_summary( local_path=self.config.local_path, is_local=is_local, begin_date=begin_date, @@ -1118,7 +1123,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_margin_detail( + lambda: self._internal.info.get_margin_detail( code_list=codes, local_path=self.config.local_path, is_local=is_local, @@ -1163,7 +1168,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_long_hu_bang( + lambda: self._internal.info.get_long_hu_bang( code_list=codes, local_path=self.config.local_path, is_local=is_local, @@ -1205,7 +1210,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_block_trading( + lambda: self._internal.info.get_block_trading( code_list=codes, local_path=self.config.local_path, is_local=is_local, @@ -1244,7 +1249,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._base_data.get_etf_pcf(code_list=codes) + lambda: self._internal.base.get_etf_pcf(code_list=codes) ) async def get_fund_share( @@ -1278,7 +1283,7 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_fund_share( + lambda: self._internal.info.get_fund_share( code_list=codes, local_path=self.config.local_path, is_local=is_local, @@ -1317,13 +1322,55 @@ class AmazingDataAdapter(DataSourceAdapter): loop = asyncio.get_event_loop() return await loop.run_in_executor( None, - lambda: self._info_data.get_kzz_issuance( + lambda: self._internal.info.get_kzz_issuance( code_list=codes, local_path=self.config.local_path, is_local=is_local ) ) + async def get_history_stock_status( + self, + codes: List[str], + start_date: Optional[str] = None, + end_date: Optional[str] = None, + is_local: Optional[bool] = None + ) -> Dict[str, pd.DataFrame]: + """获取历史股票状态数据 + + Args: + codes: 股票代码列表 + start_date: 开始日期 (YYYYMMDD) + end_date: 结束日期 (YYYYMMDD) + is_local: 是否使用本地缓存 + + Returns: + Dict[代码, DataFrame] 主要字段: + - TRADE_DATE: 交易日期 + - UP_LIMIT: 涨停价 + - DOWN_LIMIT: 跌停价 + - MAX_UP_DOWN: 最大涨跌幅限制 + - IS_ST: 是否ST + - IS_SUSPEND: 是否停牌 + """ + self._check_login() + is_local = is_local if is_local is not None else self.config.use_local_cache + + begin_date = self._format_date(start_date) if start_date else None + end_date_int = self._format_date(end_date) if end_date else None + + loop = asyncio.get_event_loop() + return await loop.run_in_executor( + None, + lambda: self._internal.info.get_history_stock_status( + code_list=codes, + local_path=self.config.local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date_int + ) + ) + # ==================== New Split Table Data Fetch Methods ==================== async def fetch_kline_base( @@ -1381,7 +1428,7 @@ class AmazingDataAdapter(DataSourceAdapter): start_int = self._format_date(start_date) end_int = self._format_date(end_date) - kline_dict = self._market_data.query_kline( + kline_dict = self._internal.market.query_kline( code_list=codes, begin_date=start_int, end_date=end_int, @@ -1473,7 +1520,7 @@ class AmazingDataAdapter(DataSourceAdapter): start_int = self._format_date(extended_start_str) end_int = self._format_date(end_date) - kline_dict = self._market_data.query_kline( + kline_dict = self._internal.market.query_kline( code_list=codes, begin_date=start_int, end_date=end_int, @@ -1487,7 +1534,7 @@ class AmazingDataAdapter(DataSourceAdapter): df = df.sort_values('kline_time') try: - code_info_df = self._base_data.get_code_info(security_type=SecurityType.STOCK_A.value) + code_info_df = self._internal.base.get_code_info(security_type=SecurityType.STOCK_A.value) if symbol in code_info_df.index: high_limited = float(code_info_df.loc[symbol, 'high_limited']) if 'high_limited' in code_info_df.columns else None low_limited = float(code_info_df.loc[symbol, 'low_limited']) if 'low_limited' in code_info_df.columns else None @@ -1635,7 +1682,7 @@ class AmazingDataAdapter(DataSourceAdapter): results = [] try: - equity_dict = self._info_data.get_equity_structure( + equity_dict = self._internal.info.get_equity_structure( code_list=codes, local_path=self.config.local_path, is_local=self.config.use_local_cache @@ -1659,7 +1706,7 @@ class AmazingDataAdapter(DataSourceAdapter): print(f"[amazingdata_adapter]Failed to get equity structure: {e}") equity_data = {} - kline_dict = self._market_data.query_kline( + kline_dict = self._internal.market.query_kline( code_list=codes, begin_date=start_int, end_date=end_int, @@ -1756,14 +1803,14 @@ class AmazingDataAdapter(DataSourceAdapter): ) -> List[Dict[str, Any]]: """Sync method to fetch stock basic info""" try: - all_codes = self._base_data.get_code_list( + all_codes = self._internal.base.get_code_list( security_type=SecurityType.STOCK_A.value ) if codes: all_codes = [c for c in all_codes if c in codes] - info_df = self._base_data.get_code_info( + info_df = self._internal.base.get_code_info( security_type=SecurityType.STOCK_A.value ) @@ -1784,7 +1831,7 @@ class AmazingDataAdapter(DataSourceAdapter): list_date = None try: - equity_dict = self._info_data.get_equity_structure( + equity_dict = self._internal.info.get_equity_structure( code_list=[code], local_path=self.config.local_path, is_local=self.config.use_local_cache diff --git a/app/adapters/internal_data_service.py b/app/adapters/internal_data_service.py new file mode 100644 index 0000000..4596666 --- /dev/null +++ b/app/adapters/internal_data_service.py @@ -0,0 +1,534 @@ +"""内部数据服务层 + +将 AmazingData SDK 的调用封装为内部接口 +对外接口不直接调用 SDK,而是通过内部接口调用 +""" +import pandas as pd +from datetime import datetime +from typing import List, Optional, Dict, Any + +from app.core.logger import info, error + + +class _MarketDataInternal: + """市场数据内部接口 - 封装 _market_data""" + + def __init__(self, market_data): + self._market_data = market_data + + def login(self, username: str, password: str, ip: str, port: str) -> bool: + """登录""" + try: + return self._market_data.login( + username=username, + password=password, + ip=ip, + port=port + ) + except Exception as e: + error(f"[_MarketDataInternal] Login failed: {e}") + raise + + def is_login(self) -> bool: + """检查登录状态""" + try: + return self._market_data.is_login() + except Exception: + return False + + def query_kline( + self, + code_list: List[str], + begin_date: int, + end_date: int, + period: int + ) -> Dict[str, pd.DataFrame]: + """查询K线数据""" + try: + return self._market_data.query_kline( + code_list=code_list, + begin_date=begin_date, + end_date=end_date, + period=period + ) + except Exception as e: + error(f"[_MarketDataInternal] Query kline failed: {e}") + return {} + + def query_snapshot( + self, + code_list: List[str], + begin_date: int, + end_date: int + ) -> Dict[str, pd.DataFrame]: + """查询快照数据""" + try: + return self._market_data.query_snapshot( + code_list=code_list, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_MarketDataInternal] Query snapshot failed: {e}") + return {} + + +class _BaseDataInternal: + """基础数据内部接口 - 封装 _base_data""" + + def __init__(self, base_data): + self._base_data = base_data + + def get_code_list(self, security_type: str) -> List[str]: + """获取代码列表""" + try: + return self._base_data.get_code_list(security_type=security_type) + except Exception as e: + error(f"[_BaseDataInternal] Get code list failed: {e}") + return [] + + def get_future_code_list(self, security_type: str) -> List[str]: + """获取期货代码列表""" + try: + return self._base_data.get_future_code_list(security_type=security_type) + except Exception as e: + error(f"[_BaseDataInternal] Get future code list failed: {e}") + return [] + + def get_code_info(self, security_type: str) -> pd.DataFrame: + """获取代码信息""" + try: + return self._base_data.get_code_info(security_type=security_type) + except Exception as e: + error(f"[_BaseDataInternal] Get code info failed: {e}") + return pd.DataFrame() + + def get_calendar(self, market: str) -> List[int]: + """获取交易日历""" + try: + return self._base_data.get_calendar(market=market) + except Exception as e: + error(f"[_BaseDataInternal] Get calendar failed: {e}") + return [] + + def get_adj_factor( + self, + code_list: List[str], + local_path: str, + is_local: bool + ) -> pd.DataFrame: + """获取复权因子""" + try: + return self._base_data.get_adj_factor( + code_list=code_list, + local_path=local_path, + is_local=is_local + ) + except Exception as e: + error(f"[_BaseDataInternal] Get adj factor failed: {e}") + return pd.DataFrame() + + def get_backward_factor( + self, + code_list: List[str], + local_path: str, + is_local: bool + ) -> pd.DataFrame: + """获取后复权因子""" + try: + return self._base_data.get_backward_factor( + code_list=code_list, + local_path=local_path, + is_local=is_local + ) + except Exception as e: + error(f"[_BaseDataInternal] Get backward factor failed: {e}") + return pd.DataFrame() + + def get_etf_pcf(self, code_list: List[str]) -> tuple: + """获取ETF申赎数据""" + try: + return self._base_data.get_etf_pcf(code_list=code_list) + except Exception as e: + error(f"[_BaseDataInternal] Get ETF PCF failed: {e}") + return ({}, {}) + + def get_hist_code_list(self, security_type: str) -> pd.DataFrame: + """获取历史代码列表""" + try: + return self._base_data.get_hist_code_list(security_type=security_type) + except Exception as e: + error(f"[_BaseDataInternal] Get hist code list failed: {e}") + return pd.DataFrame() + + +class _InfoDataInternal: + """信息数据内部接口 - 封装 _info_data""" + + def __init__(self, info_data): + self._info_data = info_data + + def get_equity_structure( + self, + code_list: List[str], + local_path: str, + is_local: bool + ) -> Dict[str, pd.DataFrame]: + """获取股本结构""" + try: + return self._info_data.get_equity_structure( + code_list=code_list, + local_path=local_path, + is_local=is_local + ) + except Exception as e: + error(f"[_InfoDataInternal] Get equity structure failed: {e}") + return {} + + def get_share_holder( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取股东数据""" + try: + return self._info_data.get_share_holder( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get share holder failed: {e}") + return {} + + def get_holder_num( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取股东户数""" + try: + return self._info_data.get_holder_num( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get holder num failed: {e}") + return {} + + def get_income( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取利润表""" + try: + return self._info_data.get_income( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get income failed: {e}") + return {} + + def get_balance_sheet( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取资产负债表""" + try: + return self._info_data.get_balance_sheet( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get balance sheet failed: {e}") + return {} + + def get_cash_flow( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取现金流量表""" + try: + return self._info_data.get_cash_flow( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get cash flow failed: {e}") + return {} + + def get_profit_express( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取业绩预告""" + try: + return self._info_data.get_profit_express( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get profit express failed: {e}") + return {} + + def get_profit_notice( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取业绩快报""" + try: + return self._info_data.get_profit_notice( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get profit notice failed: {e}") + return {} + + def get_margin_summary( + self, + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> pd.DataFrame: + """获取融资融券汇总""" + try: + return self._info_data.get_margin_summary( + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get margin summary failed: {e}") + return pd.DataFrame() + + def get_margin_detail( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取融资融券明细""" + try: + return self._info_data.get_margin_detail( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get margin detail failed: {e}") + return {} + + def get_long_hu_bang( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> pd.DataFrame: + """获取龙虎榜数据""" + try: + return self._info_data.get_long_hu_bang( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get long hu bang failed: {e}") + return pd.DataFrame() + + def get_block_trading( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> pd.DataFrame: + """获取大宗交易数据""" + try: + return self._info_data.get_block_trading( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get block trading failed: {e}") + return pd.DataFrame() + + def get_index_constituent( + self, + code_list: List[str], + local_path: str, + is_local: bool + ) -> Dict[str, pd.DataFrame]: + """获取指数成分股""" + try: + return self._info_data.get_index_constituent( + code_list=code_list, + local_path=local_path, + is_local=is_local + ) + except Exception as e: + error(f"[_InfoDataInternal] Get index constituent failed: {e}") + return {} + + def get_index_weight( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取指数权重""" + try: + return self._info_data.get_index_weight( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get index weight failed: {e}") + return {} + + def get_fund_share( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取基金份额""" + try: + return self._info_data.get_fund_share( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get fund share failed: {e}") + return {} + + def get_kzz_issuance( + self, + code_list: List[str], + local_path: str, + is_local: bool + ) -> Dict[str, pd.DataFrame]: + """获取可转债发行数据""" + try: + return self._info_data.get_kzz_issuance( + code_list=code_list, + local_path=local_path, + is_local=is_local + ) + except Exception as e: + error(f"[_InfoDataInternal] Get kzz issuance failed: {e}") + return {} + + def get_history_stock_status( + self, + code_list: List[str], + local_path: str, + is_local: bool, + begin_date: Optional[int] = None, + end_date: Optional[int] = None + ) -> Dict[str, pd.DataFrame]: + """获取历史股票状态数据 + + 包含字段: + - TRADE_DATE: 交易日期 + - UP_LIMIT: 涨停价 + - DOWN_LIMIT: 跌停价 + - MAX_UP_DOWN: 最大涨跌幅限制 + - IS_ST: 是否ST + - IS_SUSPEND: 是否停牌 + """ + try: + return self._info_data.get_history_stock_status( + code_list=code_list, + local_path=local_path, + is_local=is_local, + begin_date=begin_date, + end_date=end_date + ) + except Exception as e: + error(f"[_InfoDataInternal] Get history stock status failed: {e}") + return {} + + +class InternalDataService: + """内部数据服务统一入口""" + + def __init__(self, ad): + """ + Args: + ad: AmazingData SDK instance with _market_data, _base_data, _info_data + """ + self.market = _MarketDataInternal(ad._market_data) + self.base = _BaseDataInternal(ad._base_data) + self.info = _InfoDataInternal(ad._info_data) diff --git a/scripts/replace_sdk_calls.py b/scripts/replace_sdk_calls.py new file mode 100644 index 0000000..daa2e1a --- /dev/null +++ b/scripts/replace_sdk_calls.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""自动替换 SDK 调用为内部接口调用""" + +replacements = [ + # _base_data 调用 + ("self._base_data.get_code_info(", "self._internal.base.get_code_info("), + ("self._base_data.get_code_list(", "self._internal.base.get_code_list("), + ("self._base_data.get_future_code_list(", "self._internal.base.get_future_code_list("), + ("self._base_data.get_calendar(", "self._internal.base.get_calendar("), + ("self._base_data.get_adj_factor(", "self._internal.base.get_adj_factor("), + ("self._base_data.get_backward_factor(", "self._internal.base.get_backward_factor("), + ("self._base_data.get_etf_pcf(", "self._internal.base.get_etf_pcf("), + ("self._base_data.get_hist_code_list(", "self._internal.base.get_hist_code_list("), + + # _market_data 调用 + ("self._market_data.query_kline(", "self._internal.market.query_kline("), + ("self._market_data.query_snapshot(", "self._internal.market.query_snapshot("), + + # _info_data 调用 + ("self._info_data.get_equity_structure(", "self._internal.info.get_equity_structure("), + ("self._info_data.get_share_holder(", "self._internal.info.get_share_holder("), + ("self._info_data.get_holder_num(", "self._internal.info.get_holder_num("), + ("self._info_data.get_income(", "self._internal.info.get_income("), + ("self._info_data.get_balance_sheet(", "self._internal.info.get_balance_sheet("), + ("self._info_data.get_cash_flow(", "self._internal.info.get_cash_flow("), + ("self._info_data.get_profit_express(", "self._internal.info.get_profit_express("), + ("self._info_data.get_profit_notice(", "self._internal.info.get_profit_notice("), + ("self._info_data.get_margin_summary(", "self._internal.info.get_margin_summary("), + ("self._info_data.get_margin_detail(", "self._internal.info.get_margin_detail("), + ("self._info_data.get_long_hu_bang(", "self._internal.info.get_long_hu_bang("), + ("self._info_data.get_block_trading(", "self._internal.info.get_block_trading("), + ("self._info_data.get_index_constituent(", "self._internal.info.get_index_constituent("), + ("self._info_data.get_index_weight(", "self._internal.info.get_index_weight("), + ("self._info_data.get_fund_share(", "self._internal.info.get_fund_share("), + ("self._info_data.get_kzz_issuance(", "self._internal.info.get_kzz_issuance("), +] + +def main(): + with open('app/adapters/amazingdata_adapter.py', 'r', encoding='utf-8') as f: + content = f.read() + + count = 0 + for old, new in replacements: + if old in content: + occurrences = content.count(old) + content = content.replace(old, new) + count += occurrences + print(f"Replaced {occurrences} occurrence(s): {old} -> {new}") + + with open('app/adapters/amazingdata_adapter.py', 'w', encoding='utf-8') as f: + f.write(content) + + print(f"\nTotal replacements: {count}") + +if __name__ == "__main__": + main() diff --git a/todo_20260311.md b/todo_20260311.md index e6f4638..7a5b038 100644 --- a/todo_20260311.md +++ b/todo_20260311.md @@ -19,3 +19,14 @@ 2. 获取k线 stock/klines/{symbol} -- 缺少股本等信息 3. 获取交易日历 stock/trading-dates +# 2026年3月15日 接口调整 + +## 已实现 + +1. _fetch_calendar_sync 调用接口 get_calendar + +## 待调整 + +1. close接口需要logout进行退出 +2. +