From a848993e7e0a6ffeaabeabecc08bbf12ebf93b12 Mon Sep 17 00:00:00 2001 From: Lxy Date: Sun, 15 Mar 2026 12:04:51 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=8E=B7=E5=8F=96=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E6=97=A5=E5=92=8C=E8=8E=B7=E5=8F=96=E8=82=A1=E7=A5=A8=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=88=97=E8=A1=A8=E5=A2=9E=E5=8A=A0=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INTERFACE_DOCUMENTATION.md | 956 ++++++++++++++++++ .../internal_data_service.cpython-311.pyc | Bin 22338 -> 31026 bytes app/adapters/internal_data_service.py | 169 +++- 3 files changed, 1120 insertions(+), 5 deletions(-) create mode 100644 INTERFACE_DOCUMENTATION.md diff --git a/INTERFACE_DOCUMENTATION.md b/INTERFACE_DOCUMENTATION.md new file mode 100644 index 0000000..cdaf3e5 --- /dev/null +++ b/INTERFACE_DOCUMENTATION.md @@ -0,0 +1,956 @@ +# 市场数据服务接口文档 + +## 架构概览 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ HTTP 客户端 │ +│ (浏览器/Postman/其他服务) │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ API 路由层 (HTTP) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ +│ │ /stock/* │ │ /futures/* │ │ /admin/* │ │ /stream (WS) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 服务层 (Service) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ +│ │ StockService │ │FuturesService│ │ AdminService │ │ TestService │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 适配器服务层 (Adapter) │ +│ ┌──────────────────────────────────┐ │ +│ │ AdapterService (单例) │ │ +│ │ ┌────────────────────────────┐ │ │ +│ │ │ AmazingDataAdapter │ │ │ +│ │ │ ┌──────────────────────┐ │ │ │ +│ │ │ │ _internal: │ │ │ │ +│ │ │ │ InternalDataService │ │ │ │ +│ │ │ └──────────────────────┘ │ │ │ +│ │ └────────────────────────────┘ │ │ +│ └──────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 内部数据服务层 (Internal) │ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │ _MarketDataInternal│ │ _BaseDataInternal │ │ _InfoDataInternal │ │ +│ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────────────┐ │ │ +│ │ │ query_kline │ │ │ │get_code_list │ │ │ │ get_equity_structure │ │ │ +│ │ │query_snapshot│ │ │ │ get_calendar │ │ │ │ get_share_holder │ │ │ +│ │ └──────────────┘ │ │ │get_adj_factor│ │ │ │ get_income │ │ │ +│ │ │ │ │ get_etf_pcf │ │ │ │ get_balance_sheet │ │ │ +│ │ │ │ └──────────────┘ │ │ │ get_cash_flow │ │ │ +│ │ │ │ │ │ │ get_margin_* │ │ │ +│ │ │ │ │ │ │ get_index_* │ │ │ +│ │ │ │ │ │ │ get_fund_share │ │ │ +│ └──────────────────┘ └──────────────────┘ │ └──────────────────────┘ │ │ +│ └──────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ SDK 层 (AmazingData) │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ login │ │BaseData │ │MarketData│ │ InfoData │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ (银河证券星耀数智) │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 一、对外接口 (HTTP API) + +对外接口是提供给外部系统调用的 RESTful API,路径以 `/v1` 为前缀。 + +### 1.1 股票接口 + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 查询股票K线 | GET | `/v1/stock/klines/{symbol}` | 获取指定股票的历史K线数据 | +| 查询股票列表 | GET | `/v1/stock/symbols` | 获取所有股票代码列表 | +| 查询股票基本信息 | GET | `/v1/stock/basic/{symbol}` | 获取股票的基本信息 | +| 批量查询K线 | POST | `/v1/stock/klines/batch` | 批量获取多只股票的K线 | + +**调用链示例:** +``` +GET /v1/stock/klines/000001.SZ + │ + ▼ +routes.py:query_stock_klines() + │ + ▼ +StockService.query_klines() + │ + ├── 1. 先查数据库 (StockRepository) + │ └── 有数据 → 直接返回 + │ + └── 2. 无数据 → 从适配器获取 + │ + ▼ + AdapterService.get_active_adapter("stock") + │ + ▼ + AmazingDataAdapter.fetch_klines() + │ + ▼ + InternalDataService.market.query_kline() + │ + ▼ + AmazingData.MarketData.query_kline() +``` + +### 1.2 期货接口 + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 查询期货K线 | GET | `/v1/futures/klines/{symbol}` | 获取期货合约K线数据 | +| 查询期货列表 | GET | `/v1/futures/symbols` | 获取所有期货合约列表 | + +### 1.3 管理接口 + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 健康检查 | GET | `/v1/admin/health` | 检查服务健康状态 | +| 数据源状态 | GET | `/v1/admin/source/status` | 查看数据源连接状态 | +| 适配器列表 | GET | `/v1/admin/adapters` | 查看所有适配器配置 | + +### 1.4 测试接口 (Admin) + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 获取API测试列表 | GET | `/v1/admin/tests/api` | 获取对外接口测试列表 | +| 执行API测试 | POST | `/v1/admin/tests/api/run` | 执行指定的API测试 | +| **获取内部接口测试列表** | **GET** | **`/v1/admin/tests/internal`** | **获取对内SDK接口测试列表** | +| **执行内部接口测试** | **POST** | **`/v1/admin/tests/internal/run`** | **执行指定的对内接口测试** | + +--- + +## 二、对内接口 (Internal SDK 封装层) + +对内接口是直接封装 AmazingData SDK 的内部方法,通过 `InternalDataService` 统一暴露。 + +### 2.1 访问路径 + +```python +# 获取 adapter +adapter = AdapterService.get_active_adapter("stock") + +# 访问内部接口 +adapter._internal.market.query_kline(...) +adapter._internal.base.get_code_list(...) +adapter._internal.info.get_equity_structure(...) +``` + +### 2.2 接口清单 (23个) + +#### 2.2.1 市场数据接口 (_MarketDataInternal) + +| 序号 | 方法名 | 说明 | 对应 SDK | +|------|--------|------|----------| +| 1 | `query_kline()` | 查询K线数据 | `MarketData.query_kline()` | +| 2 | `query_snapshot()` | 查询快照数据 | `MarketData.query_snapshot()` | + +#### 2.2.2 基础数据接口 (_BaseDataInternal) + +| 序号 | 方法名 | 说明 | 对应 SDK | +|------|--------|------|----------| +| 3 | `get_code_list()` | 获取股票代码列表 | `BaseData.get_code_list()` | +| 4 | `get_future_code_list()` | 获取期货代码列表 | `BaseData.get_future_code_list()` | +| 5 | `get_code_info()` | 获取代码详细信息 | `BaseData.get_code_info()` | +| 6 | `get_calendar()` | 获取交易日历 | `BaseData.get_calendar()` | +| 7 | `get_adj_factor()` | 获取复权因子 | `BaseData.get_adj_factor()` | +| 8 | `get_etf_pcf()` | 获取ETF申赎数据 | `BaseData.get_etf_pcf()` | + +#### 2.2.3 股本股东接口 (_InfoDataInternal) + +| 序号 | 方法名 | 说明 | 对应 SDK | 特殊处理 | +|------|--------|------|----------|----------| +| 9 | `get_equity_structure()` | 获取股本结构 | `InfoData.get_equity_structure()` | - | +| 10 | `get_share_holder()` | 获取股东数据 | `InfoData.get_share_holder()` | **每次创建新实例** | +| 11 | `get_holder_num()` | 获取股东户数 | `InfoData.get_holder_num()` | - | + +#### 2.2.4 财务报表接口 (_InfoDataInternal) + +| 序号 | 方法名 | 说明 | 对应 SDK | +|------|--------|------|----------| +| 12 | `get_income()` | 获取利润表 | `InfoData.get_income()` | +| 13 | `get_balance_sheet()` | 获取资产负债表 | `InfoData.get_balance_sheet()` | +| 14 | `get_cash_flow()` | 获取现金流量表 | `InfoData.get_cash_flow()` | + +#### 2.2.5 市场状态接口 (_InfoDataInternal) + +| 序号 | 方法名 | 说明 | 对应 SDK | +|------|--------|------|----------| +| 15 | `get_history_stock_status()` | 获取历史股票状态 | `InfoData.get_history_stock_status()` | +| 16 | `get_margin_summary()` | 获取融资融券汇总 | `InfoData.get_margin_summary()` | +| 17 | `get_margin_detail()` | 获取融资融券明细 | `InfoData.get_margin_detail()` | + +#### 2.2.6 特色数据接口 (_InfoDataInternal) + +| 序号 | 方法名 | 说明 | 对应 SDK | +|------|--------|------|----------| +| 18 | `get_long_hu_bang()` | 获取龙虎榜数据 | `InfoData.get_long_hu_bang()` | +| 19 | `get_block_trading()` | 获取大宗交易数据 | `InfoData.get_block_trading()` | +| 20 | `get_index_constituent()` | 获取指数成分股 | `InfoData.get_index_constituent()` | +| 21 | `get_index_weight()` | 获取指数权重 | `InfoData.get_index_weight()` | + +#### 2.2.7 基金可转债接口 (_InfoDataInternal) + +| 序号 | 方法名 | 说明 | 对应 SDK | +|------|--------|------|----------| +| 22 | `get_fund_share()` | 获取基金份额 | `InfoData.get_fund_share()` | +| 23 | `get_kzz_issuance()` | 获取可转债发行 | `InfoData.get_kzz_issuance()` | + +--- + +## 三、调用链详解 + +### 3.1 完整调用链示例:查询股票K线 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第1层:HTTP 客户端请求 │ +│ GET http://localhost:8080/v1/stock/klines/000001.SZ?start=20240101&... │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第2层:API 路由层 (app/api/routes.py) │ +│ @router.get("/stock/klines/{symbol}") │ +│ def query_stock_klines(symbol, start, end, freq, adjust, db, api_key) │ +│ │ +│ 职责:参数校验、认证、调用服务层 │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第3层:服务层 (app/services/stock_service.py) │ +│ class StockService: │ +│ def query_klines(self, req: KLineQueryRequest) -> KLineData: │ +│ │ +│ 职责:业务逻辑、数据聚合、缓存策略 │ +│ 步骤: │ +│ 1. 先查询数据库 (StockRepository) │ +│ 2. 如无数据,调用适配器获取 │ +│ 3. 保存到数据库 │ +│ 4. 返回格式化数据 │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ (如无缓存) + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第4层:适配器服务层 (app/services/adapter_service.py) │ +│ class AdapterService: # 单例模式 │ +│ def get_active_adapter(self, asset_class: str) -> DataSourceAdapter: │ +│ │ +│ 职责:管理适配器生命周期、连接管理、配置管理 │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第5层:适配器实现 (app/adapters/amazingdata_adapter.py) │ +│ class AmazingDataAdapter(DataSourceAdapter): │ +│ async def fetch_klines(self, symbol, start, end, freq) -> List[KLine] │ +│ │ +│ 职责:适配器具体实现、数据格式转换、调用内部接口 │ +│ │ +│ 内部调用: │ +│ self._internal.market.query_kline( │ +│ code_list=[symbol], │ +│ begin_date=int(start), │ +│ end_date=int(end), │ +│ period=10000 # SDK只支持日线 │ +│ ) │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第6层:内部数据服务层 (app/adapters/internal_data_service.py) │ +│ class _MarketDataInternal: │ +│ def query_kline(self, code_list, begin_date, end_date, period): │ +│ return self._market_data.query_kline(...) │ +│ │ +│ 职责:SDK 封装、异常处理、日志记录 │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第7层:SDK 层 (AmazingData) │ +│ from AmazingData import MarketData │ +│ market_data = MarketData(calendar) │ +│ market_data.query_kline(code_list, begin_date, end_date, period) │ +│ │ +│ 职责:与银河证券星耀数智服务器通信 │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### 3.2 内部接口测试调用链 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第1层:管理后台点击"测试"按钮 │ +│ POST /v1/admin/tests/internal/run │ +│ Body: { "id": "internal_market_query_kline", "params": {...} } │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第2层:Admin 路由 (app/api/admin_routes.py) │ +│ @admin_router.post("/admin/tests/internal/run") │ +│ async def run_internal_test(req: APITestRequest, token): │ +│ │ +│ 职责:获取 adapter、确保连接、调用测试服务 │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第3层:测试服务 (app/services/test_service.py) │ +│ class TestService: │ +│ async def run_internal_test(self, adapter, req: APITestRequest): │ +│ │ +│ 职责:根据 test_id 分发到对应的内部接口调用 │ +│ │ +│ 例如 req.id == "internal_market_query_kline": │ +│ adapter._internal.market.query_kline(...) │ +│ │ +│ 例如 req.id == "internal_info_get_share_holder": │ +│ adapter._internal.info.get_share_holder(...) # 创建新 InfoData 实例 │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第4层:内部数据服务层 (app/adapters/internal_data_service.py) │ +│ │ +│ 标准调用流程: │ +│ def query_kline(self, ...): │ +│ return self._market_data.query_kline(...) │ +│ │ +│ 特殊处理(get_share_holder): │ +│ def get_share_holder(self, ...): │ +│ # 创建新的 InfoData 实例以避免 SDK 内部状态问题 │ +│ import AmazingData as ad │ +│ info_data = ad.InfoData() # <-- 新实例 │ +│ return info_data.get_share_holder(...) │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 第5层:SDK 层 (AmazingData) │ +│ 与银河证券星耀数智服务器通信 │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 四、关键代码片段 + +### 4.1 内部数据服务初始化 + +```python +# app/adapters/amazingdata_adapter.py +class AmazingDataAdapter(DataSourceAdapter): + def __init__(self): + self._internal: Optional[InternalDataService] = None + + def _do_login(self): + # 初始化 SDK 数据类 + self._base_data = self._ad.BaseData() + self._info_data = self._ad.InfoData() + self._calendar = self._base_data.get_calendar() + self._market_data = self._ad.MarketData(self._calendar) + + # 初始化内部数据服务层(关键!) + self._internal = InternalDataService(self) +``` + +### 4.2 内部数据服务封装 + +```python +# app/adapters/internal_data_service.py +class InternalDataService: + """内部数据服务统一入口""" + + def __init__(self, ad): + """ + Args: + ad: AmazingDataAdapter 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) +``` + +### 4.3 特殊处理:get_share_holder + +```python +# app/adapters/internal_data_service.py +class _InfoDataInternal: + def get_share_holder(self, code_list, local_path, is_local, begin_date=None, end_date=None): + """获取股东数据""" + try: + # 创建新的 InfoData 实例以避免 SDK 内部状态问题 + import AmazingData as ad + info_data = ad.InfoData() + return 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 {} +``` + +--- + +## 五、接口测试清单 + +### 5.1 对外接口测试 (18个) + +| 分类 | 测试项 | 路径 | +|------|--------|------| +| 股票接口 | 查询股票K线 | GET /v1/stock/klines/{symbol} | +| 股票接口 | 查询股票列表 | GET /v1/stock/symbols | +| 期货接口 | 查询期货K线 | GET /v1/futures/klines/{symbol} | +| 期货接口 | 查询期货列表 | GET /v1/futures/symbols | +| 管理接口 | 健康检查 | GET /v1/admin/health | +| ... | ... | ... | + +### 5.2 对内接口测试 (23个) + +详见第二节接口清单,每个内部接口都有对应的测试用例。 + +--- + +## 六、配置说明 + +### 6.1 数据源配置 (config.json) + +```json +{ + "sources": { + "stock": { + "active": "amazingdata", + "list": { + "amazingdata": { + "type": "sdk", + "config": { + "username": "your_username", + "password": "your_password", + "host": "140.206.44.234", + "port": "8600", + "local_path": "./amazing_data_cache/", + "use_local_cache": "true" + } + } + } + } + } +} +``` + +--- + +## 七、总结 + +1. **对外接口**:HTTP RESTful API,供外部系统调用 +2. **对内接口**:SDK 封装层,通过 `adapter._internal` 访问 +3. **调用链**:外部请求 → 路由 → 服务 → 适配器服务 → 适配器 → 内部接口 → SDK +4. **特殊处理**:`get_share_holder` 每次创建新的 `InfoData` 实例以避免 SDK 状态问题 + +--- + +## 八、数据流向与缓存策略 + +### 8.1 数据流向图 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 数据流向架构 │ +└─────────────────────────────────────────────────────────────────────────────┘ + + 外部请求 + │ + ▼ +┌─────────┐ ┌─────────┐ ┌─────────┐ +│ API │────▶│ Service│────▶│Repository│ +│ Layer │ │ Layer │ │ Layer │ +└────┬────┘ └────┬────┘ └────┬────┘ + │ │ │ + │ │ ▼ + │ │ ┌─────────┐ + │ │ │ MySQL │ + │ │ │ Database│ + │ │ └────┬────┘ + │ │ │ + │ ▼ │ + │ ┌─────────┐ │ + │ │ Adapter │ │ + │ │ Service │ │ + │ └────┬────┘ │ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────┐ +│ AmazingDataAdapter │ +│ ┌─────────────────────────────────┐ │ +│ │ InternalDataService │ │ +│ │ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ _Market │ │ _Base │ ... │ │ +│ │ │ _Data │ │ _Data │ │ │ +│ │ └────┬────┘ └────┬────┘ │ │ +│ └───────┼───────────┼────────────┘ │ +└──────────┼───────────┼─────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────┐ +│ AmazingData SDK │ +│ MarketData BaseData InfoData │ +└─────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ 银河证券星耀数智服务器 │ +└─────────────────────────────────────────┘ +``` + +### 8.2 缓存策略 + +```python +# 数据获取优先级 +1. 内存缓存 (Redis) - 最高优先级,实时数据 +2. 数据库 (MySQL) - 持久化存储,历史数据 +3. SDK 实时获取 - 兜底方案,网络请求 +``` + +**缓存规则:** + +| 数据类型 | 缓存位置 | 缓存时间 | 更新策略 | +|---------|---------|---------|---------| +| K线数据 | MySQL + Redis | 永久 + 5分钟 | 懒加载 | +| 股票列表 | MySQL | 永久 | 每日同步 | +| 实时行情 | Redis | 1分钟 | 实时推送 | +| 财务数据 | MySQL | 永久 | 季度更新 | + +--- + +## 九、错误处理机制 + +### 9.1 错误处理层级 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 错误处理流程 │ +└─────────────────────────────────────────────────────────────────────────────┘ + +SDK 错误 + │ + ▼ +┌─────────────────┐ +│ Internal Layer │ 捕获异常,记录日志,返回默认值 +│ try/except │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ Adapter Layer │ 转换数据格式,处理空值 +│ │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ Service Layer │ 业务逻辑判断,降级策略 +│ │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ API Layer │ 包装错误响应,HTTP 状态码 +│ │ +└────────┬────────┘ + │ + ▼ + 客户端收到错误响应 +``` + +### 9.2 错误码定义 + +| 错误码 | 说明 | 处理方式 | +|-------|------|---------| +| 0 | 成功 | - | +| 400 | 请求参数错误 | 检查参数格式 | +| 401 | 未授权 | 检查 API Key | +| 404 | 数据不存在 | 尝试从 SDK 获取 | +| 500 | 服务器内部错误 | 查看日志,联系管理员 | +| 503 | 数据源不可用 | 检查适配器连接状态 | + +### 9.3 内部接口错误处理示例 + +```python +# app/adapters/internal_data_service.py +class _MarketDataInternal: + def query_kline(self, code_list, begin_date, end_date, period): + """查询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}") + error(f" Params: code_list={code_list}, period={period}") + # 返回空结果而非抛出异常,保证服务可用性 + return {} +``` + +--- + +## 十、WebSocket 实时数据接口 + +### 10.1 WebSocket 连接 + +``` +ws://localhost:8080/v1/stream +``` + +### 10.2 消息类型 + +| 消息类型 | 方向 | 说明 | +|---------|------|------| +| subscribe | C→S | 订阅标的 | +| unsubscribe | C→S | 取消订阅 | +| tick | S→C | 实时 tick 数据 | +| kline | S→C | 实时 K线数据 | +| error | S→C | 错误通知 | + +### 10.3 订阅示例 + +```javascript +// 客户端订阅 +const ws = new WebSocket('ws://localhost:8080/v1/stream'); + +ws.onopen = () => { + // 订阅股票 + ws.send(JSON.stringify({ + action: 'subscribe', + symbols: ['000001.SZ', '000002.SZ'] + })); +}; + +ws.onmessage = (event) => { + const data = JSON.parse(event.data); + console.log('收到数据:', data); +}; +``` + +### 10.4 WebSocket 调用链 + +``` +客户端订阅 + │ + ▼ +WebSocket Endpoint + │ + ▼ +StreamService + │ + ▼ +AdapterService.get_adapter("stock") + │ + ▼ +AmazingDataAdapter.subscribe_tick(symbols, callback) + │ + ▼ +AmazingData.MarketData.subscribe(...) # SDK 订阅 + │ + ▼ +数据推送回调 + │ + ▼ +WebSocket 发送给客户端 +``` + +--- + +## 十一、数据同步机制 + +### 11.1 同步任务类型 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 数据同步架构 │ +└─────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 股票K线同步 │ │ 期货K线同步 │ │ 股票列表同步│ │ 财务数据同步│ +└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ │ + └────────────────┴────────────────┴────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ Sync Service │ + └────────┬────────┘ + │ + ┌────────────┼────────────┐ + ▼ ▼ ▼ + ┌─────────┐ ┌─────────┐ ┌─────────┐ + │ Daily │ │ Weekly │ │ Manual │ + │ Sync │ │ Sync │ │ Sync │ + └─────────┘ └─────────┘ └─────────┘ +``` + +### 11.2 同步接口 + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 手动同步 | POST | `/v1/admin/data/sync` | 触发指定类型的数据同步 | + +### 11.3 同步调用链 + +``` +POST /v1/admin/data/sync + │ + ▼ +AdminService.sync_data(sync_type) + │ + ├── sync_type = "base" → 同步基础K线 + ├── sync_type = "quote" → 同步行情指标 + ├── sync_type = "finance" → 同步财务数据 + └── sync_type = "full" → 全量同步 + │ + ▼ +AmazingDataAdapter.fetch_stock_basic_info() + │ + ▼ +InternalDataService.base.get_code_list() +InternalDataService.base.get_code_info() +InternalDataService.info.get_equity_structure() + │ + ▼ +保存到 MySQL +``` + +--- + +## 十二、数据库表结构 + +### 12.1 表清单 + +| 表名 | 说明 | 数据来源 | +|------|------|---------| +| `stock_symbols` | 股票基础信息 | SDK BaseData | +| `stock_klines_1d_base` | 股票日线基础数据 | SDK MarketData | +| `stock_klines_1d_quote` | 股票日线行情指标 | SDK MarketData | +| `stock_klines_1d_finance` | 股票日线财务数据 | SDK InfoData | +| `futures_klines_1d_base` | 期货日线基础数据 | SDK MarketData | +| `futures_klines_1d_quote` | 期货日线行情指标 | SDK MarketData | +| `realtime_quotes` | 实时行情数据 | SDK MarketData | +| `sync_tasks` | 同步任务记录 | 系统生成 | + +### 12.2 分表策略 + +``` +stock_klines_* +├── stock_klines_1d_base # 日线基础K线 (OHLCV) +├── stock_klines_1d_quote # 日线行情指标 (均线、MACD等) +├── stock_klines_1d_finance # 日线财务数据 (市值、PE等) +├── stock_klines_1m_base # 分钟线基础数据 +└── stock_klines_5m_base # 5分钟线基础数据 +``` + +--- + +## 十三、部署与运维 + +### 13.1 启动流程 + +```bash +# 1. 启动服务 +python -m uvicorn app.main:app --host 0.0.0.0 --port 8080 + +# 2. 服务初始化流程 +main.py + │ + ├── 加载配置 (config.json) + │ + ├── 初始化数据库连接 + │ + ├── 初始化 Redis 连接 + │ + └── 启动 FastAPI 服务 + │ + └── 首次请求时懒加载适配器 +``` + +### 13.2 适配器连接流程 + +``` +首次请求 + │ + ▼ +AdapterService.get_active_adapter("stock") + │ + ├── 检查 active_adapters 缓存 + │ ├── 存在且已连接 → 直接返回 + │ └── 不存在或断开 → 创建新连接 + │ + ▼ +AmazingDataAdapter.connect(config) + │ + ├── 登录 SDK + │ AmazingData.login(username, password, host, port) + │ + ├── 初始化数据类 + │ BaseData(), InfoData(), MarketData() + │ + └── 初始化 InternalDataService + │ self._internal = InternalDataService(self) + │ + ▼ +保存到 active_adapters 缓存 +``` + +### 13.3 健康检查 + +```bash +# 健康检查接口 +GET /v1/admin/health + +# 响应示例 +{ + "code": 0, + "message": "success", + "data": { + "status": "healthy", + "database": "connected", + "redis": "connected", + "adapters": { + "amazingdata": "connected" + } + } +} +``` + +--- + +## 十四、开发指南 + +### 14.1 新增对外接口 + +```python +# 1. 在 routes.py 添加路由 +@router.get("/stock/new_endpoint/{param}") +def new_endpoint(param: str, db: Session = Depends(get_db)): + service = StockService(db) + return service.new_method(param) + +# 2. 在 service 实现业务逻辑 +class StockService: + def new_method(self, param): + # 业务逻辑 + return result + +# 3. 如需 SDK 数据,在 adapter 添加方法 +class AmazingDataAdapter: + async def fetch_new_data(self, param): + return self._internal.xxx.method(param) +``` + +### 14.2 新增对内接口 + +```python +# 1. 在 internal_data_service.py 添加方法 +class _InfoDataInternal: + def new_internal_method(self, param): + try: + return self._info_data.sdk_method(param) + except Exception as e: + error(f"[_InfoDataInternal] new method failed: {e}") + return default_value + +# 2. 在 test_service.py 添加测试用例 +APITestCase( + id="internal_info_new_method", + name="SDK: new_method", + description="...", + method="INTERNAL", + path="AmazingDataAdapter._internal.info.new_internal_method" +) +``` + +### 14.3 调用关系速查 + +``` +需要调用 SDK 的接口: + ↓ +adapter._internal.{category}.{method} + +category 可选值: + - market: 市场数据 (K线、快照) + - base: 基础数据 (代码列表、日历、复权因子) + - info: 信息数据 (财务、股本、融资融券等) +``` + +--- + +## 十五、附录 + +### 15.1 SDK Period 参数对照表 + +| 周期 | SDK 参数值 | 说明 | +|------|-----------|------| +| 日线 | 10000 | 仅支持此值 | +| 1分钟 | - | SDK 不支持 | +| 5分钟 | - | SDK 不支持 | + +> 注意:当前 SDK 版本 `query_kline` 仅支持 `period=10000`(日线),其他值会导致 `'NoneType' object cannot be interpreted as an integer` 错误。 + +### 15.2 文件结构 + +``` +app/ +├── api/ +│ ├── routes.py # 对外接口路由 +│ └── admin_routes.py # 管理后台路由 +├── services/ +│ ├── stock_service.py # 股票业务服务 +│ ├── futures_service.py # 期货业务服务 +│ ├── adapter_service.py # 适配器管理服务 +│ └── test_service.py # 测试服务 +├── adapters/ +│ ├── amazingdata_adapter.py # 星耀数智适配器 +│ ├── internal_data_service.py # 内部数据服务层 +│ └── base.py # 适配器基类 +├── models/ # 数据模型 +├── repositories/ # 数据访问层 +└── core/ # 核心工具 +``` + +### 15.3 关键配置项 + +| 配置项 | 说明 | 默认值 | +|-------|------|--------| +| `sources.stock.active` | 当前激活的股票数据源 | `amazingdata` | +| `sources.stock.list.amazingdata.config.local_path` | 本地缓存路径 | `./amazing_data_cache/` | +| `sources.stock.list.amazingdata.config.use_local_cache` | 是否使用本地缓存 | `true` | + +--- + +**文档版本**: 1.0 +**最后更新**: 2026-03-15 +**适用版本**: Python Market Data Service diff --git a/app/adapters/__pycache__/internal_data_service.cpython-311.pyc b/app/adapters/__pycache__/internal_data_service.cpython-311.pyc index 7cc299fda924d2e48ed2cd708ab2fabf05ed23ee..e39274a3c4df2f4e8309c90062ee7be21a071637 100644 GIT binary patch literal 31026 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 a)4gxfcm!yE;IX3ng|k_-|Ehs@Vf}vytu9Rf delta 4678 zcmai2Yj9K75xz$+OO_?emMvubKz@KN-UJ8_1LhIR@G@X95J4*+?NqPHXqx<94xLI1u;IHm}wG42(vqs4V7?sKpO z>MSj+HM7F9x6oR82GUapAC*>b5*F>6MKu;ksQvL|o>8O5Nr6#e&xl6z_?Uu^=_Y8j zOtgHDvlx1;?@g{mMOLpSdDhSyk4N}KeC7=#3_=r*CAkTuG)Op?B<`epL9~_b zNZ2w*%bJ);XsmGAR-xU0G$p)8WtS zRRx=?%i(g9b>)14(5EE*!MLpWBVpW7N^_B$m-Jei&^j#{^>~NLq3vKhKBOm|Y<9sd zx5d$gPpX3AN77w`!Qz7U$&Ik1*#Up(p27YJr<&)&9~<9fi{Vhy;)TL&6`2jCk}0kf zv(A?g;uwn^@Smn>%m=N_Q6pc9YA`RmMS;RoEoG*~I4~NX1#YxdEH4Nz91F#B;qh6x zfX^mzzo``jHWHlCNDF&m`sCN)ca7)CVCAo08h#FjPrv-d&wq?1VHM6kIb+&#szt(P znV>WfQDSm{=S+G>7^V3|7+nnwGn>78QJK%=$Ic1Qc6{WV^?`HN)dRPkv)*?u`_Q@U zcG)u6)a-^o&7M&rxZsmACl(l)RUGOckbyODVd1<-g+F%kCm`?jhXTQ5JkO&X377JV zg0_Ct%Yve?4i#w+?tEDuq?Fmb=oB-pCEe_iBr|}%y^hTXPkYVG(X`5$-7DMlB;NpOrG{rD>)m)vQq~l^wMh&dd&XYq4GW3dHvHvY5>rV`BSGbqPbWd;lhfJa9VBu|N-4=_l2}b*1_{~;PK;%Z$y6eu za0-OeS>$s_acU6CRys6h#{8=mIb2C3%dB&8hZ*+vl*0<4-g8H^|?WCrQlOkxkC8p13f4=JJ;}VEEioF0yY9EbDQ@(7JlI1J16S*pl0-V)^Dn#Pk%{-0iAV z_NSE)qX|Edp(U~BMgD9ZMttIIKK6$0R2F7dn&novfDLZ0NV&S7*PBDp-6ln z$JaQZ5m?{d!km!kK8BdjC^zBycAJJfOVIJR?X{R~-)(nK8QuPIpkl;93vp1M;PFrx zi`BsdS5&nP(OqzG2T?&5e!0^osHlfKJ0~Le-JKH}9|WGj;A`C@xUqOL98{JvOfY(< za0PZf`b~9_L`-Wj4EbtM}7#gie-?!F95Vf>2q1F6!=DEePMV0#*a*rl_d81c^(u`t|U}E)@D`m!lko%&i{L8=$VMvFKnoX?L9Tf zWTMA4Eyv_&JmYNG0X&4Qx-^5vKy~KtJx`OL_qrP3`8_qO@{%+X5AO3Pc|Z=w`tp2z z5c(kQSe-q8@h`~IGll^q3qG@4L+?u=(d%Gq;c#y|djEEB?E^~M4~geiN-<|RCMyTA zy^5*Ha5AODk}ols9DJO$Qi|D!pL%lm)Yaj$N4|dYX|6GJuGa*oW#>W>nL|6<1Tmn5 z`}!Yz`VLXWg8#~8>@AVb9z{#%TS(fT(7bj5E2>o}6_fEuV`Q{OOkL^oAWOIU9)39I{l0YCA6C^ARggSG#oh#mMYYXe@K*+MKau6wc0Kca z*gaAK=Zho%{@%vb!j79*>c`eOj^B4EF%Q&aAemD0>=h5}2n9A(l-Z5@s&sc51os^s zqIkTz&Q5Q`_^pEI5=6fs`T#mV22TEF0@J1Xg%3NR0oh>)OK#1kawX!0b9 z1ES9uq+7}(euxC^06$EETrTV_x@nBW%lM~GE`f{7n=|i5j3rtt(4D!Xo@Q&TG|Nt6 z6$xrZv}V~rUpitvCqB6u#mH9>--GuB{Xr(x$SUDZ>XF6N-E?}5L!G2l^B) z{1zWh;=5K0P*5Z6j`ii=<|UQUxSWb8%lL6b!(^854RCm%siOY2H0IyO@XkOzyf List[str]: - """获取代码列表""" + """获取代码列表 - 优先从数据库获取,无数据则从SDK获取并缓存""" + # 1. 先尝试从数据库获取 + if DB_AVAILABLE: + try: + db = SessionLocal() + # 检查数据库中是否有数据(且数据不超过1天) + count = db.query(StockSymbol).count() + latest_update = db.query(func.max(StockSymbol.updated_at)).scalar() + + if count > 0 and latest_update: + # 数据存在且在1天内,直接返回 + if datetime.now() - latest_update < timedelta(days=1): + info(f"[_BaseDataInternal] Get code list from database: {count} records") + symbols = db.query(StockSymbol.symbol_id).all() + db.close() + return [s[0] for s in symbols] + else: + info(f"[_BaseDataInternal] Database cache expired (last update: {latest_update}), fetching from SDK...") + else: + info(f"[_BaseDataInternal] No data in database, fetching from SDK...") + db.close() + except Exception as db_err: + error(f"[_BaseDataInternal] Database query failed: {db_err}, fallback to SDK") + if 'db' in locals(): + db.close() + + # 2. 从SDK获取 try: - return self._base_data.get_code_list(security_type=security_type) + codes = self._base_data.get_code_list(security_type=security_type) + info(f"[_BaseDataInternal] Got {len(codes)} codes from SDK") + + # 3. 保存到数据库(异步保存,不阻塞返回) + if DB_AVAILABLE and codes: + try: + self._save_codes_to_db(codes, security_type) + except Exception as save_err: + error(f"[_BaseDataInternal] Failed to save codes to db: {save_err}") + + return codes except Exception as e: error(f"[_BaseDataInternal] Get code list failed: {e}") return [] + def _save_codes_to_db(self, codes: List[str], security_type: str): + """将代码列表保存到数据库""" + db = SessionLocal() + try: + now = datetime.now() + saved_count = 0 + + for code in codes: + # 解析代码和交易所 + if '.' in code: + symbol_id, exchange = code.split('.') + else: + symbol_id = code + exchange = 'UNKNOWN' + + # 检查是否已存在 + existing = db.query(StockSymbol).filter_by(symbol_id=code).first() + if not existing: + symbol = StockSymbol( + symbol_id=code, + symbol_type=security_type, + exchange=exchange, + name=symbol_id, # 暂时用代码作为名称 + status='active', + created_at=now, + updated_at=now + ) + db.add(symbol) + saved_count += 1 + + db.commit() + info(f"[_BaseDataInternal] Saved {saved_count} new codes to database") + except Exception as e: + db.rollback() + raise e + finally: + db.close() + def get_future_code_list(self, security_type: str) -> List[str]: """获取期货代码列表""" try: @@ -104,13 +188,88 @@ class _BaseDataInternal: return pd.DataFrame() def get_calendar(self, market: str) -> List[int]: - """获取交易日历""" + """获取交易日历 - 优先从数据库获取,无数据则从SDK获取并缓存""" + # 1. 先尝试从数据库获取 + if DB_AVAILABLE: + try: + db = SessionLocal() + # 检查数据库中是否有数据(且数据不超过7天) + count = db.query(StockTradingCalendar).count() + latest_update = db.query(func.max(StockTradingCalendar.updated_at)).scalar() + + if count > 0 and latest_update: + # 数据存在且在7天内,直接返回交易日列表 + if datetime.now() - latest_update < timedelta(days=7): + info(f"[_BaseDataInternal] Get calendar from database: {count} records") + trading_days = db.query(StockTradingCalendar.trade_date).filter_by( + is_trading_day=True + ).all() + db.close() + return [int(d[0]) for d in trading_days] + else: + info(f"[_BaseDataInternal] Database cache expired (last update: {latest_update}), fetching from SDK...") + else: + info(f"[_BaseDataInternal] No calendar data in database, fetching from SDK...") + db.close() + except Exception as db_err: + error(f"[_BaseDataInternal] Database query failed: {db_err}, fallback to SDK") + if 'db' in locals(): + db.close() + + # 2. 从SDK获取 try: - return self._base_data.get_calendar(market=market) + calendar = self._base_data.get_calendar(market=market) + info(f"[_BaseDataInternal] Got {len(calendar)} calendar days from SDK") + + # 3. 保存到数据库 + if DB_AVAILABLE and calendar: + try: + self._save_calendar_to_db(calendar, market) + except Exception as save_err: + error(f"[_BaseDataInternal] Failed to save calendar to db: {save_err}") + + return calendar except Exception as e: error(f"[_BaseDataInternal] Get calendar failed: {e}") return [] + def _save_calendar_to_db(self, calendar: List[int], market: str): + """将交易日历保存到数据库""" + db = SessionLocal() + try: + now = datetime.now() + saved_count = 0 + + for date_int in calendar: + date_str = str(date_int) + # 解析日期 + try: + date_obj = datetime.strptime(date_str, '%Y%m%d') + week_day = date_obj.weekday() + 1 # 1-7 + except: + week_day = None + + # 检查是否已存在 + existing = db.query(StockTradingCalendar).filter_by(trade_date=date_str).first() + if not existing: + cal_entry = StockTradingCalendar( + trade_date=date_str, + is_trading_day=True, + week_day=week_day, + created_at=now, + updated_at=now + ) + db.add(cal_entry) + saved_count += 1 + + db.commit() + info(f"[_BaseDataInternal] Saved {saved_count} new calendar days to database") + except Exception as e: + db.rollback() + raise e + finally: + db.close() + def get_adj_factor( self, code_list: List[str],