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 7cc299f..e39274a 100644 Binary files a/app/adapters/__pycache__/internal_data_service.cpython-311.pyc and b/app/adapters/__pycache__/internal_data_service.cpython-311.pyc differ diff --git a/app/adapters/internal_data_service.py b/app/adapters/internal_data_service.py index d6aa893..5ff0dbf 100644 --- a/app/adapters/internal_data_service.py +++ b/app/adapters/internal_data_service.py @@ -4,12 +4,22 @@ 对外接口不直接调用 SDK,而是通过内部接口调用 """ import pandas as pd -from datetime import datetime +from datetime import datetime, timedelta from typing import List, Optional, Dict, Any from app.core.logger import info, error +# 数据库相关导入(可选,如果数据库未配置则回退到SDK) +try: + from app.repositories.database import SessionLocal + from app.repositories.models import StockSymbol, StockTradingCalendar + from sqlalchemy import func + DB_AVAILABLE = True +except ImportError: + DB_AVAILABLE = False + + class _MarketDataInternal: """市场数据内部接口 - 封装 _market_data""" @@ -80,13 +90,87 @@ class _BaseDataInternal: self._base_data = base_data def get_code_list(self, security_type: str) -> 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],