310 lines
10 KiB
Markdown
310 lines
10 KiB
Markdown
# 策略回测平台使用说明
|
||
|
||
## 1. 项目结构
|
||
|
||
当前目录 `strategy_backtest/` 结构如下:
|
||
|
||
- `main.py`:回测入口脚本。
|
||
- `requirements.txt`:Python 依赖列表。
|
||
- `config/settings.py`:全局配置:路径、日期、初始资金、策略参数等。
|
||
- `utils/`:工具模块。
|
||
- `logger.py`:统一日志模块,所有文件使用 `setup_logger` 获取 logger。
|
||
- `data_loader.py`:行情数据加载模块,从 `data/day/*.txt` 读取日线数据。
|
||
- `performance.py`:根据资金曲线计算收益、夏普、最大回撤等指标。
|
||
- `plotter.py`:根据资金曲线 CSV 绘制资金曲线图。
|
||
- `strategies/`:策略模块。
|
||
- `base_strategy.py`:抽象基类,封装账户与持仓、主回测循环等。
|
||
- `ma_cross.py`:示例策略:均线交叉(持有 5 天,最多 2 只)。
|
||
- `results/`:回测输出。
|
||
- `logs/app.log`:统一运行日志。
|
||
- `equity/{strategy_name}_equity.csv`:资金曲线 CSV。
|
||
- `plots/{strategy_name}_curve.png`:资金曲线图。
|
||
|
||
## 2. 环境准备
|
||
|
||
1. 安装 Python 3.10+(推荐)。
|
||
2. 在 `strategy_backtest/` 目录执行:
|
||
|
||
```bash
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
## 3. 行情数据准备
|
||
|
||
1. 将日线 TXT 行情文件放在项目根目录上一级的 `data/day/` 下(即 `d:/data/jq_hc/data/day/`):
|
||
- 文件名格式:`{ts_code}_daily_data.txt`,例如:`000001.SZ_daily_data.txt`;
|
||
- 编码:UTF-8 无 BOM;
|
||
- 分隔符:制表符 `\t`;
|
||
- 首行必须包含至少这些列:`ts_code`, `trade_date`, `open`, `high`, `low`, `close`, `vol`;
|
||
- 日期列 `trade_date` 格式为 `YYYYMMDD`。
|
||
2. 可选:在 `data/code/all_stock_codes.txt` 中按行列出股票代码(含后缀),例如:
|
||
|
||
```text
|
||
000001.SZ
|
||
600000.SH
|
||
```
|
||
|
||
若该文件存在,则回测股票池从该文件读取;否则会扫描 `data/day/` 目录下的文件名自动推断。
|
||
|
||
## 4. 配置说明(config/settings.py)
|
||
|
||
主要配置项说明如下:
|
||
|
||
- `BASE_DIR`:项目根目录(`d:/data/jq_hc/`)。
|
||
- `DATA_DAY_DIR`:日线数据目录(`BASE_DIR / "data" / "day"`)。
|
||
- `RESULTS_DIR`:回测结果目录(`BASE_DIR / "strategy_backtest" / "results"`)。
|
||
- `START_DATE` / `END_DATE`:回测起止日期(`YYYYMMDD`)。
|
||
- `INITIAL_CASH`:初始资金金额。
|
||
- `MAX_POSITIONS`:最多持仓股票数。
|
||
- `HOLD_DAYS`:单只股票持有天数。
|
||
- `MA_SHORT` / `MA_LONG`:均线参数。
|
||
- `STRATEGY`:策略配置,包含模块名、类名与参数。
|
||
|
||
如需调整策略参数或回测时间窗口,直接修改 `settings.py` 中对应变量即可。
|
||
|
||
## 5. 运行回测
|
||
|
||
### 5.1 单策略回测模式
|
||
|
||
在 `strategy_backtest/` 目录执行:
|
||
|
||
```bash
|
||
python main.py
|
||
```
|
||
|
||
程序将自动执行以下步骤:
|
||
|
||
1. 加载股票代码列表(优先使用 `data/code/all_stock_codes.txt`,否则扫描 `data/day/`)。
|
||
2. 对股票池中的每只股票,从 `DATA_DAY_DIR` 加载日线数据,并按日期升序整理。
|
||
3. **性能优化**:预计算日期索引(O(1) 查询)、均线指标、交易信号(金叉+放量布尔掩码)。
|
||
4. 动态加载 `STRATEGY` 中配置的策略类(默认 `MaCrossStrategy`),并运行回测。
|
||
5. 将资金曲线保存到 `results/equity/{strategy_name}_equity.csv`。
|
||
6. 计算绩效指标(累计收益、年化收益、夏普比率、最大回撤、胜率、盈亏比等)并记录到日志中。
|
||
7. 生成资金曲线图 `results/plots/{strategy_name}_curve.png`。
|
||
|
||
### 5.2 参数优化模式
|
||
|
||
使用多进程网格搜索找到最优参数组合:
|
||
|
||
```bash
|
||
# 使用默认配置(4核,保存前20组,按夏普比率排序)
|
||
python main.py --optimize
|
||
|
||
# 自定义配置
|
||
python main.py --optimize --jobs 8 --top 50 --metric sharpe
|
||
|
||
# 可选排序指标:sharpe / total_return / max_drawdown / annual_return
|
||
python main.py --optimize --metric total_return --top 10
|
||
```
|
||
|
||
**参数说明**:
|
||
- `--optimize`:启用参数优化模式
|
||
- `--strategy`:策略名称(默认:ma_cross)
|
||
- `--jobs`:并行进程数(默认:4)
|
||
- `--top`:保存前N个最优结果(默认:20)
|
||
- `--metric`:排序指标(默认:sharpe)
|
||
|
||
**优化结果**:
|
||
- 优化过程会显示进度条和实时完成数
|
||
- 结果保存在 `results/optimization/grid_search_{timestamp}.csv`
|
||
- 日志中打印 Top 5 最优参数组合及其绩效
|
||
|
||
## 6. 日志说明
|
||
|
||
- 日志格式:`YYYY-MM-DD HH:MM:SS [LEVEL] 文件名.py:行号 - 消息内容`;
|
||
- 输出位置:
|
||
- 终端标准输出;
|
||
- 文件:`results/logs/app.log`;
|
||
- 关键日志:
|
||
- `utils/data_loader.py`:加载成功/失败/缺失文件;
|
||
- `strategies/base_strategy.py`:回测起止;
|
||
- `strategies/ma_cross.py`:买卖信号;
|
||
- `main.py`:主流程异常等。
|
||
|
||
## 7. 参数优化配置(config/settings.py)
|
||
|
||
### 7.1 参数空间定义
|
||
|
||
在 `config/settings.py` 中配置参数空间:
|
||
|
||
```python
|
||
PARAM_SPACES: dict = {
|
||
"ma_cross": {
|
||
"ma_short": [3, 20, 2], # 短期均线:3-20,步长2 -> [3,5,7,9,...,19]
|
||
"ma_long": [20, 60, 5], # 长期均线:20-60,步长5 -> [20,25,30,...,55]
|
||
"hold_days": [3, 10, 1], # 持有天数:3-10,步长1 -> [3,4,5,...,9]
|
||
"position_pct_per_stock": [0.4, 0.5, 0.6], # 单股仓位:离散值
|
||
},
|
||
}
|
||
```
|
||
|
||
**格式说明**:
|
||
- **范围格式**:`[min, max, step]` - 生成等差数列
|
||
- **离散格式**:`[value1, value2, ...]` - 直接指定取值
|
||
- 支持整数和浮点数
|
||
|
||
### 7.2 参数约束条件
|
||
|
||
使用 lambda 函数过滤无意义的参数组合:
|
||
|
||
```python
|
||
PARAM_CONSTRAINTS: dict = {
|
||
"ma_cross": lambda params: (
|
||
# 约束1:短期均线必须小于长期均线
|
||
params.get("ma_short", 0) < params.get("ma_long", 999)
|
||
# 可以添加更多约束,例如:
|
||
# and params.get("hold_days", 0) >= 3
|
||
# and params.get("position_pct_per_stock", 0) <= 0.6
|
||
),
|
||
}
|
||
```
|
||
|
||
**约束示例**:
|
||
- 避免 `ma_short >= ma_long`(短期≥长期无意义)
|
||
- 限制参数范围(如仓位不超过60%)
|
||
- 参数之间的逻辑关系
|
||
|
||
**优化效果**:
|
||
- 自动过滤无效组合,减少计算量
|
||
- 例:648 组 -> 过滤后 400 组(节省 38% 时间)
|
||
|
||
### 7.3 优化方法
|
||
|
||
当前实现的是 **网格搜索(Grid Search)** 方法:
|
||
|
||
| 特性 | 说明 |
|
||
|------|------|
|
||
| **方法** | 全遍历(笛卡尔积) |
|
||
| **优点** | 简单直观,保证找到全局最优 |
|
||
| **缺点** | 参数多时计算量大(指数增长) |
|
||
| **适用** | 参数维度低(2-4个)、参数范围小 |
|
||
| **加速** | 多进程并行(默认4核,可配置) |
|
||
|
||
**参数组合数计算**:
|
||
```
|
||
总数 = ma_short的取值数 × ma_long的取值数 × hold_days的取值数 × ...
|
||
例:9 × 9 × 7 × 3 = 1701 组
|
||
```
|
||
|
||
**其他可选方法**(未实现):
|
||
- 贝叶斯优化:高维空间高效,智能搜索
|
||
- 遗传算法:适合复杂约束条件
|
||
- 随机搜索:快速探索可行域
|
||
|
||
## 8. 性能优化详解
|
||
|
||
### 8.1 优化策略
|
||
|
||
回测系统实现了多层次性能优化:
|
||
|
||
#### ① 日期索引预计算(Date Index Dict)
|
||
|
||
**问题**:原始方法使用 `df[df['trade_date'] == date]` 过滤,每次 O(n) 复杂度
|
||
|
||
**解决**:预先构建 `{ts_code: {date: row_index}}` 字典,查询时间降为 O(1)
|
||
|
||
```python
|
||
date_index_dict = {
|
||
"000001.SZ": {"20230101": 0, "20230102": 1, ...},
|
||
"600000.SH": {"20230101": 0, "20230102": 1, ...},
|
||
}
|
||
|
||
# 使用:
|
||
idx = date_index_dict[ts_code][current_date]
|
||
row = df.iloc[idx] # 直接定位,无需过滤
|
||
```
|
||
|
||
**提升**:727天 × 3776股 = 274万次查询,每次从 O(n) 降为 O(1)
|
||
|
||
#### ② 信号预计算(Signal Precomputation)
|
||
|
||
**问题**:回测时每天重复计算 `rolling_mean()`、`pct_change()` 等
|
||
|
||
**解决**:使用 pandas 向量化操作一次性计算所有信号,存储为布尔列
|
||
|
||
```python
|
||
# 预计算金叉信号
|
||
df["golden_cross"] = (
|
||
(df["ma_short"] > df["ma_long"]) &
|
||
(df["ma_short"].shift(1) <= df["ma_long"].shift(1))
|
||
)
|
||
|
||
# 预计算放量信号
|
||
df["volume_surge"] = df["vol"] > df["vol"].rolling(5).mean() * 1.5
|
||
|
||
# 组合买入信号
|
||
df["buy_signal"] = df["golden_cross"] & df["volume_surge"]
|
||
```
|
||
|
||
**提升**:避免 274万次重复计算,约节省 80% CPU 时间
|
||
|
||
#### ③ 买入信号索引(Buy Signal Index)
|
||
|
||
**问题**:每天遍历所有 3776 只股票检查是否有信号
|
||
|
||
**解决**:构建反向索引 `{date: [stock_list]}`,只遍历有信号的股票
|
||
|
||
```python
|
||
buy_signal_index = {
|
||
"20230103": ["000001.SZ", "600000.SH"], # 只有20只股票有信号
|
||
"20230104": ["000002.SZ", "600519.SH"],
|
||
}
|
||
|
||
# 使用:
|
||
signal_stocks = buy_signal_index.get(current_date, [])
|
||
for ts_code in signal_stocks: # 只遍历 20-50 只,而非 3776 只
|
||
# 处理买入逻辑
|
||
```
|
||
|
||
**提升**:每天从遍历 3776 只减少到 20-50 只,**减少 99% 循环**
|
||
|
||
#### ④ 避免 DataFrame.attrs deepcopy
|
||
|
||
**问题**:将索引存储在 `df.attrs` 中,`iloc` 切片时触发 deepcopy
|
||
|
||
**解决**:将索引字典作为独立参数传递,不使用 `df.attrs`
|
||
|
||
```python
|
||
# 错误做法:
|
||
df.attrs["date_index"] = date_index # 切片时 deepcopy 整个字典
|
||
|
||
# 正确做法:
|
||
strategy = Strategy(date_index_dict=date_index_dict) # 作为参数传递
|
||
```
|
||
|
||
**提升**:消除 deepcopy 开销,避免程序卡顿
|
||
|
||
### 8.2 性能对比
|
||
|
||
| 优化阶段 | 回测耗时 | 提升倍数 |
|
||
|----------|----------|----------|
|
||
| 原始版本 | 约 40 分钟 | - |
|
||
| +日期索引 | 约 5 分钟 | 8x |
|
||
| +信号预计算 | 约 1 分钟 | 40x |
|
||
| +信号索引 | **约 40 秒** | **60x** |
|
||
|
||
**注**:其中数据加载约 28 秒,实际回测执行 < 1 秒
|
||
|
||
### 8.3 使用建议
|
||
|
||
1. **大规模回测**(1000+ 股票):必须启用所有优化
|
||
2. **参数优化**:利用多核并行,8核可将 1701 组从 40 分钟降至 5 分钟
|
||
3. **内存优化**:优化后自动 `gc.collect()`,但大数据集建议 16GB+ 内存
|
||
|
||
## 9. 扩展新策略
|
||
|
||
1. 在 `strategies/` 新建文件,例如 `my_strategy.py`,继承 `BaseStrategy` 并实现 `on_bar` 方法;
|
||
2. 在 `config/settings.py` 中修改 `STRATEGY`:
|
||
|
||
```python
|
||
STRATEGY = {
|
||
"name": "MyStrategy",
|
||
"module": "strategies.my_strategy",
|
||
"params": {
|
||
# 自定义参数
|
||
},
|
||
}
|
||
```
|
||
|
||
3. 重新运行 `python main.py` 即可用新策略回测。
|