experiment(rotation): 同大类扩充与纳指vs标普替换对比实验
技术修复: - SOCKS5代理IPv6问题:socks5:// → socks5h:// (hybrid_source.py, yfinance_source.py) 目录整理: - scripts/ → 仅保留策略入口(daily_scheduler, run_rotation, run_cci_screener) - 实验脚本移至 tests/experiments/ - 工具脚本移至 tests/utils/ - 实验记录新增 docs/experiments/ - results/ 添加到 gitignore 实验结果: 实验001 - 同大类扩充(添加标普500): ├─ 累计收益: 1467.35% → 1176.26% (-291%) ├─ CAGR: 48.10% → 43.82% (-4.28%) ├─ 调仓次数: 459 → 501 (+42次) └─ 结论: 添加同大类标的不增加跨类分散,反而侵蚀收益 实验002 - 纳指vs标普替换对比: ├─ 累计收益: 1467.35% → 1118.77% (-348%) ├─ CAGR: 48.10% → 42.87% (-5.22%) ├─ Sharpe: 2.21 → 2.08 (-0.13) ├─ MaxDD: -17.33% → -15.14% (+2.18%) └─ 结论: 纳指100优于标普500,成长风格更适合动量策略 策略建议: - 保持纳指100作为美股大类代表 - 不添加同大类新标的(避免类内切换成本) - 新增标的应优先考虑新大类(增加跨类分散)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -199,3 +199,6 @@ report*.png
|
||||
|
||||
# Downloaded articles
|
||||
zhihu-articles/
|
||||
|
||||
# Results directory (test outputs, charts, etc.)
|
||||
results/
|
||||
|
||||
@@ -72,7 +72,8 @@ class SSHTunnelManager:
|
||||
return False
|
||||
|
||||
# 设置代理环境变量
|
||||
proxy_url = f"socks5://127.0.0.1:{self.local_port}"
|
||||
# 使用 socks5h:// 让代理服务器远程解析DNS,避免IPv6问题
|
||||
proxy_url = f"socks5h://127.0.0.1:{self.local_port}"
|
||||
os.environ["HTTP_PROXY"] = proxy_url
|
||||
os.environ["HTTPS_PROXY"] = proxy_url
|
||||
os.environ["ALL_PROXY"] = proxy_url
|
||||
|
||||
@@ -75,7 +75,8 @@ class SSHTunnelManager:
|
||||
return False
|
||||
|
||||
# 设置代理环境变量
|
||||
proxy_url = f"socks5://127.0.0.1:{self.local_port}"
|
||||
# 使用 socks5h:// 让代理服务器远程解析DNS,避免IPv6问题
|
||||
proxy_url = f"socks5h://127.0.0.1:{self.local_port}"
|
||||
os.environ["HTTP_PROXY"] = proxy_url
|
||||
os.environ["HTTPS_PROXY"] = proxy_url
|
||||
os.environ["ALL_PROXY"] = proxy_url
|
||||
|
||||
197
docs/experiments/001_same_category_expansion_ab_test.md
Normal file
197
docs/experiments/001_same_category_expansion_ab_test.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# 实验记录 001: 同大类扩充对轮动策略的影响
|
||||
|
||||
## 实验信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 实验编号 | 001 |
|
||||
| 实验日期 | 2026-05-06 |
|
||||
| 实验类型 | A/B对比测试 |
|
||||
| 研究问题 | `diversified=true`模式下,添加同大类新标地对策略绩效的影响 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 实验背景
|
||||
|
||||
### 理论假设
|
||||
|
||||
`diversified=true` 模式的选股逻辑:
|
||||
|
||||
```
|
||||
Step 1: 类内竞争 → 每个 market 大类只保留得分最高的1只标的(大类冠军)
|
||||
Step 2: 跨类排序 → 从大类冠军中按得分从高到低选 Top 3
|
||||
```
|
||||
|
||||
**核心假设**:
|
||||
- 添加同大类新标的不会增加跨大类分散度(每大类还是只输出1只)
|
||||
- 可能增加类内切换频率,导致额外调仓成本
|
||||
- 额外切换时机可能不理想,侵蚀收益
|
||||
|
||||
---
|
||||
|
||||
## 2. 实验设计
|
||||
|
||||
### A/B组配置
|
||||
|
||||
| 组别 | 标的数量 | 美股大类标的 | 其他大类 |
|
||||
|------|---------|-------------|---------|
|
||||
| **A组(对照组)** | 11只 | 纳指100 (NDX) | A股2、港股2、日本1、欧洲1、商品3、固收1 |
|
||||
| **B组(实验组)** | 12只 | 纳指100 + 标普500 (SPX) | 同A组 |
|
||||
|
||||
### 关键差异
|
||||
|
||||
B组在美股大类(market="US")中添加了标普500:
|
||||
- A组:美股大类只有纳指100,自动成为大类冠军
|
||||
- B组:美股大类有纳指100和标普500,需要类内竞争决定冠军
|
||||
|
||||
---
|
||||
|
||||
## 3. 回测结果
|
||||
|
||||
### 数据获取情况
|
||||
|
||||
修复了 `socks5://` → `socks5h://` 的代理问题后,所有 YFinance 数据成功获取:
|
||||
|
||||
```
|
||||
✓ SSH 隧道已建立: socks5h://127.0.0.1:1080
|
||||
下载 NDX (纳指100) - YFinance... ✓ 1845 条
|
||||
下载 SPX (标普500) - YFinance... ✓ 1845 条
|
||||
其他标的均成功获取
|
||||
```
|
||||
|
||||
### 绩效对比
|
||||
|
||||
| 指标 | A组(无SPX) | B组(有SPX) | 差异 |
|
||||
|------|-------------|-------------|------|
|
||||
| 标的数量 | 11只 | 12只 | +1 |
|
||||
| **累计收益** | **1467.35%** | 1176.26% | **-291.09%** |
|
||||
| **CAGR** | **48.10%** | 43.82% | **-4.28%** |
|
||||
| **Sharpe** | **2.21** | 2.06 | **-0.15** |
|
||||
| MaxDD | -17.33% | -17.18% | +0.14%(略好) |
|
||||
| **Calmar** | **2.78** | 2.55 | **-0.23** |
|
||||
| 日胜率 | 56.45% | 56.11% | -0.34% |
|
||||
| **调仓次数** | 459次 | 501次 | **+42次** |
|
||||
| 年均调仓 | 66.0次 | 72.1次 | +6.1次 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键发现
|
||||
|
||||
### 发现1:跨类分散不变
|
||||
|
||||
添加标普500后,美股大类在最终持仓中的占比不变:
|
||||
- 美股大类始终只有1只冠军进入Top3候选池
|
||||
- 跨大类分散度没有增加
|
||||
|
||||
### 发现2:调仓次数增加
|
||||
|
||||
- B组调仓次数增加42次(从459→501)
|
||||
- 类内切换更频繁(纳指100 ↔ 标普500)
|
||||
- 额外调仓成本侵蚀收益
|
||||
|
||||
### 发现3:绩效反而变差
|
||||
|
||||
```
|
||||
B组绩效全面下滑:
|
||||
├─ 累计收益 -291%
|
||||
├─ CAGR -4.28%
|
||||
├─ Sharpe -0.15
|
||||
├─ Calmar -0.23
|
||||
└─ 原因:类内切换时机不佳 + 额外调仓成本
|
||||
```
|
||||
|
||||
### 发现4:类内切换逻辑
|
||||
|
||||
美股大类竞争示例:
|
||||
```
|
||||
某日得分:
|
||||
纳指100: 4.7 → 美股冠军(持有纳指ETF)
|
||||
标普500: 3.5 → 淘汰
|
||||
|
||||
另一天得分:
|
||||
纳指100: -1.0(下跌)
|
||||
标普500: 2.5 → 美股冠军(切换到标普ETF)
|
||||
|
||||
问题:切换时机可能滞后,错过最佳窗口
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 实验结论
|
||||
|
||||
### 核心结论
|
||||
|
||||
| 假设 | 实证结果 |
|
||||
|-----|---------|
|
||||
| 添加同大类标的**不增加跨类分散** | ✓ **验证通过** |
|
||||
| 可能**增加调仓次数** | ✓ **验证通过**(+42次) |
|
||||
| 额外切换**可能侵蚀收益** | ✓ **验证通过**(累计收益-291%) |
|
||||
|
||||
### 策略建议
|
||||
|
||||
```
|
||||
diversified=true 模式下的标的池优化策略:
|
||||
|
||||
✗ 不要盲目添加同大类新标的
|
||||
→ 可能增加切换频率,侵蚀收益
|
||||
→ 每大类保持1-2只代表性标的即可
|
||||
|
||||
✓ 应该添加新大类(增加跨类分散)
|
||||
→ 印度、越南、短债等新大类
|
||||
→ 真正扩大 Top 3 候选池
|
||||
→ 提升跨大类分散度
|
||||
|
||||
✓ 类内标的选择原则
|
||||
→ 选择该大类最具代表性的标的
|
||||
→ 避免风格过度细分导致频繁切换
|
||||
→ 例:美股选纳指100即可(成长代表)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 技术修复记录
|
||||
|
||||
### 代理问题修复
|
||||
|
||||
本次实验过程中发现了 SSH SOCKS5 隧道的 IPv6 问题:
|
||||
|
||||
**问题**:
|
||||
```python
|
||||
# 原配置(失败)
|
||||
proxy_url = "socks5://127.0.0.1:1080"
|
||||
# 本地DNS解析 → IPv6地址 → SSH隧道拒绝IPv6 → 连接失败
|
||||
```
|
||||
|
||||
**修复**:
|
||||
```python
|
||||
# 新配置(成功)
|
||||
proxy_url = "socks5h://127.0.0.1:1080"
|
||||
# 'h'表示远程DNS解析 → 代理服务器只用IPv4 → 连接成功
|
||||
```
|
||||
|
||||
**修改文件**:
|
||||
- `core/datasource/hybrid_source.py`
|
||||
- `core/datasource/yfinance_source.py`
|
||||
|
||||
---
|
||||
|
||||
## 7. 相关文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|-----|------|
|
||||
| `tests/experiments/ab_test_spx.py` | A/B测试脚本 |
|
||||
| `results/ab_test_spx.csv` | 测试结果数据 |
|
||||
| `docs/轮动策略核心逻辑_v2.md` | 策略核心逻辑文档 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 后续研究方向
|
||||
|
||||
1. **新大类扩充实验**:添加印度NIFTY、短债等新大类,验证跨类分散效果
|
||||
2. **类内切换时机分析**:深入分析纳指100 vs 标普500切换的具体时间点
|
||||
3. **最佳大类数量研究**:多少个大类是最优配置?
|
||||
|
||||
---
|
||||
|
||||
*实验记录版本: v1.0*
|
||||
*最后更新: 2026-05-06*
|
||||
171
docs/experiments/002_ndx_vs_spx_replacement.md
Normal file
171
docs/experiments/002_ndx_vs_spx_replacement.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# 实验记录 002: 纳指100 vs 标普500 替换对比
|
||||
|
||||
## 实验信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 实验编号 | 002 |
|
||||
| 实验日期 | 2026-05-06 |
|
||||
| 实验类型 | A/B对比测试(替换场景) |
|
||||
| 研究问题 | 将美股大类代表从纳指100替换为标普500后的绩效变化 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 实验背景
|
||||
|
||||
### 与001实验的区别
|
||||
|
||||
| 实验 | 操作 | 类内竞争 | 标的数量 |
|
||||
|------|------|---------|---------|
|
||||
| 001 | **添加**标普500 | 有(纳指vs标普) | 11→12 |
|
||||
| 002 | **替换**纳指为标普 | 无 | 11→11 |
|
||||
|
||||
**002实验聚焦**:评估标的特性变化对策略绩效的影响(无类内切换干扰)
|
||||
|
||||
### 理论假设
|
||||
|
||||
```
|
||||
纳指100 (NDX):
|
||||
├─ 成分股:100只科技龙头
|
||||
├─ 风格:纯成长、高波动
|
||||
├─ 动量特性:趋势强、涨跌幅大
|
||||
└─ 与动量策略匹配度:高
|
||||
|
||||
标普500 (SPX):
|
||||
├─ 成分股:500只大盘股
|
||||
├─ 风格:价值+成长混合、中波动
|
||||
├─ 动量特性:趋势相对平缓
|
||||
└─ 与动量策略匹配度:中
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 实验设计
|
||||
|
||||
### A/B组配置
|
||||
|
||||
| 组别 | 美股大类标的 | 其他大类 |
|
||||
|------|-------------|---------|
|
||||
| **A组(对照组)** | 纳指100 (NDX) → 513100.SH | A股2、港股2、日本1、欧洲1、商品3、固收1 |
|
||||
| **B组(实验组)** | 标普500 (SPX) → 513500.SH | 同A组 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 回测结果
|
||||
|
||||
### 绩效对比
|
||||
|
||||
| 指标 | A组(纳指100) | B组(标普500) | 差异 |
|
||||
|------|---------------|---------------|------|
|
||||
| 美股标的 | 纳指100 | 标普500 | 替换 |
|
||||
| **累计收益** | **1467.35%** | 1118.77% | **-348.58%** |
|
||||
| **CAGR** | **48.10%** | 42.87% | **-5.22%** |
|
||||
| **Sharpe** | **2.21** | 2.08 | **-0.13** |
|
||||
| MaxDD | -17.33% | **-15.14%** | **+2.18%** ✓ |
|
||||
| Calmar | 2.78 | **2.83** | +0.06 |
|
||||
| 日胜率 | 56.45% | 56.22% | -0.23% |
|
||||
| 调仓次数 | 459次 | 475次 | +16次 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键发现
|
||||
|
||||
### 发现1:纳指100累计收益显著更高
|
||||
|
||||
```
|
||||
差距分析:
|
||||
├─ 累计收益差距:348.58%
|
||||
├─ CAGR差距:5.22%
|
||||
└─ 原因:纳指100成长性强,动量信号更明显
|
||||
```
|
||||
|
||||
### 发现2:标普500回撤控制更好
|
||||
|
||||
```
|
||||
风险指标:
|
||||
├─ MaxDD改善:2.18%(标普更稳定)
|
||||
├─ Calmar略优:+0.06
|
||||
└─ 原因:标普500波动率更低,成分股更多元
|
||||
```
|
||||
|
||||
### 发现3:纳指100风险调整收益更优
|
||||
|
||||
```
|
||||
Sharpe对比:
|
||||
├─ 纳指100:2.21
|
||||
├─ 标普500:2.08
|
||||
└─ 纳指虽波动大,但收益补偿足够
|
||||
```
|
||||
|
||||
### 发现4:调仓次数差异不大
|
||||
|
||||
```
|
||||
替换场景(无类内竞争):
|
||||
├─ 调仓次数差:仅+16次(vs 001实验+42次)
|
||||
└─ 证明:替换比添加更稳定
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 实验结论
|
||||
|
||||
### 核心结论
|
||||
|
||||
| 维度 | 结论 |
|
||||
|-----|------|
|
||||
| 收益能力 | 纳指100 **显著优于** 标普500 (+348%) |
|
||||
| 风险控制 | 标普500 **略优于** 纳指100 (+2.18%) |
|
||||
| 风险调整收益 | 纳指100 **优于** 标普500 (Sharpe +0.13) |
|
||||
| 综合评价 | **保持纳指100** |
|
||||
|
||||
### 策略建议
|
||||
|
||||
```
|
||||
当前策略建议:保持纳指100作为美股大类代表
|
||||
|
||||
理由:
|
||||
1. 动量策略本质是捕捉强趋势
|
||||
2. 纳指100成长股特性使其动量信号更强
|
||||
3. 累计收益差距显著(1467% vs 1118%)
|
||||
4. 标普500虽更稳定,但牺牲收益太大
|
||||
|
||||
例外情况(可能考虑标普500):
|
||||
├─ 风险偏好极低,优先回撤控制
|
||||
├─ 牛市末期或市场不确定性高时
|
||||
└─ 需要降低组合整体波动率
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 与001实验对比
|
||||
|
||||
| 实验 | 操作 | 收益变化 | 调仓变化 |
|
||||
|------|------|---------|---------|
|
||||
| 001(添加) | 纳指 + 标普 | -291% | +42次 |
|
||||
| 002(替换) | 纳指 → 标普 | -348% | +16次 |
|
||||
|
||||
**洞察**:
|
||||
- 替换场景调仓更稳定(+16 vs +42)
|
||||
- 但收益损失更大(无纳指成长性补偿)
|
||||
|
||||
---
|
||||
|
||||
## 7. 相关文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|-----|------|
|
||||
| `tests/experiments/ab_test_ndx_vs_spx.py` | A/B测试脚本 |
|
||||
| `results/ab_test_ndx_vs_spx.csv` | 测试结果数据 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 后续研究方向
|
||||
|
||||
1. **纳指100 vs 其他美股成长指数**:如罗素1000成长、MSCI美国成长
|
||||
2. **不同市场周期表现**:牛市、熊市分别测试纳指和标普效果
|
||||
3. **动态切换机制**:根据市场状态动态选择纳指或标普
|
||||
|
||||
---
|
||||
|
||||
*实验记录版本: v1.0*
|
||||
*最后更新: 2026-05-06*
|
||||
45
docs/experiments/README.md
Normal file
45
docs/experiments/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 实验记录索引
|
||||
|
||||
本目录用于保存 ETF 轮动策略研究中的有洞察的实验结果。
|
||||
|
||||
---
|
||||
|
||||
## 实验列表
|
||||
|
||||
| 编号 | 实验名称 | 日期 | 类型 | 核心发现 |
|
||||
|------|---------|------|------|---------|
|
||||
| [001](001_same_category_expansion_ab_test.md) | 同大类扩充对轮动策略的影响 | 2026-05-06 | A/B测试 | 添加同大类标的不增加跨类分散,反而因切换成本侵蚀收益 |
|
||||
| [002](002_ndx_vs_spx_replacement.md) | 纳指100 vs 标普500替换对比 | 2026-05-06 | A/B测试 | 纳指100优于标普500(收益+348%,Sharpe+0.13),成长风格更适合动量 |
|
||||
|
||||
---
|
||||
|
||||
## 文档命名规范
|
||||
|
||||
```
|
||||
格式: {编号}_{实验主题}.md
|
||||
|
||||
示例:
|
||||
- 001_same_category_expansion_ab_test.md # 同大类扩充实验
|
||||
- 002_new_category_diversification.md # 新大类分散化实验
|
||||
- 003_rebalance_threshold_tuning.md # 调仓阈值调优实验
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实验文档模板
|
||||
|
||||
每个实验文档应包含以下章节:
|
||||
|
||||
1. **实验信息** - 编号、日期、类型、研究问题
|
||||
2. **实验背景** - 理论假设、研究动机
|
||||
3. **实验设计** - A/B组配置、关键变量
|
||||
4. **回测结果** - 数据、绩效对比表格
|
||||
5. **关键发现** - 核心洞察、数据支撑
|
||||
6. **实验结论** - 假设验证结果、策略建议
|
||||
7. **技术修复记录** - 实验过程中发现的技术问题
|
||||
8. **相关文件** - 脚本、数据文件引用
|
||||
9. **后续研究方向** - 待探索的问题
|
||||
|
||||
---
|
||||
|
||||
*目录创建日期: 2026-05-06*
|
||||
109
fetch_159516_nav.py
Normal file
109
fetch_159516_nav.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
获取159516 ETF净值数据
|
||||
"""
|
||||
|
||||
import os
|
||||
import pandas as pd
|
||||
import tushare as ts
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 设置Tushare token
|
||||
def get_tushare_token():
|
||||
# 首先尝试从环境变量获取
|
||||
token = os.environ.get("TUSHARE_TOKEN")
|
||||
if token:
|
||||
return token
|
||||
|
||||
# 尝试从.env文件获取
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
token = os.environ.get("TUSHARE_TOKEN")
|
||||
if token:
|
||||
return token
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# 手动读取.env文件
|
||||
env_path = os.path.join(os.path.dirname(__file__), '.env')
|
||||
if os.path.exists(env_path):
|
||||
with open(env_path, 'r') as f:
|
||||
for line in f:
|
||||
if line.startswith('TUSHARE_TOKEN='):
|
||||
token = line.strip().split('=', 1)[1].strip().strip('"').strip("'")
|
||||
if token:
|
||||
return token
|
||||
|
||||
raise ValueError("请设置 TUSHARE_TOKEN 环境变量或在.env文件中配置")
|
||||
|
||||
|
||||
def fetch_etf_nav(etf_code="159516.SZ", days=30):
|
||||
"""
|
||||
获取ETF净值数据
|
||||
|
||||
Args:
|
||||
etf_code: ETF代码,如 "159516.SZ"
|
||||
days: 获取天数
|
||||
|
||||
Returns:
|
||||
DataFrame: 包含日期和净值
|
||||
"""
|
||||
pro = ts.pro_api(get_tushare_token())
|
||||
|
||||
# 计算日期范围
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days + 5)
|
||||
|
||||
start_str = start_date.strftime('%Y%m%d')
|
||||
end_str = end_date.strftime('%Y%m%d')
|
||||
|
||||
# 转换代码格式 (tushare使用.SH而不是.SS)
|
||||
ts_code = etf_code.replace(".SS", ".SH")
|
||||
|
||||
print(f"获取 {etf_code} 净值数据...")
|
||||
print(f"日期范围: {start_str} ~ {end_str}")
|
||||
|
||||
try:
|
||||
# 获取ETF净值数据
|
||||
nav_df = pro.fund_nav(
|
||||
ts_code=ts_code,
|
||||
start_date=start_str,
|
||||
end_date=end_str
|
||||
)
|
||||
|
||||
if nav_df is None or len(nav_df) == 0:
|
||||
print("未获取到净值数据")
|
||||
return None
|
||||
|
||||
# 排序并处理数据
|
||||
nav_df = nav_df.sort_values('nav_date')
|
||||
|
||||
# 转换日期格式
|
||||
nav_df['date'] = pd.to_datetime(nav_df['nav_date'])
|
||||
nav_df = nav_df.set_index('date')
|
||||
|
||||
print(f"\n获取到 {len(nav_df)} 条净值数据")
|
||||
print(f"最新净值日期: {nav_df.index.max().strftime('%Y-%m-%d')}")
|
||||
print(f"最新净值: {nav_df['unit_nav'].iloc[-1]}")
|
||||
|
||||
# 显示最近10条数据
|
||||
print(f"\n最近10条净值数据:")
|
||||
print(nav_df[['unit_nav']].tail(10).to_string())
|
||||
|
||||
return nav_df
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取净值数据失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 获取159516的净值数据
|
||||
result = fetch_etf_nav("159516.SZ", days=30)
|
||||
|
||||
if result is not None:
|
||||
# 保存到CSV文件
|
||||
output_file = "159516_nav_data.csv"
|
||||
result[['unit_nav']].to_csv(output_file)
|
||||
print(f"\n数据已保存到: {output_file}")
|
||||
183
fetch_159930.py
Normal file
183
fetch_159930.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
获取159930 ETF最新10天的收盘价、净值并计算溢价率
|
||||
"""
|
||||
|
||||
import os
|
||||
import pandas as pd
|
||||
import tushare as ts
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 设置Tushare token
|
||||
def get_tushare_token():
|
||||
# 首先尝试从环境变量获取
|
||||
token = os.environ.get("TUSHARE_TOKEN")
|
||||
if token:
|
||||
return token
|
||||
|
||||
# 尝试从.env文件获取
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
token = os.environ.get("TUSHARE_TOKEN")
|
||||
if token:
|
||||
return token
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# 手动读取.env文件
|
||||
env_path = os.path.join(os.path.dirname(__file__), '.env')
|
||||
if os.path.exists(env_path):
|
||||
with open(env_path, 'r') as f:
|
||||
for line in f:
|
||||
if line.startswith('TUSHARE_TOKEN='):
|
||||
token = line.strip().split('=', 1)[1].strip().strip('"').strip("'")
|
||||
if token:
|
||||
return token
|
||||
|
||||
raise ValueError("请设置 TUSHARE_TOKEN 环境变量或在.env文件中配置")
|
||||
|
||||
|
||||
def fetch_etf_data(etf_code: str, days: int = 10):
|
||||
"""
|
||||
获取ETF最新N天的价格、净值数据
|
||||
|
||||
Args:
|
||||
etf_code: ETF代码,如 "159930.SZ"
|
||||
days: 获取天数
|
||||
|
||||
Returns:
|
||||
DataFrame: 包含日期、收盘价、净值、溢价率
|
||||
"""
|
||||
pro = ts.pro_api(get_tushare_token())
|
||||
|
||||
# 计算日期范围(多取几天确保有足够数据)
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days + 5)
|
||||
|
||||
start_str = start_date.strftime('%Y%m%d')
|
||||
end_str = end_date.strftime('%Y%m%d')
|
||||
|
||||
# 转换代码格式
|
||||
ts_code = etf_code.replace(".SS", ".SH")
|
||||
|
||||
print(f"获取 {etf_code} 数据...")
|
||||
print(f"日期范围: {start_str} ~ {end_str}")
|
||||
|
||||
# 1. 获取ETF价格数据(fund_daily接口)
|
||||
print("\n1. 获取ETF价格数据...")
|
||||
try:
|
||||
price_df = pro.fund_daily(
|
||||
ts_code=ts_code,
|
||||
start_date=start_str,
|
||||
end_date=end_str
|
||||
)
|
||||
if price_df is not None and len(price_df) > 0:
|
||||
price_df = price_df.sort_values('trade_date')
|
||||
print(f" 获取到 {len(price_df)} 条价格数据")
|
||||
print(f" 最新日期: {price_df['trade_date'].max()}")
|
||||
else:
|
||||
print(" 未获取到价格数据")
|
||||
price_df = None
|
||||
except Exception as e:
|
||||
print(f" 获取价格数据失败: {e}")
|
||||
price_df = None
|
||||
|
||||
# 2. 获取ETF净值数据(fund_nav接口)
|
||||
print("\n2. 获取ETF净值数据...")
|
||||
try:
|
||||
# 净值通常滞后,多取一天
|
||||
nav_end_date = end_date + timedelta(days=1)
|
||||
nav_end_str = nav_end_date.strftime('%Y%m%d')
|
||||
|
||||
nav_df = pro.fund_nav(
|
||||
ts_code=ts_code,
|
||||
start_date=start_str,
|
||||
end_date=nav_end_str
|
||||
)
|
||||
if nav_df is not None and len(nav_df) > 0:
|
||||
nav_df = nav_df.sort_values('nav_date')
|
||||
print(f" 获取到 {len(nav_df)} 条净值数据")
|
||||
print(f" 最新日期: {nav_df['nav_date'].max()}")
|
||||
else:
|
||||
print(" 未获取到净值数据")
|
||||
nav_df = None
|
||||
except Exception as e:
|
||||
print(f" 获取净值数据失败: {e}")
|
||||
nav_df = None
|
||||
|
||||
# 3. 合并数据并计算溢价率
|
||||
print("\n3. 合并数据并计算溢价率...")
|
||||
|
||||
if price_df is None:
|
||||
print("错误: 没有价格数据")
|
||||
return None
|
||||
|
||||
# 准备价格数据
|
||||
price_df['date'] = pd.to_datetime(price_df['trade_date'])
|
||||
price_df = price_df.set_index('date')
|
||||
price_series = price_df['close']
|
||||
|
||||
# 准备净值数据
|
||||
if nav_df is not None:
|
||||
nav_df['date'] = pd.to_datetime(nav_df['nav_date'])
|
||||
nav_df = nav_df.set_index('date')
|
||||
nav_series = nav_df['unit_nav']
|
||||
else:
|
||||
nav_series = pd.Series()
|
||||
|
||||
# 创建结果DataFrame
|
||||
result = pd.DataFrame({
|
||||
'收盘价': price_series
|
||||
})
|
||||
|
||||
# 对齐净值数据(按日期)
|
||||
result = result.join(nav_series.rename('净值'), how='left')
|
||||
|
||||
# 计算溢价率
|
||||
result['溢价率'] = (result['收盘价'] - result['净值']) / result['净值'] * 100
|
||||
|
||||
# 取最新N天
|
||||
result = result.tail(days)
|
||||
|
||||
# 格式化输出
|
||||
result['收盘价'] = result['收盘价'].round(3)
|
||||
result['净值'] = result['净值'].round(3)
|
||||
result['溢价率'] = result['溢价率'].round(2)
|
||||
|
||||
# 重置索引,将日期作为列
|
||||
result = result.reset_index()
|
||||
result['日期'] = result['date'].dt.strftime('%Y-%m-%d')
|
||||
result = result[['日期', '收盘价', '净值', '溢价率']]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
etf_code = "159930.SZ"
|
||||
days = 10
|
||||
|
||||
print("=" * 60)
|
||||
print(f"ETF: {etf_code} (中证能源ETF)")
|
||||
print(f"获取最近 {days} 天数据")
|
||||
print("=" * 60)
|
||||
|
||||
df = fetch_etf_data(etf_code, days)
|
||||
|
||||
if df is not None and len(df) > 0:
|
||||
print("\n" + "=" * 60)
|
||||
print("结果表格:")
|
||||
print("=" * 60)
|
||||
print(df.to_string(index=False))
|
||||
|
||||
# 保存到CSV
|
||||
output_file = f"{etf_code.replace('.', '_')}_latest_{days}days.csv"
|
||||
df.to_csv(output_file, index=False, encoding='utf-8-sig')
|
||||
print(f"\n数据已保存到: {output_file}")
|
||||
else:
|
||||
print("\n获取数据失败")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
tests/experiments/__init__.py
Normal file
2
tests/experiments/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# 实验脚本目录
|
||||
# 存放策略研究相关的A/B测试、对比实验等脚本
|
||||
187
tests/experiments/ab_test_ndx_vs_spx.py
Normal file
187
tests/experiments/ab_test_ndx_vs_spx.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
A/B测试:纳指100 vs 标普500 替换对比
|
||||
对比:
|
||||
- A组(对照组):纳指100作为美股大类代表
|
||||
- B组(实验组):标普500替换纳指100作为美股大类代表
|
||||
|
||||
核心问题:替换后对策略绩效的影响(无类内竞争)
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from strategies.rotation.engine import RotationStrategy
|
||||
import pandas as pd
|
||||
import yaml
|
||||
|
||||
|
||||
def create_config_replace_ndx_with_spx(base_config: dict) -> dict:
|
||||
"""将纳指100替换为标普500"""
|
||||
config = base_config.copy()
|
||||
config['code_list'] = base_config['code_list'].copy()
|
||||
|
||||
# 移除纳指100
|
||||
if 'NDX' in config['code_list']:
|
||||
del config['code_list']['NDX']
|
||||
|
||||
# 添加标普500(替换纳指100)
|
||||
config['code_list']['SPX'] = {
|
||||
'name': '标普500',
|
||||
'etf': '513500.SH',
|
||||
'market': 'US'
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def run_backtest(config: dict, label: str) -> dict:
|
||||
"""运行回测并返回关键指标"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {label}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
strategy = RotationStrategy(config)
|
||||
result = strategy.run()
|
||||
|
||||
if result is None or len(result) == 0:
|
||||
return None
|
||||
|
||||
# 计算指标
|
||||
strategy_nav = result['轮动策略净值']
|
||||
strategy_ret = result['轮动策略日收益率']
|
||||
|
||||
total_return = strategy_nav.iloc[-1] - 1
|
||||
days = len(result)
|
||||
years = days / 250
|
||||
cagr = (strategy_nav.iloc[-1] ** (1/years)) - 1
|
||||
excess_ret = strategy_ret.mean() * 250
|
||||
vol = strategy_ret.std() * (250 ** 0.5)
|
||||
sharpe = excess_ret / vol if vol > 0 else 0
|
||||
rolling_max = strategy_nav.cummax()
|
||||
drawdown = (strategy_nav - rolling_max) / rolling_max
|
||||
max_dd = drawdown.min()
|
||||
calmar = cagr / abs(max_dd) if max_dd < 0 else 0
|
||||
win_rate = (strategy_ret > 0).sum() / len(strategy_ret)
|
||||
|
||||
metrics = {
|
||||
'label': label,
|
||||
'美股标的': '纳指100' if 'NDX' in config['code_list'] else '标普500',
|
||||
'累计收益': total_return,
|
||||
'CAGR': cagr,
|
||||
'Sharpe': sharpe,
|
||||
'MaxDD': max_dd,
|
||||
'Calmar': calmar,
|
||||
'日胜率': win_rate,
|
||||
}
|
||||
|
||||
print(f"\n美股代表: {metrics['美股标的']}")
|
||||
print(f"累计收益: {metrics['累计收益']:.2%}")
|
||||
print(f"CAGR: {metrics['CAGR']:.2%}")
|
||||
print(f"Sharpe: {metrics['Sharpe']:.2f}")
|
||||
print(f"MaxDD: {metrics['MaxDD']:.2%}")
|
||||
print(f"Calmar: {metrics['Calmar']:.2f}")
|
||||
print(f"日胜率: {metrics['日胜率']:.2%}")
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def compare_results(a_metrics: dict, b_metrics: dict):
|
||||
"""对比两组结果"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" 对比结果")
|
||||
print(f"{'='*60}")
|
||||
|
||||
print(f"\n{'指标':<15} {'A组(纳指100)':<15} {'B组(标普500)':<15} {'差异':<15}")
|
||||
print("-" * 60)
|
||||
|
||||
metrics_keys = ['美股标的', '累计收益', 'CAGR', 'Sharpe', 'MaxDD', 'Calmar', '日胜率']
|
||||
|
||||
for key in metrics_keys:
|
||||
a_val = a_metrics.get(key, 0)
|
||||
b_val = b_metrics.get(key, 0)
|
||||
|
||||
if key == '美股标的':
|
||||
print(f"{key:<15} {a_val:<15} {b_val:<15} {'替换':<15}")
|
||||
continue
|
||||
|
||||
diff = b_val - a_val
|
||||
if key in ['累计收益', 'CAGR', 'MaxDD', '日胜率']:
|
||||
a_str = f"{a_val:.2%}"
|
||||
b_str = f"{b_val:.2%}"
|
||||
diff_str = f"{diff*100:+.2f}%"
|
||||
else:
|
||||
a_str = f"{a_val:.2f}"
|
||||
b_str = f"{b_val:.2f}"
|
||||
diff_str = f"{diff:+.2f}"
|
||||
|
||||
print(f"{key:<15} {a_str:<15} {b_str:<15} {diff_str:<15}")
|
||||
|
||||
print("-" * 60)
|
||||
|
||||
print(f"\n【关键发现】")
|
||||
print(f"纳指100 → 标普500 替换效果:")
|
||||
|
||||
if b_metrics['CAGR'] < a_metrics['CAGR']:
|
||||
print(f" - CAGR下降 {a_metrics['CAGR'] - b_metrics['CAGR']:.2%}")
|
||||
print(f" → 标普500动量信号可能不如纳指强")
|
||||
|
||||
if b_metrics['MaxDD'] > a_metrics['MaxDD']: # 注意MaxDD是负数
|
||||
print(f" - MaxDD改善 {b_metrics['MaxDD'] - a_metrics['MaxDD']:.2%}")
|
||||
print(f" → 标普500更稳定,回撤更小")
|
||||
|
||||
if b_metrics['Sharpe'] > a_metrics['Sharpe']:
|
||||
print(f" - Sharpe改善 {b_metrics['Sharpe'] - a_metrics['Sharpe']:.2f}")
|
||||
print(f" → 标普500风险调整后收益更优")
|
||||
elif b_metrics['Sharpe'] < a_metrics['Sharpe']:
|
||||
print(f" - Sharpe下降 {b_metrics['Sharpe'] - a_metrics['Sharpe']:.2f}")
|
||||
print(f" → 纳指100风险调整后收益更优")
|
||||
|
||||
print(f"\n【策略建议】")
|
||||
if b_metrics['累计收益'] < a_metrics['累计收益'] * 0.9:
|
||||
print(f" 建议:保持纳指100(成长风格更适合动量策略)")
|
||||
elif b_metrics['Sharpe'] > a_metrics['Sharpe']:
|
||||
print(f" 建议:考虑标普500(更稳定、风险调整收益更优)")
|
||||
else:
|
||||
print(f" 建议:保持纳指100(累计收益更高)")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 加载基础配置
|
||||
config_path = Path(__file__).parent.parent / 'config' / 'strategies' / 'rotation.yaml'
|
||||
with open(config_path, 'r') as f:
|
||||
base_config = yaml.safe_load(f)
|
||||
|
||||
# 添加 end_date
|
||||
from datetime import datetime
|
||||
base_config['end_date'] = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" A/B测试:纳指100 vs 标普500 替换对比")
|
||||
print(f"{'='*60}")
|
||||
print(f"\n研究问题:")
|
||||
print(f" - 将美股大类代表从纳指100替换为标普500")
|
||||
print(f" - 无类内竞争(每大类还是1只)")
|
||||
print(f" - 评估标的特性变化对绩效的影响")
|
||||
|
||||
# A组:纳指100(当前配置)
|
||||
a_metrics = run_backtest(base_config, "A组: 纳指100作为美股代表")
|
||||
|
||||
# B组:标普500替换纳指100
|
||||
config_replace = create_config_replace_ndx_with_spx(base_config)
|
||||
b_metrics = run_backtest(config_replace, "B组: 标普500替换纳指100")
|
||||
|
||||
# 对比
|
||||
if a_metrics and b_metrics:
|
||||
compare_results(a_metrics, b_metrics)
|
||||
|
||||
# 保存结果
|
||||
results_df = pd.DataFrame([a_metrics, b_metrics])
|
||||
results_path = Path(__file__).parent.parent / 'results' / 'ab_test_ndx_vs_spx.csv'
|
||||
results_df.to_csv(results_path, index=False)
|
||||
print(f"\n对比结果已保存: {results_path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
183
tests/experiments/ab_test_spx.py
Normal file
183
tests/experiments/ab_test_spx.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
A/B测试:添加标普500对轮动策略的影响
|
||||
对比:
|
||||
- A组(对照组):当前11只标的配置
|
||||
- B组(实验组):添加标普500后的12只标的配置
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from strategies.rotation.engine import RotationStrategy
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def create_config_with_spx(base_config: dict) -> dict:
|
||||
"""在基础配置上添加标普500"""
|
||||
config = base_config.copy()
|
||||
config['code_list'] = base_config['code_list'].copy()
|
||||
|
||||
# 添加标普500(美股大类内)
|
||||
config['code_list']['SPX'] = {
|
||||
'name': '标普500',
|
||||
'etf': '513500.SH',
|
||||
'market': 'US' # 与纳指100同属美股大类
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def run_backtest(config: dict, label: str) -> dict:
|
||||
"""运行回测并返回关键指标"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {label}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
strategy = RotationStrategy(config)
|
||||
result = strategy.run() # result 是 DataFrame
|
||||
|
||||
if result is None or len(result) == 0:
|
||||
return None
|
||||
|
||||
# 从 DataFrame 中直接计算指标
|
||||
strategy_nav = result['轮动策略净值']
|
||||
strategy_ret = result['轮动策略日收益率']
|
||||
benchmark_nav = result['基准净值']
|
||||
benchmark_ret = result['基准日收益率']
|
||||
|
||||
# 累计收益
|
||||
total_return = strategy_nav.iloc[-1] - 1
|
||||
|
||||
# CAGR (交易日口径)
|
||||
days = len(result)
|
||||
years = days / 250
|
||||
cagr = (strategy_nav.iloc[-1] ** (1/years)) - 1
|
||||
|
||||
# Sharpe
|
||||
excess_ret = strategy_ret.mean() * 250 # 年化收益
|
||||
vol = strategy_ret.std() * (250 ** 0.5) # 年化波动
|
||||
sharpe = excess_ret / vol if vol > 0 else 0
|
||||
|
||||
# 最大回撤
|
||||
rolling_max = strategy_nav.cummax()
|
||||
drawdown = (strategy_nav - rolling_max) / rolling_max
|
||||
max_dd = drawdown.min()
|
||||
|
||||
# Calmar
|
||||
calmar = cagr / abs(max_dd) if max_dd < 0 else 0
|
||||
|
||||
# 日胜率
|
||||
win_rate = (strategy_ret > 0).sum() / len(strategy_ret)
|
||||
|
||||
# 提取关键指标
|
||||
metrics = {
|
||||
'label': label,
|
||||
'标的数': len(config['code_list']),
|
||||
'累计收益': total_return,
|
||||
'CAGR': cagr,
|
||||
'Sharpe': sharpe,
|
||||
'MaxDD': max_dd,
|
||||
'Calmar': calmar,
|
||||
'日胜率': win_rate,
|
||||
}
|
||||
|
||||
print(f"\n标的池: {len(config['code_list'])}只")
|
||||
print(f"累计收益: {metrics['累计收益']:.2%}")
|
||||
print(f"CAGR: {metrics['CAGR']:.2%}")
|
||||
print(f"Sharpe: {metrics['Sharpe']:.2f}")
|
||||
print(f"MaxDD: {metrics['MaxDD']:.2%}")
|
||||
print(f"Calmar: {metrics['Calmar']:.2f}")
|
||||
print(f"日胜率: {metrics['日胜率']:.2%}")
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def compare_results(a_metrics: dict, b_metrics: dict):
|
||||
"""对比两组结果"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" 对比结果")
|
||||
print(f"{'='*60}")
|
||||
|
||||
print(f"\n{'指标':<12} {'A组(无SPX)':<15} {'B组(有SPX)':<15} {'差异':<15}")
|
||||
print("-" * 60)
|
||||
|
||||
metrics_keys = ['标的数', '累计收益', 'CAGR', 'Sharpe', 'MaxDD', 'Calmar', '日胜率']
|
||||
|
||||
for key in metrics_keys:
|
||||
a_val = a_metrics.get(key, 0)
|
||||
b_val = b_metrics.get(key, 0)
|
||||
|
||||
if key == '标的数':
|
||||
diff = b_val - a_val
|
||||
diff_str = f"+{diff}" if diff > 0 else str(diff)
|
||||
else:
|
||||
diff = b_val - a_val
|
||||
if key in ['累计收益', 'CAGR', 'MaxDD', '日胜率']:
|
||||
diff_str = f"{diff*100:+.2f}%"
|
||||
else:
|
||||
diff_str = f"{diff:+.2f}"
|
||||
|
||||
if key in ['累计收益', 'CAGR', 'MaxDD', '日胜率']:
|
||||
a_str = f"{a_val:.2%}"
|
||||
b_str = f"{b_val:.2%}"
|
||||
else:
|
||||
a_str = str(a_val)
|
||||
b_str = str(b_val)
|
||||
|
||||
print(f"{key:<12} {a_str:<15} {b_str:<15} {diff_str:<15}")
|
||||
|
||||
print("-" * 60)
|
||||
|
||||
# 分析美股大类内部切换情况
|
||||
print(f"\n【关键发现】")
|
||||
print(f"添加标普500后:")
|
||||
print(f" - 美股大类从1只→2只(纳指100 + 标普500)")
|
||||
print(f" - 类内竞争:纳指100 vs 标普500,得分高者代表美股大类")
|
||||
print(f" - 跨类分散不变:美股大类还是只输出1只冠军进入Top3")
|
||||
|
||||
if b_metrics['累计收益'] != a_metrics['累计收益']:
|
||||
print(f" - 累计收益变化:{a_metrics['累计收益']:.2%} → {b_metrics['累计收益']:.2%}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
import yaml
|
||||
|
||||
# 加载基础配置
|
||||
config_path = Path(__file__).parent.parent / 'config' / 'strategies' / 'rotation.yaml'
|
||||
with open(config_path, 'r') as f:
|
||||
base_config = yaml.safe_load(f)
|
||||
|
||||
# 添加缺失的 end_date(使用今天日期)
|
||||
from datetime import datetime
|
||||
base_config['end_date'] = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" A/B测试:添加标普500对diversified模式的影响")
|
||||
print(f"{'='*60}")
|
||||
print(f"\n测试假设:")
|
||||
print(f" - diversified=true 模式下,每大类只选1只冠军")
|
||||
print(f" - 添加标普500(同属美股大类)不会增加跨类分散")
|
||||
print(f" - 但可能增加类内切换频率和换手率")
|
||||
|
||||
# A组:当前配置(11只,无标普500)
|
||||
a_metrics = run_backtest(base_config, "A组: 当前配置(11只,无标普500)")
|
||||
|
||||
# B组:添加标普500后的配置(12只)
|
||||
config_with_spx = create_config_with_spx(base_config)
|
||||
b_metrics = run_backtest(config_with_spx, "B组: 添加标普500(12只)")
|
||||
|
||||
# 对比结果
|
||||
if a_metrics and b_metrics:
|
||||
compare_results(a_metrics, b_metrics)
|
||||
|
||||
# 保存对比结果
|
||||
results_df = pd.DataFrame([a_metrics, b_metrics])
|
||||
results_path = Path(__file__).parent.parent / 'results' / 'ab_test_spx.csv'
|
||||
results_df.to_csv(results_path, index=False)
|
||||
print(f"\n对比结果已保存: {results_path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
2
tests/utils/__init__.py
Normal file
2
tests/utils/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# 工具脚本目录
|
||||
# 存放数据构建、缓存、导出等辅助工具脚本
|
||||
Reference in New Issue
Block a user