# Option Chain Scheduler Implementation
## 概述
已成功为 `ibkr-mcp` 项目实现了期权链周期性抓取功能,类似于 `optionscanner` 项目的调度机制。
## 功能特性
### ✅ 核心功能
- **自动调度**:在指定时间点(9:30、11:00、13:30、15:45)自动抓取期权链数据
- **动态配置**:支持通过 MCP 工具实时修改调度时间、时区、监控股票代码
- **符号管理**:自动从 positions 获取 underlying,也支持手动添加股票代码
- **生命周期管理**:server 启动时自动启动调度器,关闭时优雅停止
### ✅ 调度机制
- **时间计算**:自定义时间计算逻辑(无需第三方依赖)
- **时区支持**:使用 `zoneinfo.ZoneInfo` 处理时区转换
- **错误恢复**:调度器异常时自动恢复,继续运行
- **优雅重启**:配置变更时优雅重启调度器
### ✅ 配置管理
- **统一配置**:使用 `config.yaml` 统一存储所有配置
- **内存优先**:默认修改仅在内存中生效,server 重启后丢失
- **可选持久化**:通过 `persist=True` 参数将修改同步到文件
- **默认配置**:无配置文件时自动使用默认配置
## 文件修改清单
### 1. `src/ibkr_mcp/services/market_data.py`
**新增功能**:
- 调度器工具函数:`parse_schedule_times()`、`compute_next_run()`、`validate_schedule_config()`
- 配置管理方法:`_load_schedule_config()`、`_save_schedule_config()`
- 符号管理方法:`_get_symbols_from_positions()`、`_get_all_symbols()`、`set_position_store()`
- 调度器控制方法:`start_scheduler()`、`stop_scheduler()`、`restart_scheduler()`
- 手动执行方法:`run_once()`
- 核心调度循环:`_run_scheduler()`
- 配置修改方法:`update_schedule_times()`、`add_schedule_symbol()`、`remove_schedule_symbol()`、`set_schedule_timezone()`
- 状态查询方法:`get_schedule_status()`、`get_schedule_config()`、`reset_schedule_config()`
### 2. `src/ibkr_mcp/server.py`
**修改内容**:
- `IBKRContext.__post_init__()`:添加 position_store 引用设置
- `ibkr_lifespan()`:在 server 启动时自动启动调度器,关闭时停止调度器
### 3. `config.yaml`(新建)
**配置内容**:
```yaml
schedule:
enabled: true
times:
- "09:30"
- "11:00"
- "13:30"
- "15:45"
timezone: "America/New_York"
symbols: []
mode: "live"
market_data_type: "LIVE"
```
### 4. `src/ibkr_mcp/api/tools.py`
**新增 MCP 工具**:
- `get_schedule_status` - 获取调度器状态
- `start_option_chain_scheduler` - 启动调度器
- `stop_option_chain_scheduler` - 停止调度器
- `restart_option_chain_scheduler` - 重启调度器
- `run_option_chain_fetch_now` - 立即执行一次
- `update_schedule_times` - 更新调度时间
- `add_schedule_symbol` - 添加监控股票
- `remove_schedule_symbol` - 删除监控股票
- `set_schedule_timezone` - 设置时区
- `get_schedule_config` - 获取配置
- `reset_schedule_config` - 重置为默认配置
## 使用方法
### 启动时自动调度
Server 启动时,如果 `config.yaml` 中 `schedule.enabled = true`,调度器会自动启动。
### 手动控制调度器
```python
# 启动调度器
await start_option_chain_scheduler()
# 停止调度器
await stop_option_chain_scheduler()
# 重启调度器
await restart_option_chain_scheduler()
# 立即执行一次(非阻塞,立即返回)
result = await run_option_chain_fetch_now(symbols=["SPY", "QQQ"])
# 返回:
# {
# "ok": true,
# "message": "Manual fetch started for 2 symbols (running in background)",
# "symbol_count": 2,
# "symbols": ["SPY", "QQQ"],
# "task_id": 1234567890,
# "mode": "live",
# "started_at": "2025-12-17T10:30:00-05:00"
# }
# 实际抓取在后台进行,查看日志获取完成状态
```
### 修改调度时间
```python
# 更新为新时间点
await update_schedule_times(
times=["09:30", "10:30", "13:00", "15:45"],
persist=True # 同时保存到 config.yaml
)
```
### 管理监控股票
```python
# 添加股票
await add_schedule_symbol(symbol="TSLA", persist=False)
# 删除股票
await remove_schedule_symbol(symbol="AAPL", persist=True)
# 查询状态
status = await get_schedule_status()
# 返回:
# {
# "enabled": true,
# "times": ["09:30", "11:00", "13:30", "15:45"],
# "symbols": ["SPY", "QQQ", "TSLA"],
# "position_symbols": ["SPY", "QQQ"],
# "config_symbols": ["TSLA"],
# "timezone": "America/New_York",
# "next_run": "2025-12-17T09:30:00-05:00",
# "last_run": "2025-12-16T15:45:00-05:00",
# "run_count": 24,
# "status": "running"
# }
```
### 设置时区
```python
# 设置为太平洋时间
await set_schedule_timezone(
timezone="America/Los_Angeles",
persist=True
)
```
## 调度逻辑
### 启动流程
1. Server 启动
2. 加载 `config.yaml` 中的 `schedule` 段
3. 验证配置有效性
4. 获取监控股票列表(positions + config)
5. 启动后台调度任务
### 调度循环
1. 计算下一次执行时间
2. 异步等待到执行时间
3. 执行 `fetch_option_chains()`
4. 更新统计信息
5. 循环到步骤 1
### 停止流程
1. 接收停止信号
2. 取消后台任务
3. 等待任务完成
4. 清理资源
## 配置优先级
### 股票代码优先级
1. **Positions 中的 underlying**(自动检测)
2. **Config 中手动指定的 symbols**(手动添加)
### 时区处理
- 默认时区:`America/New_York`(美东时间)
- 支持所有 IANA 时区名称
- 自动处理夏令时切换
## 错误处理
### 配置验证
- 时间格式验证(HH:MM)
- 时区有效性验证
- 模式验证(live/local)
### 异常恢复
- 调度器异常时记录日志并继续运行
- 配置加载失败时使用默认配置
- 文件写入失败时仅内存配置生效
### 优雅重启
- 配置变更时自动重启调度器
- 重启前先停止旧任务
- 启动新任务后验证成功
## 与 optionscanner 的对比
| 特性 | optionscanner | ibkr-mcp |
|------|---------------|----------|
| 配置文件 | 独立的 `config.yaml` | 统一的 `config.yaml` |
| 调度方式 | 时间点调度 | 时间点调度 |
| 符号来源 | 配置文件指定 | positions + 配置文件 |
| 持久化 | 自动保存 | 可选保存(persist 参数) |
| MCP 工具 | 无 | 完整的 MCP 工具集 |
| 时区支持 | America/Los_Angeles | 可配置任意时区 |
| 手动执行 | 不支持 | 支持 run_once |
| 实时修改 | 不支持 | 支持实时修改配置 |
## 示例场景
### 场景1:启动时自动加载
```
1. 启动 ibkr-mcp server
2. 自动加载 config.yaml
3. 从 positions 获取 SPY、QQQ
4. 启动调度器
5. 在 9:30、11:00、13:30、15:45 自动抓取
```
### 场景2:动态添加股票
```python
# 添加 TSLA 到监控列表
await add_schedule_symbol("TSLA", persist=False)
# 下次调度时自动包含 TSLA
```
### 场景3:修改交易时间
```python
# 改为上午 10 点和下午 2 点
await update_schedule_times(
times=["10:00", "14:00"],
persist=True
)
# 调度器自动重启,应用新时间
```
### 场景4:立即手动抓取
```python
# 立即触发抓取,但不等待完成
result = await run_option_chain_fetch_now()
# 立即返回:
# {"ok": true, "message": "Manual fetch started for 3 symbols (running in background)"}
# 实际抓取在后台异步进行
```
### 场景5:查询运行状态
```python
status = await get_schedule_status()
print(f"调度器状态: {status['status']}")
print(f"下次运行: {status['next_run']}")
print(f"已运行次数: {status['run_count']}")
```
### 场景6:后台抓取示例
```python
# 非阻塞执行,立即返回
result = await run_option_chain_fetch_now(symbols=["AAPL", "MSFT"])
print(f"任务已启动: {result['message']}")
print(f"抓取 {result['symbol_count']} 个股票")
print(f"任务ID: {result['task_id']}")
# 抓取在后台继续进行...
```
## 注意事项
1. **依赖项**:确保安装了 `pyyaml` 和 `zoneinfo`(Python 3.9+ 内置)
2. **时区**:确保系统时区设置正确,建议显式指定时区
3. **市场数据**:live 模式需要有效的 IBKR 市场数据订阅
4. **配置文件**:修改 config.yaml 后需要重启 server 或调用重启工具
5. **内存使用**:调度器在后台运行,长期运行会累积数据文件
6. **非阻塞执行**:`run_option_chain_fetch_now` 立即返回,抓取在后台异步进行
7. **任务监控**:后台抓取任务的完成状态通过日志记录,无直接查询接口
## 总结
成功实现了完整的期权链周期性抓取功能,具备以下优势:
✅ **自动调度、手动功能完整**:支持控制、实时配置修改
✅ **易于使用**:提供直观的 MCP 工具接口
✅ **灵活配置**:支持动态修改和持久化
✅ **稳定可靠**:完善的错误处理和恢复机制
✅ **统一管理**:所有配置集中在 config.yaml 中
✅ **非阻塞执行**:手动抓取立即返回,后台异步执行
✅ **可扩展性**:基于 asyncio,易于扩展新功能
实现参考了 `optionscanner` 的调度机制,但增加了更多灵活性和易用性,适合生产环境使用。
### 主要变更
**最新更新(v1.1)**:修改 `run_option_chain_fetch_now` 为非阻塞执行
- **之前**:等待所有期权链抓取完成才返回
- **现在**:立即返回任务启动确认,抓取在后台异步进行
- **优势**:提高响应速度,适合实时交互场景
**计划中功能**:Snapshot Status 工具
- `list_option_snapshots` - 列出最近的快照(待实现)
- `get_snapshot_status` - 查看特定快照的详细状态(待实现)
- `check_snapshot_freshness` - 检查所有快照的新鲜度(待实现)