From 1bf91bdcd0456e669370b113c03f68ae6057224a Mon Sep 17 00:00:00 2001 From: aszerW Date: Sun, 24 May 2026 10:39:02 +0800 Subject: [PATCH] =?UTF-8?q?docs(framework=5Fv2):=20=E6=B7=BB=E5=8A=A0=20Fl?= =?UTF-8?q?askAPIFetcher=20=E6=96=87=E6=A1=A3=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 文档(2 个文档,互相关联) - FLASK_API_FETCHER_GUIDE.md - 使用指南(365 行) - 快速开始示例 - 完整 API 参考 - 结合 CrossMarketAligner 示例 - 错误处理 + 性能优化 - 注意事项(交易日历、净值数据量) - FLASK_API_FETCHER_ARCHITECTURE.md - 架构设计(367 行) - 架构层次图 - 设计原则(DIP, SRP, OCP) - 数据流图(指数、ETF) - 与 CrossMarketAligner 集成 - 未来优化方向(缓存、异步) ## 更新 - README.md: 添加文档链接(5 个文档) - 形成完整文档网络(6 个文档互链) --- .../FLASK_API_FETCHER_ARCHITECTURE.md | 366 ++++++++++++++++++ framework_v2/FLASK_API_FETCHER_GUIDE.md | 364 +++++++++++++++++ framework_v2/README.md | 1 + 3 files changed, 731 insertions(+) create mode 100644 framework_v2/FLASK_API_FETCHER_ARCHITECTURE.md create mode 100644 framework_v2/FLASK_API_FETCHER_GUIDE.md diff --git a/framework_v2/FLASK_API_FETCHER_ARCHITECTURE.md b/framework_v2/FLASK_API_FETCHER_ARCHITECTURE.md new file mode 100644 index 0000000..b49c2cb --- /dev/null +++ b/framework_v2/FLASK_API_FETCHER_ARCHITECTURE.md @@ -0,0 +1,366 @@ +# FlaskAPIFetcher 架构设计 + +## 定位 + +`FlaskAPIFetcher` 是 framework_v2 的**数据获取层实现**,连接抽象接口与线上 API 服务。 + +--- + +## 架构层次 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Strategy (策略层) │ +│ - RotationStrategy │ +│ - CCIScreener │ +└────────────────┬────────────────────────────────────────┘ + │ 调用 +┌────────────────▼────────────────────────────────────────┐ +│ DataFetcher (抽象接口) │ +│ framework_v2/core/data.py │ +│ - fetch_indices() [ABC] │ +│ - fetch_etf() [ABC] │ +│ - get_trading_calendar() [ABC] │ +└────────────────┬────────────────────────────────────────┘ + │ 继承 +┌────────────────▼────────────────────────────────────────┐ +│ FlaskAPIFetcher (具体实现) │ +│ framework_v2/shared/data/flask_api_fetcher.py │ +│ - fetch_indices() ✅ │ +│ - fetch_etf() ✅ │ +│ - get_trading_calendar() ✅ │ +│ - get_benchmark() ✅ │ +└────────────────┬────────────────────────────────────────┘ + │ 委托 +┌────────────────▼────────────────────────────────────────┐ +│ FlaskAPIDataSource (底层数据源) │ +│ datasource/flask_api_source.py │ +│ - fetch() - HTTP 请求 + 重试 │ +│ - fetch_batch() - 批量获取 │ +│ - validate_ohlcv_response() - Pydantic 验证 │ +└────────────────┬────────────────────────────────────────┘ + │ HTTP +┌────────────────▼────────────────────────────────────────┐ +│ Flask API Server (线上服务) │ +│ https://k3s.tokenpluse.xyz │ +│ - /api/v1/ohlcv - OHLCV 数据 │ +│ - /api/v1/etf/nav - ETF 净值 │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 设计原则 + +### 1. 依赖倒置原则(DIP) + +**策略层依赖抽象接口,不依赖具体实现**: + +```python +# ✅ 正确:策略依赖 DataFetcher 抽象 +class RotationStrategy: + def __init__(self, data_fetcher: DataFetcher): + self.fetcher = data_fetcher # 可以是任何实现 + +# 使用时注入具体实现 +strategy = RotationStrategy( + data_fetcher=FlaskAPIFetcher() # 或其他实现 +) +``` + +### 2. 单一职责原则(SRP) + +每个类只负责一件事: + +| 类 | 职责 | +|----|------| +| **DataFetcher** | 定义数据获取接口(抽象) | +| **FlaskAPIFetcher** | 实现接口,组织数据获取流程 | +| **FlaskAPIDataSource** | 处理 HTTP 请求、重试、验证 | + +### 3. 开闭原则(OCP) + +**对扩展开放,对修改封闭**: + +```python +# 未来可以添加新实现,无需修改 DataFetcher +class LocalCacheFetcher(DataFetcher): + """本地缓存实现""" + pass + +class DatabaseFetcher(DataFetcher): + """数据库实现""" + pass +``` + +--- + +## 数据流 + +### 获取指数数据 + +``` +RotationStrategy.fetch_data() + │ + ▼ +FlaskAPIFetcher.fetch_indices(codes, start, end) + │ + ├─ 遍历 codes + │ │ + │ ▼ + │ FlaskAPIDataSource.fetch(code, start, end, adj='raw') + │ │ + │ ├─ 构建 HTTP 请求 + │ ├─ 发送 GET /api/v1/ohlcv + │ ├─ 接收 JSON 响应 + │ ├─ Pydantic 验证 + │ └─ 返回 DataFrame + │ + └─ 收集所有 DataFrame + │ + ▼ + Dict[str, DataFrame] +``` + +### 获取 ETF 数据 + +``` +RotationStrategy.fetch_data() + │ + ▼ +FlaskAPIFetcher.fetch_etf(codes, start, end) + │ + ├─ 遍历 codes + │ │ + │ ▼ + │ FlaskAPIDataSource.fetch(code, start, end, + │ adj='raw', + │ asset_type='china_etf') + │ │ + │ ├─ 获取 OHLCV 数据 + │ ├─ 自动获取净值数据 + │ ├─ 自动计算溢价率 + │ └─ 返回 DataFrame(带 attrs) + │ ├─ df: 价格数据 + │ └─ df.attrs: + │ ├─ nav (净值 DataFrame) + │ ├─ premium_series (溢价率序列) + │ └─ latest_premium (最新溢价率) + │ + └─ 收集所有 DataFrame + │ + ▼ + Dict[str, DataFrame] +``` + +--- + +## 与 CrossMarketAligner 集成 + +### 完整数据流 + +``` +FlaskAPIFetcher + │ + ├─ fetch_indices() → 原始 OHLCV(不同市场日历) + │ ├─ 美股: ^GSPC (252 天/年) + │ ├─ 港股: ^HSI (250 天/年) + │ └─ A股: 000300.SH (244 天/年) + │ + ▼ +CrossMarketAligner + │ + ├─ align_returns() → 对齐收益率到目标日历 + │ ├─ 价格先 reindex + ffill + │ ├─ 再 pct_change()(避免 ffill 陷阱) + │ └─ 休市日收益率 = 0% + │ + ▼ +DataFrame(统一日历) + │ + ├─ ^GSPC: 244 天(A 股日历) + ├─ ^HSI: 244 天 + └─ 000300.SH: 244 天 + │ + ▼ +Factor Calculation(因子计算) + │ + ▼ +Signal Generation(信号生成) + │ + ▼ +Backtest Execution(回测执行) +``` + +### 示例代码 + +```python +from framework_v2.shared.data import FlaskAPIFetcher, CrossMarketAligner + +# 1. 获取数据 +fetcher = FlaskAPIFetcher() +data = fetcher.fetch_indices( + codes=["^GSPC", "000300.SH"], + start="2024-01-01", + end="2024-12-31" +) + +# 2. 获取目标日历 +target_calendar = fetcher.get_trading_calendar(market='A') + +# 3. 对齐收益率 +aligner = CrossMarketAligner(target_calendar=target_calendar) +returns = aligner.align_multi_asset( + close_dict={ + "SP500": data["^GSPC"]["close"], + "CSI300": data["000300.SH"]["close"], + } +) + +# 4. 验证对齐 +assert returns.isna().sum().sum() == 0, "不应该有 NaN" +print(f"✓ 对齐成功: {len(returns)} 天") +``` + +--- + +## 测试验证 + +### 测试覆盖 + +| 测试项 | 验证内容 | 状态 | +|--------|----------|------| +| 健康检查 | API 服务可用性 | ✅ 通过 | +| 指数数据 | OHLCV 结构、数据量 | ✅ 通过 | +| ETF 数据 | 价格 + 净值 + 溢价率 | ✅ 通过 | +| 交易日历 | 日期范围、天数 | ✅ 通过 | +| 基准数据 | Series 类型、数据量 | ✅ 通过 | + +### 运行测试 + +```bash +cd /Users/aszer/Documents/vscode/etf +python framework_v2/tests/test_flask_api_fetcher.py +``` + +**测试结果**: +``` +✓ 测试通过 - 健康检查 +✓ 测试通过 - 指数数据 +✓ 测试通过 - ETF 数据 +✓ 测试通过 - 交易日历 +✓ 测试通过 - 基准数据 + +总计: 5/5 通过 +``` + +--- + +## 优势总结 + +### vs 直接使用 FlaskAPIDataSource + +| 特性 | FlaskAPIDataSource | FlaskAPIFetcher | +|------|-------------------|-----------------| +| **抽象接口** | ❌ 无 | ✅ 继承 DataFetcher | +| **批量获取** | ✅ fetch_batch() | ✅ fetch_indices() | +| **进度显示** | ❌ 无 | ✅ 自动显示 | +| **错误处理** | ✅ 基础 | ✅ 增强(验证 + 重试) | +| **策略集成** | ❌ 需适配 | ✅ 直接使用 | +| **测试覆盖** | ❌ 无 | ✅ 5/5 通过 | + +### vs 旧架构 + +| 特性 | 旧架构(datasource/) | 新架构(framework_v2/) | +|------|----------------------|------------------------| +| **抽象接口** | ❌ 无 | ✅ DataFetcher ABC | +| **Schema 验证** | ⚠️ 部分 | ✅ Pydantic 完整验证 | +| **跨市场对齐** | ❌ 无 | ✅ CrossMarketAligner | +| **分层设计** | ❌ 混合 | ✅ core/shared/strategy | +| **可测试性** | ⚠️ 困难 | ✅ 依赖注入 | +| **文档** | ❌ 缺失 | ✅ 完整文档 | + +--- + +## 未来优化 + +### 1. 交易日历准确性 + +**当前问题**:使用 pandas `bdate_range` 生成近似日历,未考虑节假日。 + +**优化方案**: +```python +# TODO: 通过 API 获取准确日历 +def get_trading_calendar(self, market: str) -> pd.Index: + # 1. 调用 API 端点 + # 2. 或从数据库查询 + # 3. 或加载本地日历文件 + pass +``` + +### 2. 缓存机制 + +**当前问题**:每次请求都调用 API,重复获取相同数据。 + +**优化方案**: +```python +# TODO: 添加本地缓存 +class FlaskAPIFetcher(DataFetcher): + def __init__(self, cache_dir: str = "data_cache"): + self.cache = LocalCache(cache_dir) + + def fetch_indices(self, codes, start, end): + # 1. 检查缓存 + cached = self.cache.get(codes, start, end) + if cached: + return cached + + # 2. 调用 API + data = self._source.fetch_batch(...) + + # 3. 写入缓存 + self.cache.set(codes, start, end, data) + + return data +``` + +### 3. 异步支持 + +**当前问题**:批量获取串行执行,效率低。 + +**优化方案**: +```python +# TODO: 使用 aiohttp 异步获取 +async def fetch_indices_async(self, codes, start, end): + async with aiohttp.ClientSession() as session: + tasks = [ + self._fetch_single(session, code, start, end) + for code in codes + ] + results = await asyncio.gather(*tasks) + return dict(zip(codes, results)) +``` + +--- + +## 相关文件 + +| 文件 | 说明 | +|------|------| +| `framework_v2/core/data.py` | DataFetcher 抽象基类 | +| `framework_v2/shared/data/flask_api_fetcher.py` | FlaskAPIFetcher 实现 | +| `framework_v2/shared/data/__init__.py` | 导出 FlaskAPIFetcher | +| `framework_v2/tests/test_flask_api_fetcher.py` | 测试套件 | +| `datasource/flask_api_source.py` | 底层 HTTP 数据源 | +| `FLASK_API_FETCHER_GUIDE.md` | 使用指南 | + +--- + +## 版本历史 + +- **2024-04-16**: 初始版本 + - 继承 DataFetcher 抽象基类 + - 实现指数、ETF 数据获取 + - 集成 FlaskAPIDataSource + - 5/5 测试通过 + - 完整文档 diff --git a/framework_v2/FLASK_API_FETCHER_GUIDE.md b/framework_v2/FLASK_API_FETCHER_GUIDE.md new file mode 100644 index 0000000..8417d5d --- /dev/null +++ b/framework_v2/FLASK_API_FETCHER_GUIDE.md @@ -0,0 +1,364 @@ +# FlaskAPIFetcher 使用指南 + +## 概述 + +`FlaskAPIFetcher` 是 framework_v2 的数据获取器实现,通过 HTTP API 获取线上数据(指数、ETF)。 + +**核心优势**: +- ✅ 无需本地 SSH 隧道配置 +- ✅ 支持远程调用(生产环境) +- ✅ 自动重试 + 超时处理 +- ✅ Pydantic Schema 验证响应 +- ✅ ETF 数据自动附加净值和溢价率 + +--- + +## 快速开始 + +### 1. 基础使用 + +```python +from framework_v2.shared.data import FlaskAPIFetcher + +# 创建数据获取器 +fetcher = FlaskAPIFetcher( + base_url="https://k3s.tokenpluse.xyz", # 或从环境变量读取 + timeout=120, + retries=3 +) + +# 获取指数数据 +data = fetcher.fetch_indices( + codes=["000300.SH", "000905.SH"], + start="2024-01-01", + end="2024-12-31" +) + +# 访问数据 +df_300 = data["000300.SH"] +print(df_300.head()) +``` + +### 2. 获取 ETF 数据 + +```python +# 获取 ETF 数据(自动附加净值和溢价率) +data = fetcher.fetch_etf( + codes=["510300.SH", "159919.SZ"], + start="2024-01-01", + end="2024-12-31" +) + +# 访问价格数据 +df = data["510300.SH"] +print(df.head()) + +# 访问净值数据 +nav = df.attrs.get('nav') +if nav is not None: + print(f"净值数据: {len(nav)} 条") + +# 访问溢价率 +premium = df.attrs.get('latest_premium') +if premium is not None: + print(f"最新溢价率: {premium:.2f}%") +``` + +--- + +## 完整示例:结合 CrossMarketAligner + +### 场景:获取跨市场数据并对齐到 A 股日历 + +```python +from framework_v2.shared.data import FlaskAPIFetcher, CrossMarketAligner + +# 1. 创建数据获取器 +fetcher = FlaskAPIFetcher() + +# 2. 获取 A 股交易日历 +a_share_calendar = fetcher.get_trading_calendar(market='A') + +# 3. 创建对齐器 +aligner = CrossMarketAligner(target_calendar=a_share_calendar) + +# 4. 获取跨市场指数数据 +us_indices = fetcher.fetch_indices( + codes=["^GSPC", "^IXIC"], # 美股 + start="2024-01-01", + end="2024-12-31" +) + +cn_indices = fetcher.fetch_indices( + codes=["000300.SH", "000905.SH"], # A股 + start="2024-01-01", + end="2024-12-31" +) + +# 5. 对齐收益率到 A 股日历 +returns_aligned = aligner.align_multi_asset( + close_dict={ + "SP500": us_indices["^GSPC"]["close"], + "NASDAQ": us_indices["^IXIC"]["close"], + "CSI300": cn_indices["000300.SH"]["close"], + "CSI500": cn_indices["000905.SH"]["close"], + } +) + +# 6. 验证对齐结果 +print(returns_aligned.head()) +print(f"\nNaN 数量: {returns_aligned.isna().sum().sum()}") # 应该为 0 +``` + +--- + +## API 参考 + +### FlaskAPIFetcher + +#### 初始化 + +```python +FlaskAPIFetcher( + base_url: str = None, # API 地址(默认从环境变量读取) + timeout: int = 120, # 请求超时时间(秒) + retries: int = 3 # 重试次数 +) +``` + +#### 核心方法 + +| 方法 | 说明 | 返回类型 | +|------|------|----------| +| `fetch_indices(codes, start, end)` | 获取指数 OHLCV 数据 | `Dict[str, DataFrame]` | +| `fetch_etf(codes, start, end)` | 获取 ETF 数据(价格+净值) | `Dict[str, DataFrame]` | +| `get_trading_calendar(market)` | 获取交易日历 | `pd.Index` | +| `get_benchmark(code, start, end)` | 获取基准数据 | `pd.Series` | +| `get_health()` | 检查 API 健康状态 | `Dict` | + +#### fetch_indices 参数 + +```python +fetcher.fetch_indices( + codes=["000300.SH", "000905.SH"], # 指数代码列表 + start="2024-01-01", # 开始日期 + end="2024-12-31" # 结束日期 +) +``` + +**返回 DataFrame 结构**: +``` + code open high low close volume +date +2024-01-02 000300.SH 3388.30 3395.40 3372.50 3390.20 12345678 +2024-01-03 000300.SH 3390.20 3405.60 3385.10 3398.50 13456789 +``` + +#### fetch_etf 参数 + +```python +fetcher.fetch_etf( + codes=["510300.SH", "159919.SZ"], # ETF 代码列表 + start="2024-01-01", # 开始日期 + end="2024-12-31" # 结束日期 +) +``` + +**返回 DataFrame 结构**: +``` + code open high low close volume +date +2024-01-02 510300.SH 3.520 3.545 3.510 3.540 45678901 +``` + +**附加信息(df.attrs)**: +- `nav`: 净值数据 DataFrame +- `premium_series`: 溢价率序列(dict) +- `latest_premium`: 最新溢价率(float) +- `premium_stats`: 溢价率统计(dict) + +--- + +## 与 DataFetcher 抽象基类的关系 + +``` +framework_v2/core/data.py # 抽象基类 + └── DataFetcher (ABC) + ├── fetch_indices() [抽象] + ├── fetch_etf() [抽象] + ├── get_trading_calendar() [抽象] + └── get_benchmark() [可选] + +framework_v2/shared/data/flask_api_fetcher.py # 具体实现 + └── FlaskAPIFetcher(DataFetcher) + ├── fetch_indices() ✅ 实现(调用 FlaskAPIDataSource) + ├── fetch_etf() ✅ 实现(调用 FlaskAPIDataSource) + ├── get_trading_calendar() ✅ 实现(临时:pandas BDay) + └── get_benchmark() ✅ 实现 +``` + +### 继承关系验证 + +```python +from framework_v2.core.data import DataFetcher +from framework_v2.shared.data import FlaskAPIFetcher + +# 验证继承 +assert issubclass(FlaskAPIFetcher, DataFetcher) + +# 验证抽象方法已实现 +fetcher = FlaskAPIFetcher() +assert hasattr(fetcher, 'fetch_indices') +assert hasattr(fetcher, 'fetch_etf') +assert hasattr(fetcher, 'get_trading_calendar') +``` + +--- + +## 环境变量配置 + +### FLASK_API_URL + +```bash +# .env 文件 +FLASK_API_URL=https://k3s.tokenpluse.xyz +``` + +**优先级**: +1. 构造函数参数 `base_url` +2. 环境变量 `FLASK_API_URL` +3. 默认值 `https://k3s.tokenpluse.xyz` + +--- + +## 错误处理 + +### 自动重试 + +```python +fetcher = FlaskAPIFetcher(retries=3) + +# 失败时自动重试: +# - 网络超时 +# - HTTP 5xx 错误 +# - JSON 解析失败 +``` + +### 手动错误处理 + +```python +data = fetcher.fetch_indices(["000300.SH"], "2024-01-01", "2024-12-31") + +if "000300.SH" not in data: + print("✗ 数据获取失败") + # 处理错误... +else: + print(f"✓ 获取 {len(data['000300.SH'])} 条数据") +``` + +--- + +## 性能优化 + +### 批量获取 vs 单个获取 + +```python +# ✅ 推荐:批量获取(内部自动重试 + 进度显示) +data = fetcher.fetch_indices( + codes=["000300.SH", "000905.SH", "000852.SH"], + start="2024-01-01", + end="2024-12-31" +) + +# ❌ 不推荐:循环单个获取(无进度显示) +for code in codes: + df = fetcher._source.fetch(code, start, end) +``` + +### 超时设置 + +```python +# 网络较慢时增加超时 +fetcher = FlaskAPIFetcher(timeout=180) # 3 分钟 +``` + +--- + +## 测试 + +运行测试验证功能: + +```bash +cd /Users/aszer/Documents/vscode/etf +python framework_v2/tests/test_flask_api_fetcher.py +``` + +**预期输出**: +``` +✓ 测试通过 - 健康检查 +✓ 测试通过 - 指数数据 +✓ 测试通过 - ETF 数据 +✓ 测试通过 - 交易日历 +✓ 测试通过 - 基准数据 + +总计: 5/5 通过 +``` + +--- + +## 相关文档 + +- **[框架总览](../README.md)** - framework_v2 架构说明 +- **[数据架构方案](../DATA_ARCHITECTURE.md)** - 数据流设计 +- **[跨市场对齐方案](../ALIGNMENT_GUIDE.md)** - CrossMarketAligner 使用 +- **[Aligner + Schema 整合](../ALIGNMENT_SCHEMA_INTEGRATION.md)** - 验证架构 + +--- + +## 注意事项 + +### 1. 交易日历准确性 + +当前 `get_trading_calendar()` 使用 pandas `bdate_range` 生成近似日历,**未考虑节假日**。 + +**临时方案**: +```python +calendar = fetcher.get_trading_calendar(market='A') +# 手动移除节假日 +holidays = pd.to_datetime(['2024-02-10', '2024-10-01', ...]) +calendar = calendar[~calendar.isin(holidays)] +``` + +**TODO**:后续通过 API 端点获取准确日历。 + +### 2. ETF 净值数据量 + +ETF 净值数据可能远多于价格数据(历史净值 vs 交易价格): + +```python +df = data["510300.SH"] +print(f"价格: {len(df)} 条") # ~60 条(2024 Q1) +print(f"净值: {len(df.attrs['nav'])} 条") # ~3695 条(全历史) +``` + +### 3. 资产类型检测 + +FlaskAPIDataSource 支持自动检测资产类型,也可手动指定: + +```python +# 自动检测 +df = fetcher._source.fetch("510300.SH", start, end) + +# 手动覆盖 +df = fetcher._source.fetch("510300.SH", start, end, asset_type='china_etf') +``` + +--- + +## 版本历史 + +- **2024-04-16**: 初始版本 + - 继承 DataFetcher 抽象基类 + - 实现指数、ETF 数据获取 + - 集成 FlaskAPIDataSource + - 5/5 测试通过 diff --git a/framework_v2/README.md b/framework_v2/README.md index 74d4844..8c283cb 100644 --- a/framework_v2/README.md +++ b/framework_v2/README.md @@ -26,6 +26,7 @@ framework_v2/ - **[跨市场对齐方案](ALIGNMENT_GUIDE.md)** - CrossMarketAligner 使用指南 - **[数据流完整推演](DATA_FLOW_DEMO.md)** - 从 OHLCV 到最终收益的 7 个阶段推演 - **[Aligner + Schema 整合方案](ALIGNMENT_SCHEMA_INTEGRATION.md)** - Pydantic Schema 与对齐器结合使用 +- **[FlaskAPIFetcher 使用指南](FLASK_API_FETCHER_GUIDE.md)** - 通过 HTTP API 获取线上数据 ---