fix: 获取交易日和获取股票代码列表增加缓存查询操作

master
Lxy 3 months ago
parent 0b4c4eb233
commit a848993e7e

@ -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

@ -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],

Loading…
Cancel
Save