chore: publish xquant-beginner open book

This commit is contained in:
xingwudao
2026-06-09 16:05:21 +08:00
commit f01f95b905
76 changed files with 8295 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
root: ./
structure:
readme: README.md
summary: SUMMARY.md
+53
View File
@@ -0,0 +1,53 @@
name: Deploy static site
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run docs:build
env:
VITEPRESS_BASE: /xquant-beginner/
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: .vitepress/dist
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
+5
View File
@@ -0,0 +1,5 @@
node_modules/
.vitepress/cache/
.vitepress/dist/
.DS_Store
npm-debug.log*
+56
View File
@@ -0,0 +1,56 @@
import { defineConfig } from 'vitepress'
export default defineConfig({
lang: 'zh-CN',
title: 'XQuant Beginner',
description: '《XQuant:人人都是量化交易员》开源书稿',
base: process.env.VITEPRESS_BASE || '/',
cleanUrls: true,
lastUpdated: true,
themeConfig: {
nav: [
{ text: '阅读', link: '/book/preface' },
{ text: '配套练习', link: 'https://github.com/xingwudao/xquant-learning' }
],
sidebar: [
{
text: 'XQuant Beginner',
items: [
{ text: '项目首页', link: '/' },
{ text: '前言', link: '/book/preface' },
{ text: '开始动手之前', link: '/book/getting-started' },
{ text: '第 1 章:第一次跑通一个完整策略', link: '/book/q1-how-to-profit' },
{ text: '第 2 章:选出你的投资宇宙', link: '/book/q2-what-to-buy' },
{ text: '第 3 章:给 ETF 分钱', link: '/book/q3-how-much' },
{ text: '第 4 章:给策略加规则', link: '/book/q4-when-to-trade' },
{ text: '第 5 章:策略好不好?', link: '/book/q5-how-to-validate' },
{ text: '第 6 章:看清好看的回测', link: '/book/q6-avoid-overfitting' },
{ text: '第 7 章:真的下单', link: '/book/q7-execution' },
{ text: '第 8 章:让策略活着', link: '/book/q8-iteration' },
{ text: '第 9 章:持续寻找盈利机会', link: '/book/q9-daily-work' }
]
}
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/xingwudao/xquant-learning' }
],
outline: {
label: '本页目录',
level: [2, 3]
},
docFooter: {
prev: '上一页',
next: '下一页'
},
lastUpdated: {
text: '最后更新',
formatOptions: {
dateStyle: 'medium',
timeStyle: 'short'
}
},
search: {
provider: 'local'
}
}
})
+46
View File
@@ -0,0 +1,46 @@
Dual License Notice
===================
This repository uses two licenses.
1. Book content and images
The book text and images are licensed under Creative Commons
Attribution-NonCommercial-ShareAlike 4.0 International.
SPDX-License-Identifier: CC-BY-NC-SA-4.0
Human-readable summary:
https://creativecommons.org/licenses/by-nc-sa/4.0/
Legal code:
https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
2. Build scripts, configuration, and automation
Build scripts, configuration files, and automation workflows are licensed
under the MIT License.
SPDX-License-Identifier: MIT
MIT License
Copyright (c) 2026 xingwudao
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+61
View File
@@ -0,0 +1,61 @@
# XQuant Beginner
《XQuant:人人都是量化交易员》开源书稿。
本仓库存放可发布的书稿源码、图片和发布配置。书中配套的
spec 与 notebook 不放在这里,统一维护在:
`https://github.com/xingwudao/xquant-learning`
## 在线发布
本仓库支持两种发布方式:
- GitBook:通过 `.gitbook.yaml``README.md``SUMMARY.md` 同步发布
- 静态网页:通过 VitePress 构建为纯静态站点
## 本地阅读
直接从 `SUMMARY.md` 开始阅读,或打开 `book/` 目录下的章节文件。
## 本地预览静态站点
先安装 Node.js,然后运行:
```bash
npm install
npm run docs:dev
```
构建静态站点:
```bash
npm run docs:build
```
预览构建结果:
```bash
npm run docs:preview
```
构建产物位于 `.vitepress/dist/`
## 配套练习仓库
书中所有动手实验对应的 spec 和 notebook 位于:
`https://github.com/xingwudao/xquant-learning`
阅读正文时,如果看到类似 `q1-how-to-profit/specs/` 的路径,
请到 `xquant-learning` 仓库中查看对应文件。
## 许可证
本仓库采用双许可证:
- 书稿正文与图片:`CC BY-NC-SA 4.0`
- 构建脚本、配置文件和自动化工作流:`MIT`
也就是说,你可以在署名、非商业、相同方式共享的前提下传播和改编
书稿内容;构建相关代码和配置可按 MIT 许可证使用。
+14
View File
@@ -0,0 +1,14 @@
# Summary
* [项目首页](README.md)
* [前言](book/preface.md)
* [开始动手之前](book/getting-started.md)
* [第 1 章:第一次跑通一个完整策略](book/q1-how-to-profit.md)
* [第 2 章:选出你的投资宇宙](book/q2-what-to-buy.md)
* [第 3 章:给 ETF 分钱](book/q3-how-much.md)
* [第 4 章:给策略加规则](book/q4-when-to-trade.md)
* [第 5 章:策略好不好?](book/q5-how-to-validate.md)
* [第 6 章:看清好看的回测](book/q6-avoid-overfitting.md)
* [第 7 章:真的下单](book/q7-execution.md)
* [第 8 章:让策略活着](book/q8-iteration.md)
* [第 9 章:持续寻找盈利机会](book/q9-daily-work.md)
+288
View File
@@ -0,0 +1,288 @@
# 开始动手之前
这一节是**动手之前的一次性准备**工作,一共做两件事:① 把 AI 编程工具和 Python 环境装好;② 顺便学习写一份对 AI 友好的 spec 。
## 准备工具
做量化交易需要的工具其实很少。入门阶段你只需要一个基础环境,本书会用三样东西搭出来:① 数据源(拿什么做实验);② 编程环境(在哪跑代码);③ AI 编程工具(替你写代码)。下面分别介绍。
### 数据源:策略的“原材料”
数据是策略的“原材料”——没有数据,回测无从谈起。本书用两个免费的数据源就够了,对比如表 II-1 所示。
**表 II-1 本书使用的数据源**
| 数据源 | 覆盖范围 | 费用 |
|--------------|--------------------------|------|
| **akshare** | A 股为主,数据全、更新快 | 免费 |
| **yfinance** | 全球股票,接口简单 | 免费 |
本书用 akshareA 股)+ yfinance(美股 / 全球)两个免费源就够了。等你把整个流程跑通、确定要认真做了,再考虑付费数据源。
数据有几个坑要提前知道:**复权方式**(前复权、后复权、不复权,用错了结果完全不同)、**停牌处理**(停牌期间的价格怎么填充)、**幸存者偏差**(只用当前在市的股票回测会高估收益)。这些问题在后续章节遇到时会展开。
### 编程环境:跑策略的“工作台”
有了数据,还需要一个可以运行策略的工作台。你不需要复杂的工程系统,只需要一组基础工具,对照如表 II-2 所示。
**表 II-2 本书使用的编程环境**
| 工具 | 用途 |
|----------------------|----------------------------------|
| **Python 3.12** | 编程语言本体 |
| **Jupyter Notebook** | 交互式编程环境,可以边写边看结果 |
| **常用库** | pandas、NumPy、Matplotlib |
不需要 GPU、不需要云服务器、不需要数据库——一台普通电脑就够了。
> 第 2 章起,本书会用到一个叫 **open-xquant** 的开源框架——它的设计理念和“为什么需要它”会在第 2 章章首详细介绍。本节只列你**现在**要装的工具。
## 安装 TRAE
工具列出来后,开始装。先装 TRAE(本书用于演示 AI 编程的AI 编程工具),再让 TRAE 按一份 spec 帮你装 Python 环境。
### 为什么选 TRAE
本书以 TRAE 为演示工具。如果你已经在用 Cursor、Claude Code、Codex 或其他 AI 编程工具,完全可以继续使用——操作方式相同:在对话框用自然语言结构化写下想法(即spec),AI 将其翻译成 python 程序,执行即可。
主流的 AI 编程工具不少,对比如表 II-3 所示。
**表 II-3 主流 AI 编程工具对比**
| 工具 | 简介 |
|---------------------|---------------------------------------|
| VS Code + Copilot | 老牌编辑器,加装 AI 插件 |
| Cursor | AI 原生编辑器,海外用户多 |
| Claude Code / Codex | 命令行工具,专业开发者常用 |
| **TRAE** | 字节跳动出品,AI-first 设计,中文界面 |
本书选择 TRAE 的原因:
- **AI-firstSOLO 模式**:整个界面围绕“和 AI 对话”设计,不需要你懂编辑器怎么操作
- **模型实力强**:字节有自己的大模型(如豆包),能力不输一线
- **免费可用**:个人开发者免费使用,还能免费接入自定义模型
- **对中国用户友好**:中文界面,国内直连
一句话:TRAE 是目前对零基础、中国用户最友好的 AI 编程工具。
### 四步装好 TRAE
#### 1. 下载 TRAE
访问 TRAE 官网 trae.cn / trae.ai,根据你的操作系统(macOS 或 Windows)下载安装包,下载页面如图 II-1 所示。
![TRAE 官网下载页面](images/getting-started/trae-download-page.png)
#### 2. 安装 TRAE
运行下载的安装包,按照向导提示完成安装。安装过程很简单,一路点“下一步”即可(Windows 安装向导见图 II-2macOS 见图 II-3)。
![TRAE Windows 安装向导](images/getting-started/trae-install-wizard-windows.png)
![TRAE macOS 安装向导](images/getting-started/trae-install-wizard-mac.png)
#### 3. 打开 TRAE,认识界面
安装完成后打开 TRAE。默认是 IDE 模式(如图 II-4 所示),但本书要用的是 SOLO 模式——它对 AI 编程更友好,整个界面围绕“和 AI 对话”设计。切换到 SOLO 模式后的界面如图 II-5 所示。
![TRAE 默认的 IDE 模式界面](images/getting-started/trae-main-interface-ide.png)
![TRAE SOLO 模式界面](images/getting-started/trae-main-interface.png)
SOLO 模式下你只需要关注一个区域:**AI 助手对话框**。后续所有操作都是在这个对话框里完成的。
#### 4. 创建学习目录
在你的桌面上新建一个文件夹(书中示例文件夹叫 `xquant` )。然后在 TRAE 中打开它:菜单 → 文件 → 打开文件夹 → 选择 `xquant`,操作过程如图 II-6 所示。
![在 TRAE 中打开 xquant 文件夹](images/getting-started/trae-open-folder.png)
打开后,你会在左侧文件管理器中看到 `xquant` 这个文件夹名称。现在它是空的,很快 AI 会帮你把需要的文件都创建好。
## 一起写第一份 spec:让 AI 装 Python 环境
到这一步 TRAE 装好了,但 Python 还没装。我们的目标是装 Python 3.12 + 7 个依赖包,并且自动验证每个组件都装对了。
最直接的做法是直接告诉 TRAE:“给我装一下 Python 环境。”
不行——AI 不知道你要哪个版本、用什么工具、要哪些包、跑通的标志是什么。它会按自己的偏好装一套,你的同学按他的电脑装另一套,半年后你们的环境完全不同。
正确的做法是写一份 **spec**——前言里讲过的「任务说明书」,把“做什么 + 做成什么样”清清楚楚交给 AI。这份 spec 我们已经写好放在仓库里了——但与其让你直接抄一遍,不如一起从零把骨架走一遍,等你看明白每一段为什么这么写,再去打开真实文件对照。下面我们一段段写。
### spec 长什么样:四段骨架
一份能让 AI 稳定执行的 spec,通常有四段:
> 📌 **spec 的四段骨架**
>
> 1\. **上下文** — 在什么前提下做这件事(AI 不知道你之前做了什么,要明示)
>
> 2\. **任务** — 一句话点明本次要做什么,不用写怎么做。本书中示例
>
> 3\. **要求** — 具体步骤,AI 自由度越小越可复现
>
> 4\. **结果呈现** — 怎么算成功(最容易被忽略也最重要的部分)
四段是 spec 必须回答的四个问题:**前提是什么 / 做什么 / 怎么做 / 算不算做对了**——哪一段都不能少。
我们一段段把“装环境”这个任务写成 spec。
### 第一段:上下文
我们的状况很简单:刚装好 TRAE,打开了一个空目录,但 Python 一切还没装。这一段写出来:
> **上下文**:我(学员)刚在 TRAE 中打开了一个空目录作为课程根目录。本机是 macOS,尚未安装 Python 或任何依赖。这是课程的第一份 spec——后续spec 都依赖这一份装出的环境。
本节后续四段 blockquote 全部以 macOS 为例展示。Windows 用户按相同骨架,把「macOS」替换成「Windows」、把 mac 命令替换成 PowerShell 命令即可——具体替换在 `specs/spec-01-env-setup-windows.md` 文件里逐行对照,本节无需重复。
这一段示范的是 spec 写作的第一条要点,下面我把它单列出来——本节后续每写完一段 spec,都会附一条对应的「📌 要点」。这是全书第一次出现这种样式块,它和正文并列、可以反复回查。
> 📌 **要点 1:能一句话说清的事就不要写两句** AI 一次能处理的信息有限。你写得啰嗦,最关键的指令容易被它忽略。写 spec 的过程是反复“还能不能再短一点”的过程。
### 第二段:任务
任务段要具体到一句话能说清。“装好 Python 环境”不够具体——什么版本?用什么工具?哪些包?
> **任务**:在当前目录下完成 macOS 量化课程编程环境配置——装 uv → 用 uv 创建 Python 3.12 虚拟环境 → 装 7 个依赖包 → 创建并跑通 check_env.py 自检脚本。
>
> 📌 **要点 2:关键事物要具体到 ID 或版本号** “Python” 不够,要 “Python 3.12”;“包管理工具”不够,要 “uv”;“装一些库”不够,要 “7 个具体的库”。任何 spec 中的关键名词都要具体到不能再具体的程度,AI 才不会按自己的偏好随便选一个。
### 第三段:要求
要求段是 spec 的主体——把“怎么做”拆成步骤。环境配置一共 6 步:确认目录、装 uv、建虚拟环境、装依赖、写 check_env.py、跑 check。以下要求比较具体,是因为编程需要让环境前后一致,避免引起不必要的问题,实际工作中我们不必告诉 AI 应该怎么做,所以你可以完全复制这一段:
> **要求**: 1. **确认当前目录**——在终端运行 `pwd`,记录路径,后续操作都在这个目录下。 2. **安装 uv**——运行 `curl -LsSf https://astral.sh/uv/install.sh | sh`,再 `source $HOME/.local/bin/env`,最后 `uv --version` 应输出版本号。 3. ……(共 6 步,完整内容见 `specs/spec-01-env-setup-mac.md`
完整 6 步加上每一步的命令、check_env.py 全文都在 spec 文件里。这里只挑两个特别值得讲的细节。
**装依赖时为什么要锁版本号?** 比较两种写法:
`uv pip install pandas numpy matplotlib``uv pip install pandas==3.0.* numpy==2.4.* matplotlib==3.10.*`
不锁版本的话,半年后你重新跑 spec,pandas 升到了 3.1,某个 API 改了,书上的图就跑不出来。锁到 `==X.Y.*` 既允许 patch 升级(修 bug 的小版本)又不会跨 minor(可能改 API 的版本),最稳。
> 📌 **要点 3:能锁的都锁住——版本号、日期、随机种子** 量化交易的命脉是“可复现”。你今天跑出来的图,明天还得跑得出来;你电脑上跑的回测,老师电脑上的结果一样;半年后回头检查,依然能复现。任何会随时间漂移的东西(库版本、日期窗口、随机数)都要锁。
环境配置这件事**没有图表可看**——某个包没装好,当下不会报错,等到第 1 章跑实验时才暴露问题。所以验证必须是机械的、固定的、可重复的。所以我们把检查脚本写进了 spec,这就是告诉 AI 使用指定的工具完成某些任务,从而提高 AI 任务的确定性。AI 只能照着抄一份一字不差的,每个学员跑出的报告完全一致——失败时哪一项 FAIL 就是哪一项装错了。
### 第四段:结果呈现
最后一段是“成功长什么样”。一份 spec 没有这一段,AI 跑完你只能凭感觉判断对错。
> **结果呈现** 1. `python check_env.py` 最末两行输出 `结果: 11/11 全部通过!` + `环境配置完成,可以开始课程。` 2. 命令退出码为 0 3. 当前目录下存在 `.venv/` 与 `check_env.py`
>
> 📌 **要点 4:成功的标志要可机械验证** “跑通了” / “看起来对” / “应该没问题”——这些都是不合格的成功标志。一份好 spec 的结果呈现段要让 AI 自己能说“对了”或“错了”,不需要学员凭感觉判断。assert 语句、具体的数字(11/11)、具体的文件名、退出码——这些就是机械验证。
四段拼起来就是一份完整 spec。我们刚才在书里写过的骨架——上下文 / 任务 / 要求 / 结果呈现——和 `specs/spec-01-env-setup-mac.md`(或 `spec-01-env-setup-windows.md`,根据你的系统)里的真实文件一一对应;区别只在于真实文件把 6 步和 check_env.py 全文都写齐了。下一节我们把它交给 TRAE。
## 让 TRAE 跑这份 spec
上一节我们把这份 spec 在书里一段段写完了,现在把它交给 TRAE 真正执行。在打开 spec 文件之前,先看下面这条提示——等下 TRAE 弹窗时你就不会被吓到:
> **💡 提示:关于“沙箱”**
>
> TRAE 帮你装环境时会弹窗询问是否允许“在沙箱外运行”。别紧张:AI 平时是在一个受限的小房间(沙箱)里活动,但现在它要帮你布置整个工作台,需要你给它开一下门。遇到这个提示,直接点“是”或“允许”即可,过程示意如图 II-7 所示。
>
> 如果你打开 spec 文件第一行也写了“所有命令在沙箱外运行”——这就是 TRAE 弹窗问你的原因;不是出错,是预期里的一步。
![TRAE 沙箱提示示意](images/getting-started/trae-solo-sandbox.png)
读完提示,再打开你系统对应的 spec 文件——
- **macOS 用户**:打开 `specs/spec-01-env-setup-mac.md`
- **Windows 用户**:打开 `specs/spec-01-env-setup-windows.md`
把 spec 全部内容复制到 TRAE 的 AI 对话框中,发送。
AI 会自动完成 6 个步骤:装 uv、建虚拟环境、装 7 个依赖包(含 open-xquant 框架)、创建 check_env.py、跑自检。整个过程几分钟。你不需要理解每一步细节,只需要看最后的检查结果。
### 验证结果
如果一切顺利,你会在 TRAE 的终端中看到类似这样的输出:
==================================================
XQuant 课程环境检查
==================================================
[OK ] Python 3.12.x
[OK ] 虚拟环境已激活
[OK ] jupyter
[OK ] pandas
[OK ] numpy
[OK ] matplotlib
[OK ] akshare
[OK ] yfinance
[OK ] open-xquant
[OK ] jupyter 命令可用
[OK ] akshare 数据源连通(获取到 5847 条数据)
--------------------------------------------------
结果: 11/11 全部通过!
环境配置完成,可以开始课程。
实际终端输出如图 II-8 所示。
![环境检查全部通过](images/getting-started/env-check-success.png)
如果有任何项目显示 `[FAIL]`,**不要紧张,也不要自己改命令重试**——把完整的输出结果复制给 TRAE 的 AI 助手,告诉它“环境检查有失败项,请帮我修复”。AI 会照着 spec 里“故障恢复”那一节的对照表逐项排查。
> **💡 关于错误处理的原则**
>
> 整本书你都不必去**啃** Python 报错——遇到任何报错,**把完整信息复制给 AI 助手**就好。AI 排查报错的能力远比让初学者读 stack trace 强。这是本书的承诺:你只写 spec、看结果,调试交给 AI。
## 你刚刚学到了什么
### 你做了什么
刚才的过程其实包含两件事:
**环境装好了**——这件一次性的事做完了,往后不用再碰。 ② **你看到了一份 spec 是怎么从零写出来的**——四段骨架在你眼前展开了一遍,四条核心要点各自示范了一次。
第二件事比第一件重要。
### 7 条 spec 自查清单
刚才本节示范了 4 条要点(写在每段 spec 之后的「📌 要点 1-4」)。把这 4 条加上 3 条通用补充,就构成下面这份“写完 spec 过一遍”的自查清单——任何一条没做到都是问题:
| \# | 自查项 |
|----|----|
| 1 | **四段都齐了?** 上下文 / 任务 / 要求 / 结果呈现,一段都不能少 —— 结构骨架 |
| 2 | **任务一句话能说清吗?** “做一下数据分析” ❌;“获取沪深 300ETF 历史数据并可视化” ✅ —— 来自要点 1 |
| 3 | **关键事物具体到 ID 了吗?** 标的代码、库版本、文件名、参数名都点名 —— 来自要点 2 |
| 4 | **能锁的都锁了吗?** 库版本、日期窗口、随机种子——任何会随时间漂移的都要锁 —— 来自要点 3 |
| 5 | **结果呈现有“机器能判断的标志”吗?** assert、具体数字、文件名、退出码 —— 来自要点 4 |
| 6 | **写的是“做 X”而不是“不要 Y”吗?** 正向指令比否定句对 AI 更稳定 —— 通用补充(首次出现) |
| 7 | **长度合理吗?** 大多数 spec 不超过 80 行(环境配置这种特殊情况除外) —— 通用补充(首次出现) |
第 6、7 两条本节没有专门演示,会在第 1 章后续 spec 写作中遇到时再补讲。这 7 条会跟着你整本书——每写一份新 spec,最后都过一遍这个清单。
### 你需要做什么
后续每一章都会让你写或改一份 spec。开始时书里会带你一段一段写(像本节这样),后面会渐渐放手——到第 6、7 章你应该能自己写完整的 spec,到第 9 章你应该能根据问题写新 spec。
> **💡 spec 写作能力的成长曲线**
>
> 抄 → 改 → 拼 → 写。第 1 章基本是“抄 + 看懂”;第 2-3 章是“改其中几个参数”;第 4-6 章是“把多个 spec 拼成新流程”;第 7-9 章是“看到一个量化问题,自己写一份新 spec”。每一步都不用着急。
## 准备就绪
恭喜,你现在已经具备了开始量化交易学习的全部条件。
### 速查表
前言和这一节涉及的核心概念汇总在表 II-4 里。
**表 II-4 动手前的核心概念速查**
| 概念 | 含义 | 类比 |
|----|----|----|
| spec(任务说明书) | 给 AI 的结构化指令 | 你说清楚要做什么,AI 负责执行 |
| spec 四段骨架 | 上下文 / 任务 / 要求 / 结果呈现 | 一份 spec 必须回答的四个问题 |
| TRAE | 本书演示用的 AI 编程工具 | 也可以换成 Cursor / Claude Code 等 |
| 量化交易 | 用数据和规则做交易决策 | 把“我觉得”变成“数据显示” |
| 量化飞轮 | 候选 / 组合 / 执行 / 归因,循环往复 | 越转越聪明的轮子 |
| 三字要点 | 飞轮每一格都要走的三步:做 / 看 / 疑 | 想法要规则、结果要指标、指标要被怀疑 |
### 下一步
**第 1 章**,我们会用一个真实的 ETF 带你**走完整个飞轮一圈**——从最简单的定投开始(做),看它能不能赚钱(看),尝试让它“聪明”一些(再做、再看),最后亲眼看到“回测时间段里收益率优美、换段时间就亏损”这种典型情况(疑)。 准备好了吗?翻到下一章,开始你的第一次飞轮转动。
Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

+176
View File
@@ -0,0 +1,176 @@
# 前言
## 为什么写这本书
你打开这本书,大概率是因为你听说过“量化交易”或者“量化投资”。多年以来,媒体都在传递一类观点:量化投资具有卓越的投资业绩表现。然后,如此卓越的量化投资通常只服务于高净值用户。普通人常常因为净值门槛被阻碍在量化投资的门外。既然如此,普通个人投资者如何才能应用量化投资呢?既然无法借助他人而使用量化投资,那么唯一的途径就是去自学量化投资。中国目前有2.4亿个人投资者,按照千分之一的人想学习量化投资而言,也至少有24万人正走在学习量化投资的路上。但这些尝试学习量化投资的人在学习的路上走得并不算顺利。究其原因,与量化投资相关的晦涩的金融术语、复杂的数学公式和天书般的程序代码阻挡了普通投资者的量化投资学习之路。有少数人能迈过以上几个门槛,可是新的阻碍又重新出现。学完了量化投资内容,在面对真实市场时,如何把量化投资和真实市场结合,如何抓住量化投资应用到金融市场的重点之处,如何把握把时间花在量化投资的哪些环节上?要解决这些问题,只有建立一套科学、完整、可执行的量化体系才行,否则,面对波诡云谲的金融市场,我们无法持续构建出有效的量化策略,也就无法真正享受量化投资的红利。
我自己是算法工程师出身,过去十多年一直做 AI 和算法相关的工作,具备了数学和编程基础。我接触量化投资的方式,是从业余时间跟着教程跑过几个简单的量化策略 demo开始。之后我发现工程思维里“拆问题、提假设、用数据验证”这一套方法在量化投资领域也成立。这不过,这里要解决的问题是金融领域的问题。三个拼图,我已经有了两块,只要补足金融领域的知识短板,我就可以继续做量化投资了。我决定深入下去,直到在这条量化投资路上找到了「一硕」——一位有十几年量化交易经验的搭档。他正在尝试用 AI 来实现量化投资,他是统计学和财务出身,具备数学和金融的基础,我们两个的能力互补。之后,他把自己开发的量化投资策略毫无保留地分享给我,我们一起把它拆开:搞清楚每一个想法是怎么产生的、怎么用数据验证、验证之后如何在工程层面真正执行,分析执行时又会遇到哪些大大小小的问题。整个拆解的过程,让我获益匪浅,这也是本书的缘起。
这本书能写出来,还有一个 2025 年才成熟的关键基础——**AI 编程能力的跃迁**。在 2024 年之前,“用 AI 替你写量化代码”还是不靠谱的:AI 写出来的代码经常跑不通、逻辑反复出错、库版本对不上。但从 2025 年下半年开始,全世界的大模型在编程方面的能力突飞猛进,以 Cursor、TRAE、Claude Code 为代表的 AI 编程工具也在持续迭代,两者叠加,AI 编程发生了一轮明显的能力跃迁——能读懂自然语言写的需求、能把复杂任务拆成小步骤、能自己调试、能记住上下文连续工作数十小时。
**这意味着什么?这意味着阻挡大家尽早动起来的门槛降低了。这也是本书提供的方法——“你用自然语言描写想法、AI 替你写代码做实验”——在 2025 年才真正可行。** 我们等到这扇窗开了,才动笔把这套从想法到落地的完整过程写成书。
## 这本书要解决什么问题
这本书要解决的,是量化投资初学者可能正在遇到的三个主要问题——
**问题 1:想做量化,但卡在“不会编程”,迟迟迈不出第一步。** 你有想法,甚至有过策略雏形,但一想到要写代码、搭环境、调库,就放弃了。这本书让你**用自然语言**表达策略思路,写代码、跑程序、画图表全部交给 AI 助手。你只需要清晰地说明“要做什么、做成什么样”。
**问题 2:看了很多量化内容,专有名词高大上,要么看不懂、要么落地难。** 各种指标、术语、模型铺天盖地,但不知道哪些是关键、哪些只是“看起来很专业”。这本书帮你回到最底层:用**数据和逻辑**来重新理解投资,这就是量化思维。
**问题 3:尽管学会了很多量化策略,但始终没有搭建出随着市场变化而进化的系统。** 你可能看过教程、读过文章,但内容都是碎片:这里学一点策略,那里学一点工具,没有一根主线把它们串起来。这本书带你从头做一遍,**亲手搭出一个成体系、可迭代的量化交易系统**。
**这本书适合两类人**:① **有兴趣 + 没实操**:刚刚对量化产生兴趣、想试试又不知道从哪开始的人;② **有尝试 + 无系统**:读过几本量化书、试过别人的示例代码,但始终没做出过一套自己的策略的人。
**要花多少时间?** 如果你按“周末半天 + 工作日两个晚上各 1.5 小时”的节奏走(每周大约 5-6 小时),**2-3 个月**可以走完全书。当然你也可以加快——重点不是速度,而是每章动手做完,能完整解释自己做了什么。
走完这条路,首先你会亲手指挥 AI 实现一份**完整的策略**——一份能告诉你“什么时候买、什么时候卖、买多少”的可执行规则,消除了动手的门槛;其次你建立了一个完整的量化交易体系,以后再实践和学习都会有的放矢。
至于具体会带你走过哪些事——下一节先给你一张地图,再带你走。
## 这本书的整体框架
在带你做实验之前,先把全书的骨架立起来——这样后面 9 章动手做的每一步,你都知道自己在地图的哪个位置。
量化交易听起来复杂,但骨架其实很简单:**它只是 4 件事循环做、每件事走 3 步**。看明白这“4×3”,你就有了一张全书都能用的地图。
### 量化飞轮:候选 → 组合 → 执行 → 归因
任何一个量化策略,都是在循环回答四个问题:
- **候选层 · 选什么** —— 从几千上万只股票/ETF 里**先有鱼塘,再捞鱼**——圈出一小撮值得长期跟踪的。
- **组合层 · 买多少 / 何时买卖** —— 从池子里挑几只,决定每只买多少、什么时候买卖,这是依据数据做决策的关键步骤。
- **执行层 · 真的下单** —— 把“数据决策”变成“真实持仓”,把组合变成真实的买卖订单,并处理价格滑动、成本、风险。
- **归因层 · 对比和打分** —— 钱到底是赚了还是亏了?和更简单的做法相比值不值?是靠本事、还是运气在赚钱?靠运气赚的钱迟早得还给市场。
这四层构成了一个完整的量化体系,也是一个**飞轮**——归因层得出的结论,会反过来修改下一轮的候选和组合,飞轮越转越准(如图 I-1 所示)。
![量化飞轮:候选 → 组合 → 执行 → 归因 → 反哺候选](images/preface/fig-quant-flywheel.png)
> **📌 小词典:什么是飞轮(Flywheel)?**
>
> 飞轮就是一个能持续转动的轮子——一开始要使劲推,一旦转起来,靠惯性自己就能加速。在量化里,“飞轮”意味着:策略不是“做一遍就完事”,而是每转一圈都比上一圈更聪明一点。
### 三字要点:做 / 看 / 疑
四层构成了一个完整的量化体系,在构建每一层时,还要遵循一套动作,才是真正的量化交易系统。比如说,同样是“做组合层”,你可以凭感觉拍脑袋,也可以用数据说了算,结果当然天差地别。所以**每一层**里,都得走完同样的 3 步:
- **做** —— 把动作写成程序,成为可重复执行的规则。不允许“我感觉茅台不错”,要写成“近 60 日均价上穿当日收盘价就买”
- **看** —— 把目标和结果都要定义出可比较的指标。不允许“感觉”,不允许“看起来还行”,要写成“年化 12%、最大回撤 18%”这种带数字的判断。只要数字上没达成目标,就不能靠“再等等看”或“应该没问题”改变结论。
- **疑** —— 永远对指标保持质疑,防止自己被指标欺骗。不允许“回测说赚了就信”,要问:“换一段时间还行吗?参数稍微调一下还稳定吗?”
这三个动作是**层层递进**的:先**做**出来,才有结果可**看**;**看**到数字了,才有东西可**疑**。
> **📌 小词典:为什么“疑”也是量化的一部分?**
>
> 量化交易中,最严重的不是错过了收益,而是乐观导致亏损。所以始终对回测结果保持怀疑就非常有必要。一个回测能跑出年化 50%——但只在那 5 年的特定数据上跑得出来,换一段时间年化就掉到接近 0。**会做、会看,但不会疑,就会被自己的策略欺骗,就会在某些时候把赚到的钱加倍还回去。** 这就是为什么“疑”和“做”“看”同等重要,甚至更重要。全书第 6 章会专门用一整章训练这个动作。
把四层飞轮和三字要点叠起来,就是完整的量化体系,也是这本书要在你脑子里建起来的整张地图(如图 I-2 所示)。
![飞轮的每一格都要走“做、看、疑”三步](images/preface/fig-flywheel-with-three-actions.png)
### 这本书的 9 站旅程
我们将用 9 个站点走完这张地图的全程。接下来的 9 章,就是带你在这张地图上走 9 站。每一站对应飞轮的哪个位置、主要练哪个动作,列在表 I-1 里。
**表 I-1 9 章主题与飞轮位置对应**
| 章节 | 问题 | 飞轮位置 | 主练动作 |
|---------|--------------------------------|----------------------|--------------|
| 第 1 章 | 第一次跑通一个完整策略 | 走一遍完整流程 | 做 + 看 + 疑 |
| 第 2 章 | 选出你的投资宇宙:3 只 ETF | 候选层(选什么) | 做 |
| 第 3 章 | 给 ETF 分钱:3 种分法实测 | 组合层(买多少) | 做 |
| 第 4 章 | 给策略加规则:调仓、止损、止盈 | 组合层(何时买卖) | 做 |
| 第 5 章 | 策略好不好?4 个评估角度 | 归因层(对比和打分) | 看 |
| 第 6 章 | 看清好看的回测:别被自己骗了 | 怀疑你的回测 | 疑 |
| 第 7 章 | 真的下单:跨到实盘 | 执行层(真下单) | 做 + 看 |
| 第 8 章 | 让策略活着:监控、诊断、迭代 | 评估并反哺起点 | 疑 + 做 |
| 第 9 章 | 持续寻找盈利机会:因子研究入门 | 找下一个赚钱机会 | 做 + 看 |
注意第 1 章——它一上来就带你**走完飞轮一圈**,而且故意让你在终点遇到一种典型陷阱:**回测里看着赚钱,换一段时间就不行了**。这不是设计失误,是**设计意图**:让你在第 1 章就亲身经历什么叫“被自己的回测骗”,从此真正记住“疑”这个动作的分量。
九站走完,飞轮的 4 格你都站过、做/看/疑 3 个动作你都练过。这就是你**带得走**的量化思维。
## 你能获得什么能力
读完这本书,你学会的不是具体的策略,而是一套不断找到新策略的方法。总结一下,你会建立起四类核心能力:
- **会用 AI 编程**:学会用自然语言结构化描述任务,让 AI 准确地翻译成代码——你将学会和 AI 协作做事,这是 AI 时代的核心能力。
- **养成量化思维**:把模糊的想法、抽象的概念转化成定义明确的指标和清晰无误的规则,用数据检验假设是否成立。
- **会做策略实验**:在 4×3 的量化框架下,你将从数据获取、回测、对比基准、参数扫描,到样本外验证,全都亲手跑一遍。
- **知道如何迭代**:你不仅有了量化体系和做 / 看 / 疑 三个动作,你还会知道这个体系以后会遇到哪些坑,从而长期迭代下去。
## 本书的核心理念
跟着全书走完,你会发现,本书自始至终在贯彻三个理念:
**第一:问题驱动。** 量化交易本质上是做研究——就现象提出问题,就问题提出假设,再用实验去验证假设。每一章都是一个大问题,引出若干小问题,回答了这些问题再引出一个新的大问题,来到新一章。跟随这些问题,我们将走完完整的 4×3 量化地图。
**第二:先动手,再认知。** 量化交易光看不动手绝学不会——你必须亲自走一遍。书中在一个接一个问题驱动下,都有对应的实验来解答这些问题;实验结果出来了,我们才会开始引入一些概念建立认知。
**第三:0 编程。** 全书都采用 AI 编程的方式做实验,因此我们需要把想法详细描述给 AI——这称之为任务说明书(specification,简称 spec)。全书一步一步告诉你如何写一份好的 spec,并提供 spec 和代码供你在动手时参考。
> **📌 小词典:什么是 spec?**
>
> spec 是 specification 的简写,可以理解为「任务说明书」。你不需要掌握任何编程语法,只需要用大白话把两件事说清楚: **要做什么** (在量化交易中就是“你想做什么策略”); **做成什么样** (在量化交易中就是“策略应该如何运行”)
>
> AI 会根据这份说明,自动完成编码、执行和输出。
## 怎么使用这本书
这本书不是用来“看完”的,而是用来“跟着做”的。**它的核心学习方式是循环**:
> 提出问题 → 动手实验 → 看结果、理解 → 进入下一个问题
每一章都按这个循环组织。
你可以在一开始,从我们提供的 GitHub 仓库中获得参考的 spec 或者 Python 代码,完成简单实验,然后模仿我们提供的 spec,根据方法编写自己的 spec,或者修改已有 spec,做新的实验。
实验结果,你需要看懂其含义,书中提供了实验结果的解读,你也可以提出自己的解读,和你的 AI 助手讨论。这也有助于建立你的量化思维。鼓励读者对本书方法和解读充满“怀疑”,这正是三字要点要训练的。
### 每一节怎么走
每节正文都会**带你一段一段把 spec 写出来**——上下文、任务、要求、结果呈现四段,对应的方法点(📌 要点)嵌在演示过程中讲,不集中讲。完整的 spec 文件在 GitHub 仓库的 `specs/` 目录里,跑实验时直接复制给 AI。
**你的目标不是“复制 spec”,而是看懂每段为什么那样写,逐步学会自己写。** 每节按这五步:
1. 阅读章节正文,理解这一节要解决什么问题
2. 跟着正文一段段写 spec,看每段对应的方法点
3. 打开仓库里完整的 spec 文件,复制给 AI 让它执行
4. 看运行结果(图表、关键数字)+ 章内的解读
5. 遇到错误,把信息发给 AI 让它排查
### 关于“自己写 spec”
第一次听到要“自己写 spec”,你可能有一点顾虑——“我完全不会写 spec,是不是从一开始就跟不上?”
**别担心。** 这本书把 spec 能力分**抄 → 改 → 拼 → 写**四阶段,一级一级铺给你:
- **第 1 章**:抄——跟着书里的段段演示走一遍,把完整 spec 复制给 AI,观察它的执行过程,先体会跑通的手感
- **第 2-3 章**:改——改 spec 的参数(换标的、换时间窗),结构本身不变
- **第 4-6 章**:拼——把多个已有 spec 的片段组合起来,做新的实验
- **第 7-9 章**:写——为自己的投资想法从零写 spec
正式开始之前,**开始动手之前**那一章会带你完整地写一份 spec——通过让 AI 帮你装环境这件事,把 spec 的四段骨架(上下文 / 任务 / 要求 / 结果呈现)演示一遍,并归纳出一份 7 条自查清单,帮你建立基本心智模型。**这份 7 条自查清单从第 1 章「抄」开始就一直陪你走到第 9 章「写」**——只不过你用它的方式会从“对照检查”逐步变成“下意识落笔”。从第 1 章起,每节正文里出现的方法点都用「📌 要点」标出来——你不用刻意记,看到一次记一次,跟着写到第 7-9 章自然就内化了。
### 两类辅助内容
全书出现两类辅助内容:
- **📌 小词典** —— 概念解释,反复使用的术语随时回查
- **📌 要点** —— spec 写作的方法点,嵌在写 spec 演示的段落里,看到一次记一次
### 配套资源
本书的免费资源包(扫书末二维码即可拿到),包含:
- 所有章节的示例 spec
- 配套的 Jupyter Notebook(含可运行代码 + 完整运行结果)
- 第 2 章起使用的量化框架 open-xquant(详细介绍见第 2 章工具升级一节)
也可以从 GitHub 直接获取(适合熟悉 git 的读者):
- 课程仓库: https://github.com/xingwudao/xquant-learning
- 量化框架:https://github.com/xingwudao/open-xquant
地图就到这里。下一节“开始动手之前”是一次性准备——把 AI 编程工具和 Python 环境装好,顺便用“装环境”这件事完整地写一份 spec。一次配完,第 1 章我们就直接跑实验。
+673
View File
@@ -0,0 +1,673 @@
# 第 1 章:第一次跑通一个完整策略
前言中那张地图——**飞轮的四层(候选 / 组合 / 执行 / 归因)+ 三字要点(做 / 看 / 疑)**——在这一章会被你**亲手走一圈**。你把“做 + 看 + 疑”三个动作都练一次,飞轮也在你手里转完第一圈。
7 个动手实验串成一条主线。先从最简单的定投起步,把动作写成规则(**做**),看它能不能赚钱(**看**)。再给它加一条判断让它“聪明”一点,看判断有没有让结果更好(**再做、再看**)。最后扫描一圈参数,亲眼看到量化交易最常见的失效场景——**回测里看着赚钱,换一段时间就不行了**(**疑**)。
这就是为什么本书的口号是「做要规则、看要指标、还要怀疑你的指标」 。
**路线图**
本章 5 节实验 + 1 节总结,串成第一次完整飞轮闭环,路线如表 1-1 所示。
**表 1-1 第 1 章路线图**
| 节 | 内容 | 实验 |
|----|----|----|
| 1.1 最简单的量化策略长什么样? | 从“笨定投”起步——拿到沪深 300ETF 数据,跑一次最朴素的每月固定金额买入回测 | 2 |
| 1.2 我的收益算好还是算差? | 引入“基准”——和“什么都不做的买入持有”对比,理解 Alpha 与 Beta | 1 |
| 1.3 怎么才能赢过市场? | 给定投加一条均线判断,看见“加判断”的双刃剑 | 2 |
| 1.4 找到“最优参数”就能盈利了? | 暴力扫描 24 个均线参数,找历史收益率最高的“冠军” | 1 |
| 1.5 真相时刻 | 把数据切成训练 / 测试,看冠军参数到了新数据上是否真有本事 | 1 |
| 1.6 本章总结 | 把 7 个实验串成一条策略进化路径,带走第一次飞轮闭环 | 0 |
| 拓展阅读 | 量化在金融世界的位置;钱从哪来 | — |
## 1.1 最简单的量化策略长什么样?
这一节我们走完飞轮的第一步——**做**:把策略写成规则、跑一次回测、看结果。
我们从最简单的策略开始:**定投**。
没错,就是你可能在银行 App 上见过的那个“定期定额投资”。每个月发了工资,拿出固定的一笔钱买入,不看涨跌、不做判断,机械地重复。这算量化吗?当然算——“量化”只要满足两个条件就够了:规则明确 + 执行可重复。定投完全够格。
不过,我们定投的不是某只个股,而是 **ETFExchange-Traded Fund,交易所交易基金)**。你可以把 ETF 理解成一份“指数套餐”。我们选择的是沪深 300ETF(代码 510300),它把 A 股市场上规模最大的 300 家公司打包在一起——银行、白酒、新能源、互联网,各行各业的龙头都在里面。你买一份 ETF,就等于同时持有了这 300 家公司的股票,不需要你自己去挑选哪家公司好、哪家公司差。
为什么选 ETF 而不是某只股票?因为选股本身就是一门极深的学问。个股可能暴涨,也可能退市归零。而 ETF 跟踪的是整个市场的平均表现——单家公司出问题,其他 299 家还在。对新手来说,一键买入整个市场,是最省心、也最不容易犯大错的起点。
所以我们的定投规则就是:**每月第一个交易日,用固定金额(1000 元)买入沪深 300ETF。** 没有任何判断,没有任何预测,不看新闻,不猜涨跌。就这么简单。
本章 7 个实验都写在同一个 notebook 文件 `q1-strategy.ipynb` 里——第一份 spec 会让 AI 帮你创建它,后面 6 份直接接着写,变量、函数、图都在同一个 notebook 里累积下来。
### 动手实验 1:获取历史数据
接下来是你的第一个动手实验——拿到沪深 300ETF 5 年的历史数据。我们一起把这份 spec 写出来。
#### 起草上下文
我们刚装好环境,notebook 是空的,什么都还没做。上下文一句话说清:
> **上下文**:学员已完成环境配置(Python 3.12 + 虚拟环境 + 依赖包已安装)。这是课程的第一个操作步骤。
>
> 📌 **要点**:上下文段交代“前提是什么”。AI 不知道你之前做了什么,要明示——否则它可能假设环境已有现成数据,跳过下载步骤。
#### 起草任务
任务段要具体到一句话能说清。“获取数据”是反例——什么数据?什么时间?什么形式?`q1-strategy.ipynb` 是这份任务说明的交付物,这个文件就是一个 Python notebook 文件。notebook 文件是一种交互式的编程源代码,区别于非交互式的 Python 代码,我们在这份文件中可以看到策略的探索过程。
> **任务**:在当前工作目录创建 `q1-strategy.ipynb`,获取沪深 300ETF510300.SS)最近 5 年的历史日线数据并可视化。
>
> 📌 **要点**:关键事物要点名到 ID。“沪深 300”不够——A 股有沪深 300 指数(000300.SH)也有跟踪它的 ETF510300.SS、510310.SS、510330.SS),AI 不知道你想要哪个。spec 里所有关键名词都要具体到不能再具体。
#### 起草要求
要求段是步骤主体——AI 自由度最低的部分。我们规定四件事:
> **要求**
1. 导入 yfinance, pandas, numpy, matplotlib.pyplot
2. 跨平台中文字体 fallback 链:`plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'STHeiti']` + `plt.rcParams['axes.unicode_minus'] = False`
3. yfinance 获取数据:标的 `510300.SS`、起始 `2021-01-01`、参数 `auto_adjust=True, multi_level_index=False`
4. 数据存到变量 `df`,索引为 DatetimeIndex,列名含 Close
> 📌 **要点**:每个非默认参数都要点名。yfinance 默认会自动调整价格、用多级列名——这两个参数不写出来,AI 可能用默认,结果跟书里不一致。**不要让 AI 揣摩你的偏好。**
#### 起草结果呈现
最后是“成功长什么样”——这是 spec 最容易被忽略也最重要的部分:
> **结果呈现**
1. 打印数据的前 5 行
2. 打印数据时间范围(起始日期 到 结束日期)和总行数
3. 画一条收盘价折线图,标题为「沪深 300ETF (510300) 收盘价走势」
> 📌 **要点**:结果呈现要让 AI 跑完自己能说“对了 / 错了”。“画一张图”是反例——AI 可能画 K 线、画柱状图、画散点图。要明确到具体的 X/Y 轴、标题文字、变量名。
完整 spec 在 [`specs/spec-01-get-data.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-01-get-data.md)——把它复制到 AI 编程工具的对话框,弹窗选「允许」。spec 文件用 Markdown 写成(`#` 标题、`-` 列表、反引号代码——都是给 AI 看的格式,完整复制就好)。
AI 助手执行完毕后,你会在你的文件夹中看到多出一个叫 `q1-strategy.ipynb` 的文件。第一次双击打开时,TRAE 会询问你使用什么 Python 核,意思就是:这份 notebook 交给哪个 Python 环境来运行。我们在这里会选择在“开始动手之前”这一章中初始化的 Python 3.12 环境,如图 1-1 所示。
![选择 notebook 使用的 Python 核](images/q1-how-to-profit/notebook-python-core.png)
打开 notebook 文件后,源代码是一个一个单元格从上向下构成。这个界面中有几个常用按钮,如图 1-2 所示。
1. 单步运行:每一个单独的代码单元格,可以单独运行这一步。运行结果会在这个单元格下方出现。
2. 全部运行:整个notebook从上向下,逐一全部运行,直到出现错误为止。每个单元格的运行结果都会紧接着出现在代码单元格下方,运行错误也在下方。
3. 重新启动:重新启动所选择的python核。通常,在初始化的python环境中安装了新的python包,或者更新了某个旧的python 包,会重新启动。
4. 当前 python 核:显示当前 notebook 在使用哪个 python 核。
![notebook 常用按钮](images/q1-how-to-profit/notebook-buttons.png)
回到你的第一个成果上来,这时候你的 notebook 里应该出现了一张折线图和一些打印输出。AI 帮你做了这些事:从雅虎财经下载了沪深 300ETF(代码 `510300.SS`)从 2021 年 1 月至今的每日价格数据,打印了前 5 行让你看看数据“长什么样”,然后画了一张收盘价走势图,如图 1-3 所示。
![沪深 300ETF 收盘价走势](images/q1-how-to-profit/01-price-history.png)
### 读懂你的第一张图
看看这张收盘价走势图。
**先看整体趋势**:价格总体是在涨还是在跌?有没有明显的“大起大落”?你大概率会看到价格经历了几轮上涨和下跌的周期,不是一条直线往上走,也不是一路向下。
**再看波动范围**:最高点和最低点大概差多少?价格在什么区间内震荡?你会发现,波动幅度可能比你想象的大——同一只 ETF,不同时间点买入,结果可能天差地别。
**这说明什么?** 价格有涨有跌,波动是常态。你不可能买在最低点、卖在最高点——没有人能做到。既然择时这么难,那有没有一种方法,可以不依赖“买在最低点”也能参与市场?定投就是这个思路。
有了数据,一个自然的问题就出来了:**如果从几年前开始,每月固定买入一些,到现在是赚了还是亏了?** 我们来用历史数据回答这个问题。
### 动手实验 2:定投回测
在真正投钱之前,我们当然想先知道这个策略靠不靠谱。怎么办?用历史数据模拟一遍。
这就是 **回测(Backtest** 的概念——用历史数据模拟“如果当时按某个规则操作,结果会怎样”。就像在脑海中“重播”历史,看看你的策略在过去能不能盈利。回测不能预测未来,但能帮你验证一个想法是否靠谱。
我们一起把这份回测 spec 写出来。这次重点看两件新东西:**spec 之间怎么接续**,以及**复用代码怎么封装成函数**。
#### 起草上下文
这次的上下文不再是“什么都没有”——上一份 spec 已经留下了 `df` 变量。我们直接接着用:
> **上下文**:在 `q1-strategy.ipynb` 中已有上一步的代码:使用 yfinance 获取了沪深 300ETF510300.SS)的日线数据,存储在变量 `df` 中,索引为 DatetimeIndex,包含 Close 列。
>
> 📌 **要点**:spec 之间会有“接续关系”——前一份 spec 的输出是后一份的输入。上下文段把这条链条写明(变量名、形状、关键列)。这样 AI 不会重新下载数据、也不会假设一个不存在的列。
#### 起草任务 + 要求
任务一句话:“实现简单定投回测——每月第一个交易日用 1000 元按收盘价买入”。要求段重点在两件事:
> **要求**
1. 把回测逻辑封装为函数 `backtest_dca(df, monthly_amount=1000)`,返回 DataFrame(**后续步骤会复用此函数**)
2. 实现:按月分组取每月第一交易日 → 每月份额 = 1000 / 收盘价 → 累计份额逐月累加 → 月末市值 = 累计份额 × 当月最后收盘价 → 累计收益率 = (市值 - 成本) / 成本
3. 返回 DataFrame 含列:`total_cost, total_shares, portfolio_value, return`
> 📌 **要点**:复用型代码必须封装成函数。“后续步骤会复用此函数”这一句话是给 AI 的硬约束——不要写一段一次性脚本,要写一个有清晰入参的函数。后面的实验 5 会基于它派生 `backtest_dca_with_ma`,函数没封装好整条 spec 链就接不上。
>
> 📌 **要点**:返回的 DataFrame 列名要点死。这里写的是 `total_cost, total_shares, portfolio_value, return`——四个名字一字不差。后面好几节实验都要从 `result_dca.return` 这种路径取数,列名只要换成 `ret` 或 `cum_return`,后面所有 spec 都得连带改。**返回值的字段名是 spec 链的“API 契约”,锁死才能让后面的 spec 写起来一行不差。**
#### 起草结果呈现
> **结果呈现**:打印投资期间 / 投资月数 / 总投入金额 / 最终市值 / 最终收益率。画两张图(上下排列,figsize 12×8):上图持仓市值 vs 累计成本(盈利绿、亏损红),下图累计收益率(零线上下绿/红填充)。
完整 spec 在 [`specs/spec-02-dca-backtest.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-02-dca-backtest.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里会出现一组数字和两张图。AI 帮你做了这些事:按月分组找到每个月的第一个交易日作为定投日,用 1000 元除以当日收盘价算出每月能买多少份,逐月累加持有份额和投入成本,最后计算每月末的持仓市值和累计收益率。
定投的机制有一个天然的好处:价格低的时候,同样 1000 块钱能买到更多份额;价格高的时候,买到的份额更少。就像去菜市场,菜贵的时候少买点,菜便宜的时候多买点。
AI 助手还画出了上下两张图(如图 1-4 所示):
- **上图「持仓市值 vs 累计成本」**:蓝色实线是你的持仓市值,它会随市场波动上下起伏;另一条线是你的累计投入成本,它稳步上升(毕竟每个月都在投钱)。两条线之间的区域用颜色标注——绿色代表盈利的时段,红色代表亏损的时段。
- **下图「累计收益率」**:更直观地看到收益率随时间的变化曲线。零线以上是赚,以下是亏。
![定投回测结果](images/q1-how-to-profit/02-dca-backtest.png)
### 读懂你的回测结果
先看打印出来的数字:总投入是多少,最终市值是多少,收益率是正还是负?
再看两张图。你大概率会发现,收益率在正负之间来回穿越——有一段时间盈利,过一阵又亏回去,然后可能又赚回来。这不是你的策略有问题,而是市场本来就这样。定投不保证盈利,也不保证亏损。它只是一种“分散买入时点”的方法,避免了把所有钱一次性投在最高点的风险,降低了运气的成分。
不管你看到的最终结果是正收益还是负收益,有一件事比结果本身更重要:**你怎么知道这个结果算好还是算差?**
赚了 5%,听起来还行?但如果同期银行理财能给你 4%,那你承受了股市的波动,只多赚了 1%,值得吗?亏了 3%,听起来很糟?但如果同期大盘跌了 15%,你其实已经大幅跑赢了市场。
这就像考试拿了 80 分——你得知道全班平均分才能判断自己考得好不好。我们需要一个参照物。
## 1.2 我的收益算好还是算差?
上一节最后留下了一个问题:一个收益率数字本身,不能告诉你任何事。赚了 20% 是好是坏?亏了 5% 是糟糕还是其实还行?这取决于你跟谁比。
你需要一个 **基准(Benchmark**。基准就是“全班平均分”——你不知道 80 分算好算差,但如果全班平均 60 分,你就知道自己考得不错;如果全班平均 95 分,那 80 分就得反思了。投资也是一样。你的收益率只有跟某个参照物放在一起看,才有意义。
跟谁比?其实有三种不同层次的基准:
- **绝对基准**:你能接受的最低收益率。比如“年化至少 5%,否则我还不如买理财”。这是你的个人底线。
- **相对基准**:市场指数。你的策略是赢了大盘还是输了大盘?赢了说明你有超额收益,输了说明你还不如买指数基金。
- **策略基准**:同类策略中最简单的那个。择时策略要和“买入持有”比——你费心选时机,总得比什么都不做强吧?权重优化策略要和“等权重”比——你费心调配比例,总得比平均分配强吧?
在后面的章节中,我们会逐步引入这三种基准。但眼下先从最基本的开始。
那什么是最自然的基准?想象你在投资的第一天,面前有两个选择:一是按照某个策略每月定投,二是把所有钱一次性全部买入,然后什么都不做,一直拿着到最后一天。第二个选择就是 **买入持有(Buy & Hold**——它代表“什么都不做”的结果。你的策略要是连“什么都不做”都赢不了,那它存在的意义是什么?
所以我们接下来要做的事很简单:算出“什么都不做”能赚多少钱,然后跟定投的结果放在一起比。
### 动手实验 3:和基准对比
我们一起把这份基准对比 spec 写出来。这次重点看一个新东西:**结果呈现段如何写“动态结论”**——不知道跑出来谁赢谁输,怎么提前定义两种结果的解读?
#### 起草上下文 + 任务
> **上下文**:在 `q1-strategy.ipynb` 中已有沪深 300ETF 日线数据(变量 `df`,含 Close 列)和简单定投回测结果(变量 `result_dca`,含 return 列)。
>
> **任务**:计算「一次性买入持有」作为基准,和定投策略对比,帮助理解收益来源。
#### 起草要求
> **要求**
1. 基准策略(Buy & Hold):起始日全部资金一次性买入,持有到最后一天;基准收益率 = (终点 - 起点) / 起点,存到 `benchmark_return`
2. 计算基准的逐月累计收益率曲线(每月末收盘价相对起始价的涨跌幅)
3. 与定投策略对比
#### 起草结果呈现
这一段是这份 spec 最有意思的地方——我们不知道沪深 300 这 5 年是涨是跌,所以**得提前写好两种结果的对应文案**:
> **结果呈现**
1. 打印起点 / 终点价格、一次性买入收益率、定投收益率、两者差异
2. 根据对比结果**动态选**一句话:
- 如果定投跑赢:「在下跌市中,定投通过分批买入避开了部分高位」
- 如果定投跑输:「在上涨市中,越早全部投入越好」
3. 无论哪种结果,都补充一句共同收束:「但两种策略都没有 Alpha——收益完全取决于市场涨跌,这就是 Beta」
4. 画一张对比图(figsize 12×6):定投 vs 一次性买入两条累计收益率曲线,图例标注名称
> 📌 **要点**:spec 不能预设结果方向。“分批买入避开高位” / “越早全部投入越好”听起来像两个相反的结论,但它们其实是对**同一个现象**的两种表述——市场跌了 / 涨了。把“双分支动态结论”写进 spec,让 AI 跑完**结果会自己讲故事**,无论涨跌都不会出现“硬写但和数据不符”的尴尬。
>
> 📌 **要点**:双分支后面常需要一句“共同收束”。本 spec 的“两种策略都没有 Alpha”是把读者的注意力**从‘谁赢’拉到‘两者本质相同’**——这是教学型 spec 写作的隐藏技巧:分歧的下面藏着共识。
完整 spec 在 [`specs/spec-03-benchmark.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-03-benchmark.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里会出现一组对比数字和一张双线对比图。AI 帮你做了这些事:计算了买入持有的收益率(用最后一天的价格减去第一天的价格,再除以第一天的价格),然后把它和定投的收益率放在一起对比,算出两者的差异。
两条收益率曲线画在同一张图上(如图 1-5 所示)。它们之间的距离,就是两种策略在每个月的差异。有时候定投在上面,有时候买入持有在上面——领先和落后是交替出现的。这张图的价值在于:它不只告诉你最终结果谁赢了,还让你看到整个过程中两者的“赛跑”。
![定投 vs 买入持有](images/q1-how-to-profit/03-dca-vs-buyhold.png)
### 读懂对比结果
先看打印出来的数字。你会发现一个有意思的现象:沪深 300ETF 的起点价格和终点价格可能非常接近。也就是说,如果你在数据起始日一次性买入,持有到现在,可能几乎没赚没亏——市场在这几年里“坐了一趟过山车”,最后又回到了起点附近。
但定投的结果呢?大概率是正收益。为什么“什么都不做”几乎没有收益,而定投反而获得了正收益?
原因不复杂。回想一下定投的机制:价格低的时候,同样 1000 块钱能买到更多份额;价格高的时候,买到的份额更少。过去几年沪深 300 经历了一轮明显的下跌,又有所回升。在下跌过程中,定投不断在低位买入,积累了大量便宜的份额。等到价格回升,这些便宜份额带来的利润弥补了高位买入的亏损。而一次性买入就没有这个“摊低成本”的机会——你在最开始买了,然后只能眼睁睁看着价格跌下去再涨回来。
再看对比图。两条线的交叉和分离,就是两种策略的攻守转换。你会发现,在市场下跌的阶段,定投那条线通常在上面(跌得少);在市场快速上涨的阶段,买入持有可能追上来甚至反超。这也符合直觉——下跌市里,分批买入天然就比一次性买入有优势,因为你避开了在最高点“全仓”的风险。
### 两个收益率背后的大问题
到这里,不管谁赢了,你可能会觉得:差距其实也没那么大嘛。对,你说得对。因为这两种策略做的是同一件事——买入沪深 300ETF 然后持有。区别只是“一次性全买”还是“分批买”。它们的收益来源是完全一样的:市场本身的涨跌。市场涨,两个策略都赚;市场跌,两个策略都亏。
这引出了量化交易中两个最重要的概念。
**Beta(贝塔)** 是你“坐电梯”获得的收益。你走进商场,坐上电梯,电梯往上走,你就到了高处。你做了什么吗?你什么都没做,只是站在电梯里。市场就是那部电梯。市场涨,你的资产就跟着涨;市场跌,你的资产就跟着跌。你承担了市场整体涨跌的风险,市场给你的回报就是 Beta。
定投和买入持有,本质上都是在“坐电梯”。收益完全取决于这部电梯——沪深 300——是往上走还是往下走。你没有做出任何“判断”,没有选择买入时机,没有选择哪只股票,没有在任何地方做出比市场更聪明的决定。赚的钱全是 Beta。
那如果有人不甘心只坐电梯呢?
**Alpha(阿尔法)** 是你“爬楼梯”多走的路。别人坐电梯到了 10 楼,你坐电梯到了 10 楼之后,又自己爬了 5 层到 15 楼——那多出来的 5 层就是你的 Alpha。翻译成投资语言:Alpha = 你的收益 - 市场的收益。如果市场涨了 10%,你赚了 15%,那你有 5% 的 Alpha。如果市场涨了 10%,你只赚了 8%,那你的 Alpha 是 -2%——你不仅没爬楼梯,还在电梯里摔了一跤。
Alpha 需要你做出某种“判断”,并且这个判断要比市场聪明。比如,判断什么时候该买、什么时候该卖、该买什么、不该买什么。这些判断如果正确,就能产生正的 Alpha;如果错误,Alpha 就是负的——你还不如什么都不做。
回到我们的定投策略:它有 Alpha 吗?没有。零。定投不做任何判断,不猜涨跌,不择时。它的收益百分之百来自 Beta。市场好,定投就盈利;市场差,定投就亏损。它只是一种资金分配方式,不是一种“聪明”的策略。
这个结论很重要,值得你记住:**到目前为止,我们的策略没有任何 Alpha。收益完全来自 Beta——市场好就赚,市场差就亏。**
既然定投没有 Alpha,怎样才能赢过市场?我们需要在策略里加入一些“自己的判断”。最简单的判断工具,是一条线。
## 1.3 怎么才能赢过市场?
上一节结尾说了:要产生 Alpha,就得在策略里加入“判断”。但什么叫“判断”?不是你拍脑袋觉得明天会涨就买——那叫赌博。量化的判断,必须是一条可以写成明确规则的逻辑。
最简单的判断工具,是一条线——**均线(Moving Average**。
均线是什么?打个比方。你今天量体温 37.5°C,是不是发烧了?不好说,也许你刚跑完步、也许体温计不太准。但如果你连续 5 天量体温,算个平均值,发现平均体温都偏高——那大概率是真的不舒服了。均线就是这个“平均体温”。它取过去 N 天收盘价的平均值,把每天的随机波动抹平,留下更稳定的趋势方向。单看某一天的价格就像单看某一次体温,可能被噪音干扰;但看一段时间的平均值,信息就可靠多了。
那“价格在均线上方”意味着什么?意味着最近的价格比过去一段时间的平均价格高。换句话说,市场最近在涨,可能正处于上升趋势。注意我用了“可能”——均线是个**滞后指标(Lagging Indicator)**,它看的是过去,不预测未来。就像你说“这个星期天气都不错”,不代表明天一定晴天。但作为一个参考信号,它总比什么都不看要强。
基于这个逻辑,我们给定投策略加一条规则:每月定投日,**仅当收盘价高于 N 日均线时才买入**,否则这个月跳过。就这么简单。不买的月份,钱留在手里,份额和成本都不变。
这就是**择时(Market Timing)**——不再闭着眼睛每月都买,而是先看看“天气”再决定出不出门。市场趋势向上的时候跟着买入,趋势不明朗的时候按兵不动。听上去很合理,对吧?
但这里有一个问题:N 取多少?过去 10 天的平均值?20 天?还是 60 天?不同的 N 就是不同的“回看窗口”:
- **MA10**(10 天,约 2 周):短期均线。反应灵敏,价格稍微涨几天它就跟着翘头,但也容易被短期波动骗到——价格随便抖一抖,信号就来回翻转。
- **MA20**(20 天,约 1 个月):中期均线。过滤掉了大部分日常噪音,是比较常用的参数。
- **MA60**(60 天,约 3 个月):长期均线。非常稳定,但反应迟钝。市场可能已经涨了一大截,它才慢悠悠地确认“嗯,确实在涨”。
到底哪个好?光想没用,我们跑一遍就知道了。但在跑回测之前,先画一张图,亲眼看看均线到底长什么样。
### 动手实验 4:看见均线
这份“画图” spec 比前面几份更挑细节——它要把图怎么画**全部锁死**到颜色、线型、标记形状。我们一起把它写出来,看看“视觉契约”要写多细才够。
#### 起草上下文 + 任务
> **上下文**:在 `q1-strategy.ipynb` 中已有沪深 300ETF 日线数据(变量 `df`,含 Close 列)。上一节引入了均线的概念——过去 N 天收盘价的平均值。
>
> **任务**:画出收盘价与三条均线(MA10 / MA20 / MA60)的对比图,并标记 MA20 的上穿 / 下穿点。
#### 起草要求:视觉契约
要求段是这份 spec 的灵魂——**视觉细节全部锁死**:
> **要求**
- 截最近 1 年数据:`df_recent = df.loc[df.index[-1] - pd.DateOffset(years=1):].copy()`
- 三条均线:`rolling(10/20/60).mean()`
- 找 MA20 交叉点:上穿(前 Close ≤ MA20,当 Close \> MA20/ 下穿(前 Close ≥ MA20,当 Close \< MA20
- 画对比图(figsize 14×7):
- 收盘价(黑色实线)/ MA10(绿色虚线)/ MA20(蓝色实线)/ MA60(红色点线)
- 上穿点:绿色向上三角(`marker='^'`, markersize=10
- 下穿点:红色向下三角(`marker='v'`, markersize=10
- 标题「沪深 300ETF:收盘价与均线(最近 1 年)」+ 图例 + 网格
> 📌 **要点**:可视化 spec 必须把视觉契约写到颜色、线型、标记形状级别。“画一张对比图”是反例——AI 可能给你 default 蓝色细线、不带 marker、不带网格的图,跟书里截图完全对不上。**每一个视觉决策都要进 spec:颜色用什么 hex / linestyle 用“–”还是“:”/ marker 多大 / figsize 多少。** 这是【精确】原则在视觉场景的具体落点。
>
> 📌 **要点**:截窗口要带 `.copy()`。要求里的 `df.loc[...].copy()` 不是装饰——pandas 里没 `.copy()` 直接切片是“视图”,后面再往这个视图上加列(MA10/MA20/MA60)会触发 `SettingWithCopyWarning`,有时甚至改不到原数据。**spec 里写明** `.copy()` **比写一句“注意切片”更稳**——AI 看到 `.copy()` 就知道你要的是独立副本。
#### 起草结果呈现
> **结果呈现**:价格与均线对比图(含交叉点标记)+ 打印交叉统计「上穿 N 次 / 下穿 N 次」。
完整 spec 在 [`specs/spec-04-ma-visual.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-04-ma-visual.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里会出现一张价格与均线的对比图,如图 1-6 所示。
![收盘价与均线](images/q1-how-to-profit/04-ma-visual.png)
### 读懂均线图
现在你亲眼看到了均线和交叉信号。几个值得注意的地方:
**三条均线的“性格”不同。** 绿色的 10 日均线紧贴价格,几乎同步波动——价格稍微涨几天它就跟着翘头,跌几天它就立刻掉头向下。蓝色的 20 日均线稍微平滑一些,过滤掉了一些日常噪音。红色的 60 日均线最“淡定”,价格上下翻飞它才慢慢跟。这正是前面说的“回看窗口”的差异:窗口越短,越灵敏也越容易被骗;窗口越长,越稳定也越迟钝。
**交叉点就是信号。** 绿色三角标记的是“上穿”——收盘价从 MA20 下方穿到上方,意味着短期价格超过了最近一个月的平均水平,趋势可能在转好。红色三角标记的是“下穿”——收盘价跌破了 MA20,趋势可能在转弱。
**信号不少,但不是每次都“对”。** 仔细看图:有些上穿之后确实涨了一段,信号是“对”的;但也有些上穿之后价格又很快跌回去——刚穿上去就掉下来,这就是“假信号”。均线越短,假信号越多;均线越长,信号越少但更迟钝。没有哪条均线能百分百准确。
有了这个直觉,我们来试试:如果真的按这些信号来决定买不买,结果会怎样?
### 动手实验 5:加入均线择时
这份均线择时 spec 多了一件事——把可能爆炸的边界 case 列出来。均线初期没值、累计成本为 0 时除法会爆,这些都要在 spec 里点名。我们一起把它写出来。
#### 起草上下文 + 任务
> **上下文**:在 `q1-strategy.ipynb` 中已有沪深 300ETF 日线数据(`df`)、简单定投回测函数 `backtest_dca` 和结果(`result_dca`)、基准对比代码。
>
> **任务**:在定投基础上加入均线择时——每月第一个交易日,仅当收盘价 \> N 日均线才买入。
#### 起草要求:边界 case 显式
> **要求**
1. 函数 `backtest_dca_with_ma(df, monthly_amount=1000, ma_period=20)`,返回格式同 `result_dca`,额外含 signal 列(布尔值)。**后续步骤会复用此函数**
2. 三个边界 case 必须显式处理:
- 均线计算初期(前 N 天)没均线值,这些月份不买入
- 跳过的月份,累计成本和份额保持不变
- 累计成本为 0 时(尚未发生任何买入),收益率直接设为 0,**不要用除法**,避免除零警告
3. 分别跑 MA10、MA20、MA60 三个参数
> 📌 **要点**:边界 case 必须显式写进 spec。“每月有买入条件”听起来简单,但隐藏着三个炸点:① 前 N 天没均线 ② 跳过月份的状态怎么传递 ③ 累计成本为 0 时除法会 NaN。**只要 spec 不点名这三件事,AI 写出的代码 60% 概率会在某一个炸点上出 warning 或错值。** 你不需要让 AI 自己发现边界——你列清楚,它照办。
>
> 📌 **要点**:派生函数的入参命名要和原函数对齐。`backtest_dca_with_ma(df, monthly_amount=1000, ma_period=20)` 的前两个入参和实验 2 的 `backtest_dca(df, monthly_amount=1000)` 完全一致——只在末尾加 `ma_period`。这不是形式主义:spec 链越往后,函数会被反复调用,入参命名一致才能让“两个回测”的对比代码读起来对称。**新加参数追在已有参数后面、不打乱原顺序,是 spec 链可读性的隐藏前提。**
#### 起草结果呈现
> **结果呈现**
1. 打印每个参数:`MA10: 收益率 X.XX%, 买入 N/M 次` + 简单定投作为对照
2. 画对比图(figsize 12×6):简单定投灰虚线 + MA10/20/60 三色实线,图例 + 网格 + 零线
完整 spec 在 [`specs/spec-05-ma-timing.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-05-ma-timing.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里会出现一组对比数据和一张多线对比图。AI 帮你做了这些事:在每个交易日计算过去 N 天收盘价的平均值(均线),然后在每月定投日比较当天收盘价和均线值——价格在均线上方就买入,在下方就跳过。和简单定投的区别就在这里:简单定投每个月都买,均线策略有些月份会选择“不出手”。AI 分别用 MA10、MA20、MA60 三个参数各跑了一遍回测,然后画了一张对比图(如图 1-7 所示):灰色虚线是简单定投作为基准,三条有色实线是不同均线参数的策略,四条线放在一起一眼就能看出谁在什么时候领先、什么时候落后。
![均线择时对比](images/q1-how-to-profit/05-ma-timing.png)
### 读懂均线择时的结果
先看打印出来的数字。每一行告诉你两件事:最终收益率和买入次数。
注意买入次数的变化。简单定投是每个月都买,均线策略则会跳过一些月份。MA10 跳过得少一些,因为短期均线容易被价格快速穿越;MA60 跳过得多一些,因为长期均线更“迟钝”,价格要涨很久才能站到它上面。换句话说,均线周期越长,策略越“挑剔”——不是每个月都觉得“可以买”。
再看收益率。三个参数的收益率各不相同——有的可能比简单定投好,有的可能更差。这是第一个值得注意的现象:**加了均线判断,不一定收益更高。**
为什么会这样?因为均线择时是一把双刃剑。它帮你跳过了一些“价格在下跌通道”的月份,避免了在下跌中继续投钱,这是好事。但它也可能让你错过一些“刚开始反弹”的月份——价格刚从底部起来,还没站上均线,你就没买到最便宜的那几次。少买的月份如果后来涨了,你就少赚了。
现在看对比图。灰色虚线是简单定投,有色实线是三条均线策略。注意它们什么时候分开、什么时候靠近。你可能会发现:在市场大幅下跌的阶段,均线策略的曲线有时比简单定投下跌得少——因为它跳过了一些下跌月份的买入。但在市场持续上涨的阶段,均线策略可能跟不上简单定投——因为有些月份它“犹豫”了,没有买入。
这里有一个关键观察,请你记住:**加入判断条件确实改变了结果,但改变的方向不确定。它不是一个“只赚不赔”的改进,而是一种取舍——用“可能错过上涨”换“可能避开下跌”。**
而且三个参数的效果不一样。MA10、MA20、MA60,谁最好?在你手上这段数据里,可能 MA20 赢了,也可能 MA60 更好——这取决于市场在过去几年具体走了什么样的路径。
三个参数效果不同——MA10、MA20、MA60,到底哪个最好?为什么不干脆把所有可能的参数都试一遍呢?从 MA5 到 MA120,每隔 5 天试一个,总有一个是最优的吧?
## 1.4 找到“最优参数”就能盈利了?
上一节我们试了三个参数,效果各不相同。你心里可能已经在想:才三个参数就想找到最优解?太不严谨了。干脆全试一遍——从 MA5 到 MA120,每隔 5 天取一个,一共 24 个参数。跑完之后,选收益率最高的那个。听起来是不是很“科学”、很“严谨”?毕竟我们没有拍脑袋猜,而是用数据说话、穷举搜索。
为什么是 5 到 120 这个范围?5 天太短,几乎等于在看每周的波动,全是噪音;120 天约等于半年,再长的话均线几乎不动,策略基本不会发出买入信号。5 到 120 覆盖了实务中常用的所有均线周期——短期、中期、长期都包含在内。每隔 5 天取一个是为了效率:不需要逐天试,大趋势的差异在 5 天级别就能体现出来,MA15 和 MA16 的结果不会有质的不同。
不过在看结果之前,你可以先想一个问题——如果我朝墙上射了 24 支箭,总有一支是最接近靶心的。但这说明我箭术好吗?先把这个问题放在心里,我们来看看实验结果。
### 动手实验 6:参数扫描
这份参数扫描 spec 有点反常——它**故意保持 naive**:不警告“过拟合”,不分训练 / 测试集,就让你“找最优”。我们一起把它写出来,先经历一次“找到漂亮数字”的过程,下一节再回头看看那个数字到底是什么。
#### 起草上下文 + 任务
> **上下文**:在 `q1-strategy.ipynb` 中已有 `df`、`result_dca`、带均线条件的回测函数 `backtest_dca_with_ma`。
>
> **任务**:扫描所有均线参数(MA5 到 MA120,步长 5),找到历史收益率最高的「最优参数」并可视化。
#### 起草要求 + 结果呈现
> **要求**:扫描范围 MA5 到 MA120 步长 5(共 24 个参数)→ 调用已有的 `backtest_dca_with_ma` 跑一遍 → 记录每个参数的最终收益率 → 找出最高那个。
>
> **结果呈现**:打印最优参数 / 最优收益率 / 简单定投对照 / 提升幅度;画柱状图(figsize 12×5),正收益绿、负收益红、最优金色高亮黑边框、灰虚线标定投基线、黑实线标零线。
>
> 📌 **要点**:教学型 spec 有时要“刻意保持 naive”。这份 spec 的 spec-writer 是知道过拟合存在的——但 spec **不能**写“小心过拟合”或者“加一个 train/test split”。如果这里就警告了,下一节“真相时刻”的冲击力就消失了。**让学员先经历“找到漂亮数字”的兴奋,下一份 spec 再让那个数字在新数据上原形毕露,认知冲击才有教学价值。** 学员自己写“研究探索型 spec”时也是同理——先写一个 naive 版本看看会怎样,再升级。
>
> 📌 **要点**:spec 的“语气”也是设计选择。本 spec 的任务段写“找到历史收益率最高的最优参数”——用了“最优”这个词。如果 spec 写“扫描参数看看分布”,学员就不会有“啊我找到秘诀了”的错觉,后面再讲过拟合就没冲击力了。**spec 的措辞要服务于章节的节奏。**
完整 spec 在 [`specs/spec-06-param-scan.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-06-param-scan.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里会出现一组数字和一张柱状图。AI 帮你做了这些事:从 MA5 到 MA120,每隔 5 天取一个参数,逐个运行均线回测并记录收益率,一共 24 次——就像考试出了 24 套卷子,让同一个策略换不同的均线参数去做,看哪套得分最高。然后从中找出收益率最高的“冠军参数”,并画成柱状图。
参数扫描结果如图 1-8 所示。柱状图中每根柱子代表一个参数的收益率。绿色柱子 = 正收益,红色柱子 = 负收益,金色高亮那根就是“冠军”——24 个参数里收益率最高的。灰色虚线是简单定投的水平线,让你一眼看出哪些参数跑赢了简单定投,哪些跑输了。黑色实线是零线——在它下面的参数连本都没保住。
![参数扫描结果](images/q1-how-to-profit/06-param-scan.png)
### 读懂参数扫描的结果
先看打印出来的数字。最优参数的收益率是多少?和简单定投比,提升了多少?你大概率会看到一个相当漂亮的数字——看起来我们真的“找到了盈利的秘诀”。
但是——请你仔细看那张柱状图的形状。
如果真的存在某个“最优均线周期”的内在规律,你会期望柱状图呈现一个“山峰”形状:最优参数附近的参数表现应该也不差,离最优越远表现越差,像一座钟形山丘,顶点就是最优值。这说明“最优参数附近就是甜蜜区”,你稍微偏一点也没关系。
但实际的柱状图是这样吗?大概率不是。你看到的更像是高高低低、毫无规律的锯齿形状。MA35 可能很好,MA40 突然变差,MA45 又回来了。相邻的两个参数——仅仅差 5 天——表现可能天差地别。金色那根“冠军”柱子的左右邻居,可能一个正收益一个负收益。
这说明什么?
说明这个“最优参数”的“好”,很可能只是碰巧。它恰好在这段特定的历史数据上表现最好,而不是因为它抓住了什么深层规律。如果真的有深层规律,相邻参数不应该差这么多——MA35 和 MA40 只差 5 天的回看窗口,它们看到的趋势信息几乎一样,凭什么一个盈利一个亏损?唯一的解释是:结果对参数极度敏感,而这种敏感性来自数据中的随机波动,不是来自某种稳定的市场规律。
现在回到开头那个问题:你朝墙上射了 24 支箭,然后走到离某支箭最近的地方画了一个靶心。“百发百中”?当然不是——你只是射得够多罢了。24 个参数里,总有一个碰巧在这段时间表现最好,这是统计学的必然,不是你发现了什么秘密。就像你抛 24 次硬币,总有一次正面连续出现最多——你不会因此断定那枚硬币有“特异功能”。
这个现象我们暂时叫它“锯齿现象”——参数和参数之间的差异不是平滑过渡的山峰,而是高低跳动的锯齿。它有一个更正式的名字,但在给它正式命名之前,我们需要一个更严格的方法去验证:这个“冠军”到底是真本事,还是只是 24 次掷骰里运气最好的那一次。
方法很简单——用这个参数从未见过的新数据来测试它。下一节就做这件事。
## 1.5 真相时刻
怎样才算一场“公平考试”?
你不能拿学生复习过的原题来考他——他全都见过,考满分不说明任何问题。要测出真实水平,得出一张他从没做过的卷子。投资策略也一样。上一节我们用全部 5 年的历史数据做参数扫描,找到了“冠军参数”。但这个冠军是在“开卷考”里拿的第一名——它见过所有数据,当然能挑到最好的答案。这不算本事。
要给它一场闭卷考,方法是把数据分成两段。
你手上有大约 5 年的历史数据。我们把前 60% 拿出来——大约 3 年——作为**训练集(Training Set)**,用来“复习”,也就是做参数扫描、找最优参数。剩下的后 40%——大约 2 年——作为**测试集(Test Set)**,这是“考试卷”。训练集里发生的事,测试集完全不知道;测试集里发生的事,训练的时候完全没看过。两段数据,互不相见。
考试的类比可以帮你记住这个逻辑:你手上有 5 年的真题。你用前 3 年的真题来复习——反复做、反复总结、找到最顺手的解题方法。然后你用后 2 年的真题来“考试”——这 2 年的题你在复习的时候完全没碰过。如果你是真的理解了规律,换一张新卷子也能考好。如果你只是背了前 3 年的答案,新卷子就会露馅。
> 📌 小词典:什么是训练集 / 测试集?
>
> **训练集(Training Set)**:用来“复习”的那段数据。
>
> **测试集(Test Set)**:用来“考试”的那段新数据,模型训练时完全没见过。
>
> 核心约束:两段数据互不交叉。否则就是开卷考。
为什么是 60/40?没有什么魔法数字。训练集不能太少,否则“复习”不充分,找到的参数不可靠;测试集也不能太少,否则“考试”的题量不够,结果说明不了问题。60/40 是一个常见的折中:前面大约 3 年够做一轮完整的参数扫描,后面大约 2 年够看出趋势。这不是完美的方法——真正的未来当然不可知,我们只是用“后来发生的事”来假装未来。但它是我们能做到的最公平的模拟。
接下来的实验就是这个思路:在训练集上重新做参数扫描,找到训练集的“冠军参数”。然后拿着这个冠军参数去测试集上跑一遍——看看它能不能通过“考试”。同时我们在测试集上也跑一遍简单定投作为对照。为什么要对照?因为简单定投不做任何参数优化,它就是“什么都不想”的基线。如果你精心优化的参数连“什么都不想”都赢不了,那优化的意义何在?
### 动手实验 7:样本外测试
这份样本外测试 spec 是本章压轴的一份——它把数据按时间切成两段,让上一节找到的“冠军参数”在没见过的新数据上重新考一次。我们一起把它写出来,重点是**数据 split 的可复现写法**和**怎么把三个数字摆在一起做对比**。
#### 起草上下文 + 任务
> **上下文**:在 `q1-strategy.ipynb` 中已有 `df`、`backtest_dca`、`backtest_dca_with_ma`、参数扫描代码(MA5 到 MA120)。
>
> **任务**:把数据按时间前 60% / 后 40% 分为训练集 / 测试集,验证「最优参数」在未见过的新数据上是否仍然有效。
#### 起草要求:split 可复现 + 三栏对比
> **要求**
1. 数据分割:**按时间前 60% 为训练集,后 40% 为测试集**(按时间顺序切,不打乱)
2. 在训练集上扫描参数(MA5 到 MA120 步长 5),找到训练集上的最优参数
3. 用该最优参数在**测试集**上跑回测
4. 同时在**测试集**上跑简单定投作为对照
5. 对比三个收益率:训练集最优 / 测试集该参数 / 测试集简单定投
> 📌 **要点**:数据 split 的写法直接决定可复现。“按时间前 60%”是确定的,“随机分 60/40”是不确定的——后者每次跑会得到不同 split,结果就不能 1:1 对照了。**任何切分操作都要写明:是按时间还是随机?切点在哪?随机种子?锁住了你的 spec 才能跨次复现。**
>
> 📌 **要点**:当一份 spec 要核对前一份 spec 的结论时,固定套路是把三个数字摆在一起对比——前一份给出“看似最优”,本份给出“测试集上的真实成绩”+“什么都不做的成绩”。三者一对比,前一份的“最优”到底是真本事还是巧合,一目了然。三个数字不是随便凑的:训练集最优是“你以为”,测试集该参数是“其实”,测试集简单定投是“什么都不做”——三者放一起,过拟合就藏不住了。
#### 起草结果呈现
> **结果呈现**
1. 打印训练集 / 测试集时间范围和条数
2. 三个数字并排打印:训练集最优参数 + 收益率 / 测试集上该参数的收益率 / 测试集上简单定投的收益率
3. 对比结论双分支:
- 最优参数在测试集上比简单定投还差:打印「这就是过拟合——你在历史数据上找到的是巧合,不是规律」
- 否则:打印「结果看起来还行,但仍需谨慎,样本量有限」
完整 spec 在 [`specs/spec-07-overfitting.md`](https://github.com/xingwudao/xquant-learning/blob/main/q1-how-to-profit/specs/spec-07-overfitting.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里会出现一组关键数字。这可能是本章最重要的一组数字——请你做好心理准备。
AI 帮你做了这些事:把数据按时间顺序在第 60% 的位置一刀切——前面是训练集(“复习用的真题”),后面是测试集(“考试当天才看到的新题”)。注意数据是按时间顺序切的,不是随机打乱的,因为在现实中你只能用过去的数据做决策,未来的数据还没发生。然后在训练集上重新做参数扫描找到冠军参数,拿着这个冠军参数去测试集上跑一遍(“正式考试”),同时在测试集上也跑一遍简单定投作为对照。
### 读懂样本外测试的结果
现在看看这三个数字。
第一个数字:训练集上最优参数的收益率。这个数字大概率很漂亮——毕竟它是 24 个参数里挑出来的冠军,在训练集上当然表现最好。记住,这是“开卷考”的成绩。
第二个数字:测试集上用同一个参数的收益率。这是“闭卷考”的成绩。
第三个数字:测试集上简单定投的收益率。这是“什么都不想”的成绩。
把第二个数字和第三个数字放在一起看。
大概率你会看到这样的结果:冠军参数在测试集上的收益率,比简单定投还差。
请你再读一遍这句话,确认你没有看错。你花了大量精力做参数扫描,从 MA5 到 MA120 穷举了 24 个参数,在训练集上找到了表现最好的那个。然后你拿着这个精心挑选的冠军参数去面对“未来”——它不仅没有比简单定投更好,反而更差了。
换句话说:**你精心优化的策略,还不如什么都不做。**
这不是你的操作有误,也不是代码有 bug。这就是过拟合的真实面目。
在训练集上,你的冠军参数看起来无懈可击——它是 24 个候选人里的第一名,收益率比第二名还高出不少。但到了测试集上,它差到几乎像换了一只策略。它在训练集上的好成绩不是因为它抓住了市场的某种深层规律,而是因为它恰好适配了那段特定数据的特定走势。换一段数据,这种适配就不存在了。
为什么会这样?我从三个角度帮你理解。
**从统计的角度看。** 24 个参数里挑最好的那个,就像在教室里随便拉 24 个人,选最高的那个。他身高 185cm——但这能代表这 24 个人的平均身高吗?当然不能。他只是碰巧最高的那个。你选的参数越多,“碰巧最好”的那个看起来越好——因为你给了随机波动更多的机会来制造“看起来很厉害”的假象。这种现象叫选择偏差(Selection Bias):当你从一堆结果里挑最好的那个,你挑到的往往不是“真正最好”的,而是“运气最好”的。
**从市场的角度看。** 前 3 年和后 2 年的市场环境不同。也许前 3 年是一轮漫长的熊市,价格一路下跌;后 2 年是触底反弹,价格逐步回升。或者反过来。一个参数在熊市中表现好——比如 MA60,因为它足够迟钝,帮你躲过了大部分下跌——但到了牛市,这种迟钝反而成了累赘:市场都涨起来了,MA60 还在犹豫要不要买入,白白错过了一大段涨幅。市场不是一成不变的。一件衣服不可能四季都合适——冬天穿着暖和的羽绒服,夏天穿就热死人。你在前 3 年找到的“最合身的衣服”,到后 2 年换了季节,哪里都不对。
**从直觉的角度看。** 最优参数就是为那段训练数据“量体裁衣”的产物。量体裁衣做出来的西装确实完美合身——但那是为你一个人做的。换一个身材不同的人穿,哪里都不对劲。你的冠军参数也是一样:它是为 2021 年到 2023 年某段特定行情“量身定做”的,每一个均线周期、每一次买入跳过,都恰好踩在了那段数据上。换一段数据——哪怕只是后面紧接着的两年——整套策略就不再合身。
这就是过拟合——量化交易最大的敌人。
它不是一个小 bug,不是代码写错了一行,修一修就好。它是一种系统性的思维陷阱。只要你在历史数据上做优化,过拟合的幽灵就始终在你身边。你测试的参数越多,优化的维度越多,它就越危险——因为你给了“碰巧”更多的生存空间。
每一个做量化的人,不管是刚入门的新手还是华尔街的资深研究员,都必须时刻警惕它。在历史数据上看到的任何“好结果”,在被样本外测试验证之前,都不值得相信。训练集上的冠军可能只是考试原题背得最熟的那个学生,真正的考试一来就露馅了。
你在本章走过的路——从简单定投、到加入均线判断、到参数扫描找最优、再到样本外测试看清真相——正是每一个量化新手都会经历的完整旅程。区别只在于:有些人在“找到最优参数”那一步就停下来了,兴冲冲地拿去实盘交易,然后亏了真金白银才明白过拟合是什么。而你,在投入一分钱之前,就已经看到了真相。
记住这个教训:**历史数据上的“最优”,不等于未来的“可用”。** 在量化交易的世界里,怀疑永远比自信更值钱。
## 1.6 本章总结
回头看看我们走过的路。从最简单的定投开始,到加入均线判断做择时,到参数扫描寻找“最优解”,再到样本外测试看着精挑的参数全线衰减——7 个实验,一步一步,你经历了一次完整的认知升级。这不只是学了几个策略,而是亲眼见证了“看起来有效”和“真的有效”之间的巨大鸿沟。
### 概念速查表
本章涉及的核心概念汇总如表 1-2 所示。
**表 1-2 第 1 章核心概念速查**
| 概念 | 含义 | 类比 |
|----|----|----|
| 回测(Backtest) | 用历史数据模拟策略执行 | 在脑海中“重播”历史 |
| 基准(Benchmark) | 用来比较的参照物 | 全班的平均分 |
| Beta(贝塔) | 承担市场风险获得的收益 | 坐电梯——市场涨你跟着涨 |
| Alpha(阿尔法) | 超越市场的那部分收益 | 爬楼梯——靠自己的本事往上走 |
| 均线(Moving Average | 过去 N 天价格的平均值 | 连续几天的平均体温 |
| 过拟合(Overfitting) | 把数据中的噪音当成规律 | 背答案 vs 理解规律 |
| 训练集(Training Set) | 用来“复习”和找参数的那段数据 | 复习用的真题 |
| 测试集(Test Set) | 模型从未见过、用来检验的那段数据 | 没做过的考试卷 |
### 策略进化路径
第 1 章 3 个版本的策略进化与教训如表 1-3 所示。
**表 1-3 第 1 章策略进化路径**
| 版本 | 策略 | 教训 |
|------|-----------------|----------------------------------------------|
| v0.1 | 简单定投 | 纯 Beta 暴露,收益完全随市场涨跌 |
| v0.2 | 定投 + 均线择时 | 加入判断是双刃剑,不保证比无脑定投更好 |
| v0.3 | 参数扫描优化 | 历史最优不等于未来可用,过拟合的风险就在面前 |
### 飞轮第一圈:你刚刚走完的路
回头看:你已经把飞轮的四层都各自踩过一次。**候选层**——你选了沪深 300ETF;**组合层**——你定了“每月买一份”或“上穿均线再买”的规则;**执行层**——回测引擎替你按规则下单;**归因层**——你比了三种策略的累计收益、对照了样本内/外。
更重要的是:**做、看、疑**三个动作你都练了一遍。前言里那句口号——“**做要规则、看要指标、还要怀疑你的指标**”——你不是听别人讲,而是亲眼看见冠军参数在测试集上现原形之后才真正信的。**这就是为什么本书把“疑”和“做”“看”放在同等位置**:会做、会看,但不会疑,最容易在某些时候把赚到的钱加倍还给市场。
### 本章最重要的收获
说实话,我们并没有学会一个能稳定盈利的策略。定投跑不赢市场,加了均线也未必更好,参数优化更是一脚踩进了过拟合的坑。如果你期待的是“学完第 1 章就能盈利”,那确实要让你失望了。
但你学会了一样比任何策略都值钱的东西——**怀疑**。面对任何一条漂亮的回测曲线,你现在知道第一个要问的问题是:“这是规律,还是巧合?”这种本能的警觉,会让你在未来少走很多弯路,少亏很多真金白银。
### 带走的问题
本章带你跑通了第一个完整策略,但也留下了一些更深的问题。带着它们继续往下走:
- **只买沪深 300 够吗?** 本章 7 个实验全在拿沪深 300 做样本,但真正投资时不会只买一只。下一章我们要选 3 只 ETF,组成一个真正的“投资宇宙”。——第 2 章会解答。
- **怎么区分运气和能力?** 一个策略盈利了,到底是真有水平还是瞎猫碰上死耗子?——第 5 章、第 6 章会解答。
- **有没有不容易过拟合的策略?** 既然参数优化这条路走不通,还有别的路吗?——第 4 章、第 6 章会解答。
- **怎么知道策略在未来还能盈利?** 样本外测试只是第一步,还有更严格的检验方法吗?——第 5 章会解答。
答案都在后面。继续。
> 💡 **提示**:本章正文聚焦“亲手跑通最小闭环”。「拓展阅读」给了两段世界观铺垫——读不读都不影响主线,但读完会让后面几章吸收得更快。
>
> 💡 **提示**:本章所有代码的可运行版本见 `notebooks/q1-strategy.ipynb`。
## 拓展阅读
本章正文聚焦“亲手跑通最小闭环”。下面两节是动手之前可以省略的视野铺垫——读完正文后回头看,反而吸收得更快。
### 你做的是哪种交易?
“量化”、“程序化”、“算法”、“高频”——这四个词常被混用,其实是层层包含的关系,如图 1-9 所示。
![四个概念层层包含](images/q1-how-to-profit/quant-concepts-nested.png)
四个概念的具体对照如表 1-4 所示。
**表 1-4 量化 / 程序化 / 算法 / 高频四个概念对照**
| 概念 | 干什么 |
|----|----|
| **量化交易**(最外层) | 用数据和模型做决策(对面是靠经验直觉的“主观交易”) |
| **程序化交易** | 用程序自动执行——你完全可以做量化但手动执行,很多个人量化交易者就这么做 |
| **算法交易** | 解决“怎么买”——把大单拆小单降低市场冲击 |
| **高频交易**(最内层) | 毫秒级速度,专用机房和光纤,门槛千万美元起 |
**“投资”与“交易”也是两件事**:投资买的是公司、赚的是公司成长的钱;交易买的是筹码、赚的是别人犯错的钱。本课程偏交易,不分析公司成长。
**本课程做的是中低频量化交易,手动或半自动执行**——持仓周期周到月级别,普通电脑就能跑。你的对手不是 Citadel 这种顶级机构,而是没有系统、靠感觉交易的散户。在这个层面上,纪律严明的量化策略已经是巨大优势。
### 钱从哪来,为什么大多数人赚不到
不管你买什么,赚到的钱本质上**只来自两类**——前言里我们用过同样的分法:**资产创造**(你拿到的是资产本身在帮你赚钱)和**价格变化**(你赚的是别人愿意付的差价)。两类的具体来源如表 1-5 所示。
**表 1-5 投资收益的两大类来源**
| 大类 | 具体来源 | 核心本质 | 关键特征 |
|----|----|----|----|
| **资产创造** | 分红 | 公司把利润分给股东 | 不依赖市场情绪(股票、ETF) |
| **资产创造** | 利息 | 借出资金的报酬 | 不依赖市场情绪(债券、国债) |
| **价格变化** | 价差 | 低买高卖的差额 | 赌价格变化方向(股票、ETF、期货) |
| **价格变化** | 套利 | 不同市场间的瞬时差价 | 几乎不预测方向,门槛高(跨市套利) |
一句话总结:**分红和利息赚的是“现金流”,价差赚的是“共识差”,套利赚的是“空间差”。** 本课程的策略主要赚价差。
**短期负和、长期正和。** 每笔交易都要付佣金、印花税、买卖价差——频繁交易等于把自己放进一个“整体必输”的游戏。但长期持有指数级资产能分享企业增长,蛋糕在变大、大家一起赚。这就是格雷厄姆那句“**短期是投票机,长期是称重机**”。本课程定位“中频”——既捕捉规律,又不被成本吃掉。
**三个差距,量化的三种对抗。** 虽然市场整体可以赚钱,但大多数散户没赚到——核心是三个“差”,恰好对应量化的三种优势(如表 1-6 所示):
**表 1-6 三个差距与量化的对应优势**
| 普通人的劣势 | 体现 | 量化的对应优势 |
|----|----|----|
| 信息差 | 你看到新闻时,机构早已先动 | 同时扫描几千标的找统计规律 |
| 情绪差 | 涨了就追买、跌了就吓得抛掉,本能在股市害人 | 规则写好机器执行,无情绪干扰 |
| 成本差 | 频繁操作累积成本,可能吃掉一半本金 | 明确规则约束,不频繁操作 |
量化不能保证赚钱,但能让你在更公平的位置上竞争。
+434
View File
@@ -0,0 +1,434 @@
# 第二章:选出你的投资宇宙:3 只 ETF
第一章我们用一只沪深300ETF 体验了量化策略的完整闭环——从下载数据、定投回测、基准对比,到均线择时、参数扫描、样本外测试。一路走下来,你已经亲手验证了一个策略从“看起来有效”到“撞上过拟合”的全过程。
但有个问题我们始终没碰:只买一只 ETF 够吗?直觉告诉你,把鸡蛋放在一个篮子里不够安全,应该广撒网、分散风险。这个直觉对不对?“分散”到底该怎么分?随便多买几只就算分散了吗?这些问题,就是本章要回答的。
不过在动手之前,我们要先把“市场上到底有什么可以买”这个最基础的问题搞清楚——第一章直接给了你一只沪深300ETF,但你还没正式见过投资市场的全图。
### 路线图
从第二章开始,我们要把第一章的闭环拆开,一步步做扎实:
**选什么资产(第二章 本章)→ 每个买多少(第三章)→ 什么时候买卖(第四章)→ 怎么验证有效(第五章)**
本章七节按“准备 → 实验 → 收尾”三段组织,对照如表 2-1 所示。
**表 2-1 第二章七节的三段组织**
| 节 | 内容 | 类型 |
|----|------|------|
| 2.1 - 2.2 | 你到底能买什么 → 工具升级(引入 open-xquant | 准备 |
| 2.3 - 2.5 | 选什么类型 → 选哪个地区 → 怎么保护组合 | 实验 |
| 2.6 - 2.7 | 回头看 + 本章总结 | 收尾 |
---
## 2.1 你到底能买什么?
直接给你答案:**新手不要买个股,从 ETF 开始。** 等你通过 ETF 理解了市场运作,再去研究其他资产也不迟。
为什么?新手的典型特点:不懂选股、资金有限、时间有限、心态不稳。而 ETF 恰好能解决这些问题(如表 2-2 所示):
**表 2-2 ETF 解决新手四大痛点**
| 新手痛点 | ETF 如何解决 |
|---------|-------------|
| 不懂选股 | 已经帮你选好了一篮子股票 |
| 资金有限 | 一手 ETF 可能只要几百块 |
| 时间有限 | 不用研究单个公司,买指数 = 买这个国家的整体经济 |
| 心态不稳 | 分散持有降低波动,不会因一只股票暴雷而血本无归 |
一个无奈的现实:**大多数散户的长期收益低于指数。** 既然如此,为什么不直接买指数?本章接下来的实验,就是带你看清“为什么不要买个股、怎么用 ETF 构建投资组合”。
> ETF 之外还有股票、债券、期货期权;A 股之外有美股、港股;具体可交易的资产清单、各类资产的赚钱方式、券商怎么选——见本章末「拓展阅读」。
---
## 2.2 工具升级:从手写脚本到利用框架
2.1 把“为什么不要买个股、怎么用 ETF”的结论给了你。下一节就要动手实验。但在动手之前,先认识一下接下来三个实验都会用到的工具:open-xquant。
第一章手写循环跑通了一个均线策略——AI 帮你写了几十行代码就实现了。能跑,但稍有量化经验的人都知道:**真正的量化策略不是这么写的。**
### 你需要的不是更多代码,而是一套经过实战检验的体系
技术指标网上一搜就有,让 AI 抄一份代码也不难。但**怎么把指标变成一个真正可上线的策略**——怎么定义信号、怎么组合规则、怎么管仓位、怎么样本外验证、怎么避开常见陷阱——这套体系是多年自营交易桌上**踩过坑、亏过钱、改过的纪律**,看几本书内化不了。
新手自己摸索,要走的弯路三年起步。但用对的框架,等于让一个老手在背后**约束你的每一步**。
前提是:这个框架真的来自一个老手。
### quantstrat:业界公认的方法论参考
R 语言生态里有一个叫 **quantstrat** 的开源框架——它在量化策略开发领域久负盛名。主要作者 Brian Peterson 是一位有十几年自营交易实战经验的量化从业者,他公开发布的白皮书《Developing & Backtesting Systematic Trading Strategies》([strat_dev_process.pdf](http://braverock.com/brian/strat_dev_process.pdf))描述了一套从“想法”到“可上线策略”的完整流程,是业界教学和入职培训的常见教材。
一句话:**quantstrat 凝结了一位资深量化从业者多年的实战经验。**
### open-xquant:用 Python 致敬 quantstrat 的方法论
quantstrat 体系完整,但作为本书的学习工具,它有两个不利因素:
1. **R 语言在 AI 编程生态里是少数派**。主流 AI 编程助手对 Python 的支持远好于 R。你写 spec 让 AI 写代码,它写 Python 流畅,写 R 经常磕碰
2. **R 的指标 / 数据库迭代慢**。Python 这边 pandas / NumPy / scikit-learn / akshare / yfinance 都很活跃,新指标新数据源出来很快就有 Python 实现
直接学 R + quantstrat,方法论地道,但后续扩展、找数据、用 AI 都不顺手。
**open-xquant**[github.com/xingwudao/open-xquant](https://github.com/xingwudao/open-xquant))就是为此存在的:
- **沿用 quantstrat 的体系、理念、流程**——尤其是 Peterson 那份 strat_dev_process.pdf 的方法论。从顶层架构(Strategy / Signal / Rule / Position)到验证流程,都参照 quantstrat 的设计思路
- **用 Python 独立实现**——融入 pandas / NumPy 等数据科学栈,AI 能直接对话
- **Agent-First 设计**——你在 spec 里描述“做等权信号”、“做参数扫描”,AI 就知道调用哪个模块;框架约束 AI “怎么做”,**让结果稳定可复现**
> open-xquant 是 quantstrat 的 Python **致敬实现**——继承的是体系、理念、流程,而非代码(不是逐行 R → Python 翻译)。Brian Peterson 等人的工作以 GPL-3 开源,本书谨此致谢。
### 顺带解决:AI 编程的“不可复现”
前言里说过,AI 编程能力的跃迁是这本书能写出来的关键基础。但 AI 编程用在量化交易上有个绕不过去的问题:**不可复现**。
AI 大模型的底层原理是概率采样。同一份 spec,AI 今天写出来的代码和明天写出来的,细节上可能不一样。写网页、画图表无所谓;量化交易不行——同一个策略跑两遍结果不一样,你根本无法判断它到底有没有效。**不可复现 = 不可信 = 不可交易。**
open-xquant 的 Agent-First 接口把“框架做什么”约束得很死——AI 拼接的是积木,不是从零写代码。这样既享受 AI 编程的便利,又保证两次跑出来的结果一样。
> 你已经在「开始动手之前」的环境配置脚本里装过 open-xquant 了——如果当时漏装,现在补装:
>
> `pip install open-xquant[yfinance,akshare]`
>
> 把这行命令复制给 AI 编程工具,让它在终端里执行。遇到网络报错,把错误信息贴给 AI。
---
## 2.3 先拿几只股票试试
第一章用的是沪深300ETF,但你日常听到的投资故事,主角往往是个股——贵州茅台涨了多少倍、中国平安跌了多少、招商银行分红有多高。这些家喻户晓的大公司,能不能直接拿来跑第一章学的均线策略?
直觉告诉你:这些都是 A 股里最优秀的公司,策略在它们身上应该也能跑出不错的结果。真的吗?我们来试试——先猜后验。
### 动手实验 1:个股 vs ETF 信号质量对比
我们一起把这份 spec 写出来。这次重点看两件新东西:**怎么给 AI 一个能查到接口的入口**(让它有据可依地调框架),以及**怎么把对照实验的“唯一变量”锁死**。
(下文 spec 里把 open-xquant 简写为 oxq——和 numpy 简写为 np 一样的意思。)
#### 起草上下文 + 任务
> **上下文**:本章探讨“该选哪些标的”。第一步——先拿大家熟悉的几只个股试一遍第一章学的均线策略,跟沪深300ETF 摆在一起看效果。
>
> **任务**:在 notebook `q2-what-to-buy.ipynb` 中新建代码单元格,对 5 只 A 股个股 + 沪深300ETF 跑均线策略,让你直观看到个股信号的嘈杂。
#### 起草要求:给 AI 接口入口 + 锁死唯一变量
要求段是这份 spec 的关键。这是 open-xquant 框架第一次出场,要求段开头给 AI 一个能查到接口的入口(教程链接),而不是把框架的每个接口签名抄进 spec:
> **要求**
>
> 1. 数据:沪深300ETF`510300.SS`+ 5 只 A 股(`600519.SS` 茅台 / `000858.SZ` 五粮液 / `601318.SS` 中国平安 / `000001.SZ` 平安银行 / `600036.SS` 招商银行),时间窗 `2023-01-01` 到 `2026-01-01` 共 3 年
> 2. 6 个标的跑同一个策略:均线交叉信号(SMA(1) 上穿 SMA(20))、等权入场、信号反转出场——每个标的都是独立的单资产投资宇宙。具体 oxq 模块(`Engine.run` / `Crossover` / `EqualWeightOptimizer` / `ExitRule`)请参考 [open-xquant 教程](https://github.com/xingwudao/open-xquant/tree/main/examples/tutorials)
> 3. 取每个标的的累计收益率和年化波动率
> 4. 两张横向柱状图(figsize 10×8):上图收益率(沪深300蓝色高亮、其余按正负绿/红),下图年化波动率(沪深300蓝色、个股灰色)
oxq 里 `EqualWeightOptimizer` 就是把钱平均分给入选的标的,`ExitRule` 就是按设定的反转条件平仓——你不需要钻接口细节,AI 会去查教程。
> 📌 **要点**:用到框架时,**给 AI 一个能查到接口的入口**(如教程链接),再让它写代码。不要把 `Engine.run(rules=[...], router=..., receiver=...)` 这种接口签名硬抄进 spec——框架升级一次,硬抄的接口就全部失效;给一个入口让 AI 现查现用,经得起框架升级。
> 📌 **要点**:对照实验只留一个变量。这份 spec 让 6 个标的跑**完全一样**的策略——同样的均线周期、同样的时间窗、同样的入场出场规则——这样最终柱状图上的差异就只能归因于“标的本身”。spec 里“6 个标的、同一个策略”必须写死,任何“看 AI 心情”的余地都不能留。
#### 起草结果呈现
> **结果呈现**:每只标的打印 `代码(中文名): 收益率 X.XX% 年化波动率 X.XX%` + 两张横向柱状图 + 三句结论:「5 只个股结果天差地别」「个股波动率 XX%-XX%、ETF 只有 XX%」「组合能对抗波动,ETF 就是一种组合」。
完整 spec 在 [`specs/spec-01-signal-quality.md`](https://github.com/xingwudao/xquant-learning/blob/main/q2-what-to-buy/specs/spec-01-signal-quality.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了一组数字和一张柱状图。
open-xquant 的工作方式和你写 spec 很像:你不用描述 AI 该怎么一步步执行,只需要清楚地说明你要什么。比如“用 20 日均线作为信号,上穿就买、下穿就卖”——框架替你处理所有执行细节。
这里还出现了一个新指标:**年化波动率(Annualized Volatility** ——价格上下波动的剧烈程度,数字越大说明越颠簸。就像坐车,波动率高就是过山车,波动率低就是高铁。年化是把每天的波动换算成一年的尺度,方便理解。
**回到实验设计本身**——6 只标的、同一个策略、同样的参数,唯一的变量是标的本身。结果如图 2-1 所示。
![个股 vs ETF 信号质量对比](images/q2-what-to-buy/01-signal-quality.png)
### 读懂这张图
先看图 2-1 上半部分(收益率):同样的 20 日均线策略,跑在 5 只个股上的结果反差大得超出直觉——从 +34.59% 到 -7.47%,最好和最差之间差了 40 多个百分点。你选中国平安就赚了,选贵州茅台就亏了。同一个策略、同一个时间段,结果完全取决于你碰巧选了哪只股票。
再看图 2-1 下半部分(年化波动率):个股年化波动率普遍在 16%-21%,而沪深300ETF 只有 13.61%。这不是巧合——沪深300ETF 包含 300 只股票,个股各自的噪音互相抵消,波动被分散掉了。均线策略在这种“干净”的趋势信号上,才能正常发挥作用。
**数据说了算:单只股票波动大、不可预测,但组合能对抗这种波动。** ETF 就是一种组合。第一章我们说过,ETF 是“一篮子股票的打包组合”——当时的理由是“适合新手”。现在你有了更深的理解:组合不只是省事,它能从根本上降低噪音,让策略信号更干净。
这个发现让问题一下子简化了——从“在 5000 只个股里选哪只”变成了“在几百只 ETF 里选哪只”。范围缩小了一个数量级。但几百只 ETF 还是太多,怎么继续缩小范围?
---
## 2.4 几百只 ETF,选哪几只?
用 ETF 没问题了,但全球 ETF 有几百只,覆盖美国、中国、欧洲、日本、新兴市场……总不能全买。怎么缩小范围?
最直觉的思路:**经济最强的国家,资本市场应该最有潜力。** 一个国家的 GDP 越高,说明这个国家创造财富的能力越强,企业整体的盈利能力也应该更强——股市自然有更大的上涨空间。**这只是起手的猜测,不是铁律**——日本九十年代后 GDP 全球前列但股市低迷十年就是反例。但作为“圈池子”的起点,它有数据支撑。下一步不是直接相信它,而是用数据验证。先猜后验,拿数据看看。
### 动手实验 2:用 GDP 数据锁定投资方向
同样的写法套路,这次重点演示两件没碰过的事:**怎么把宏观数据源接进来**,以及**怎么把“先猜后验”写进 spec 内部的打印语句**。
#### 起草上下文 + 任务
接续 spec-01——你已经知道“个股嘈杂、用 ETF”,这份 spec 解决“几百只 ETF 怎么缩范围”的开放问题:
> **上下文**:在 `q2-what-to-buy.ipynb` 中已有 spec-01 的“个股 vs ETF”实验代码,读者已理解“用 ETF 替代个股”。当前开放问题:**全球 ETF 几百只,怎么缩小范围?**
>
> **任务**:用全球主要经济体的 GDP 数据支撑“锁定中美”的选择,并下载对应的两只 ETF(沪深300ETF + 纳指100ETF)。
#### 起草要求:宏观数据源 + 两步式接口
宏观数据(GDP)和行情数据(ETF 价格)来自不同源、有不同的更新频率,但都包在 oxq 同一种“先 download → 再 read”的两步式接口里:
> **要求**
>
> 1. 用世界银行数据下载器拉全球前 10 大经济体 GDP`["USA", "CHN", "DEU", "JPN", "IND", "GBR", "FRA", "ITA", "BRA", "CAN"]`),时间窗 2020-2024 共 5 年(取最新一年使用,前 4 年留作缓冲、防止当年数据延迟发布)
> 2. 读出数据,取最新一年,单位换算为万亿美元
> 3. 横向柱状图(figsize 10×6):从高到低排列,中红美蓝高亮、其余灰色,柱子右侧标具体数值,标题「全球前 10 大经济体 GDP」
> 4. 用 yfinance 数据下载器拉两只 ETF`510300.SS`(沪深300ETF,代表中国核心资产)+ `513100.SS`(纳指100ETF,A 股上市的美国科技龙头),时间窗 `2021-01-01` 到 `2026-01-01`
> 5. 读出数据,画归一化走势图(figsize 12×6,起点=100,两色实线,图例标中文名)
>
> 具体 oxq 模块(`WorldBankDownloader` / `read_factor` / `YFinanceDownloader` / `LocalMarketDataProvider.get_bars`)请参考 [open-xquant 教程](https://github.com/xingwudao/open-xquant/tree/main/examples/tutorials)。
> 📌 **要点**:用一个数据锚点支撑你的选择。这份 spec 把“数据源 → 锚点 → 选择”三段写顺:先取 GDP 数据(来源),再算“中美遥遥领先”(锚点),最后下载对应 ETF(选择)。三段缺一不可,缺锚点段就成了“凭印象选标的”。
> 📌 **要点**:spec 内部就要把“先猜后验”的三段闭环打印出来。结果呈现里**预先写好**「最直觉的思路:经济最强的国家,资本市场应该最有潜力」(猜想)→「数据说了算:锁定中国 + 美国」(验证)→「但中美都是股市,同时下跌怎么办?」(开放下一份 spec 的问题)。让 AI 跑完 notebook,**结果会自己讲故事**——不用你回头补叙事。
#### 起草结果呈现
> **结果呈现**:GDP 柱状图 + 两条 ETF 归一化曲线 + 三句打印(猜想/验证/开放问题,见上方要点)。
完整 spec 在 [`specs/spec-02-gdp-anchor.md`](https://github.com/xingwudao/xquant-learning/blob/main/q2-what-to-buy/specs/spec-02-gdp-anchor.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了两张图和一组分析文字。
这里用到的 GDP(国内生产总值)是一个国家一年内创造的商品和服务的总价值,是衡量经济规模最常用的指标。GDP 告诉我们中美经济最强,接下来就去拿对应的 ETF 数据。`510300.SS` 是沪深300ETF——跟踪沪深两市市值最大的 300 家公司,代表中国核心资产的整体表现。`513100.SS` 是纳指100ETF(即纳斯达克100ETF)——在 A 股上市,跟踪纳斯达克100指数,涵盖苹果、微软、英伟达等美国科技成长龙头。两只 ETF 都在 A 股交易,交易日历完全一致。
走势对比图中出现了一个新概念:**归一化(Normalization)** ——把不同起点的价格都调整到同一个起跑线(100),这样才能公平比较谁涨得多、谁跌得多。两只 ETF 单价不同,直接放在一张图上没法比。归一化就像让两个选手从同一条起跑线出发——起点都是 100,后面谁高谁低,就是谁涨得多谁跌得多。
全球前 10 大经济体 GDP 对比如图 2-2 所示,两只 ETF 的归一化走势如图 2-3 所示。
![全球 GDP 排名](images/q2-what-to-buy/02-gdp-ranking.png)
![沪深300ETF vs 纳指100ETF 归一化走势](images/q2-what-to-buy/03-etf-comparison.png)
### 读懂这两张图
先看图 2-2 GDP 柱状图:美国和中国遥遥领先,合计占全球 GDP 的约 40%,远超第三名的德国。第一梯队和第二梯队之间的差距不是一点半点——美国约 29 万亿美元、中国约 18 万亿美元,而德国只有约 4.5 万亿美元。经济体量决定了资本市场的深度和广度,选 ETF 自然要从最大的两个经济体入手。对应的投资标的也很明确:沪深300ETF 代表中国最大的 300 家上市公司,纳指100ETF 代表美国科技成长的龙头企业。**数据说了算:锁定中美。**
再看图 2-3 归一化走势:两条线的走势并不完全同步——有时候纳斯达克涨而沪深300在调整,有时候沪深300反弹而纳斯达克在横盘。这其实是好事。如果两个市场完全同步涨跌,那买两只和买一只没有本质区别。正是因为它们的节奏不同,当一个市场表现不好的时候,另一个市场可能在涨,组合整体的波动就被平滑了。这就是“分散”的真正含义——不是随便多买几个,而是买涨跌节奏不同的标的。
但仔细想想,你可能会有一个担忧:中美毕竟都是股票市场。如果遇到全球性的恐慌——比如 2022 年的全球加息周期——两个市场有可能一起跌。图 2-3 里其实也能看到这样的时段。那怎么办?
都是股市,同涨同跌并非不可能。我们需要一个“不同步”的东西来保护组合——一个在股市下跌时不一定跟着跌、甚至可能逆势上涨的资产。这样的东西存在吗?
---
## 2.5 加什么能保护组合?
我们要找的是一个在股市下跌时不一定跟着跌的东西。债券、现金都是候选,但还有一个更经典的选项——每次股市大跌的新闻里,总会提到“资金涌入黄金避险”。黄金常被称为“避险资产”,意思是当股市恐慌的时候,投资者倾向于把钱从股市撤出,转移到黄金这种“硬通货”上,推高金价。
这个说法听起来有道理,但“听起来有道理”不是我们做决策的依据。先猜后验——我们来用数据验证:黄金和股市的涨跌到底有没有关系?如果真的关系不大,加入黄金能给组合带来多大的保护效果?
### 动手实验 3:相关性分析与投资宇宙构建
最后一份 spec,难度上一个台阶——重点是**金融指标的计算路径要在 spec 里点名**,以及**怎么用 oxq 的 `StaticUniverse` 把“投资宇宙”这个抽象概念封装成可调用对象**。
#### 起草上下文 + 任务
接续 spec-02 留下的开放问题——中美都是股市,需要找一个不同步的资产来保护组合:
> **上下文**:在 `q2-what-to-buy.ipynb` 中已有沪深300ETF + 纳指100ETF 数据,读者已通过 GDP 锁定中美。当前开放问题:**中美都是股市,可能同涨同跌,加什么能保护组合?**
>
> **任务**:引入黄金ETF 作为对冲资产,用相关性矩阵验证三资产的分散效果,最后用 `StaticUniverse` 把整章的“投资宇宙”概念封装成可调用对象。
#### 起草要求:金融指标显式 + 概念落地
要求段有两件难事:① 金融指标(日收益率 / 累计收益 / 年化波动率 / 最大回撤)对 0 金融基础读者是黑箱,必须在 spec 里点名计算路径;② `StaticUniverse` 是 q3-q9 反复用到的核心对象,第一次出场要让读者看清“它是个什么东西”。
> **要求**
>
> 1. 用 yfinance 数据下载器拉黄金ETF`518880.SS`),时间窗 `2021-01-01` 到 `2026-01-01`,与前两只 ETF 对齐
> 2. 读出三只 ETF 的收盘价,合并成一个 DataFrame,列名改中文(沪深300 / 纳指100 / 黄金)
> 3. 计算相关性矩阵:先 `pct_change()` 算日收益率,再 `.corr()` 出 3×3 矩阵
> 4. 相关性热力图(figsize 8×6imshow + coolwarm,每格标 2 位小数,标题「三资产日收益率相关性」)
> 5. 组合 vs 单押对比——按下方“三个金融指标的计算路径”给出的公式实现累计收益率、年化波动率、最大回撤
> 6. 累计收益率对比图(figsize 12×6,单押沪深300 灰色虚线 + 三资产等权 绿色实线,图例标方案名)
> 7. 用 `StaticUniverse(symbols=[...], name="global-macro-etf")` 构建投资宇宙,调用 `get_universe(as_of_date="2026-01-01")` 取出快照
>
> 具体 oxq 模块(`YFinanceDownloader` / `LocalMarketDataProvider.get_bars` / `StaticUniverse` 的 `get_universe(as_of_date=...)` 返回结构)请参考 [open-xquant 教程](https://github.com/xingwudao/open-xquant/tree/main/examples/tutorials)。
要求 5 用到的三个金融指标,计算路径都要在 spec 里点名——别让 AI 凭感觉写。三条公式如下:
- 累计收益率:`(1 + 日收益率).prod() - 1`
- 年化波动率:`日收益率.std() * sqrt(252)`252 是年交易日数)
- 最大回撤:累计净值 / 累计净值的历史最大值 - 1,取最小值
> 📌 **要点**:spec 里写到金融指标,要给出计算路径——要么写出公式(如 `日收益率.std() * sqrt(252)`),要么明确指向 oxq 现成函数。“计算最大回撤”这种自然语言反例,AI 写出的代码 5 个版本可能 5 个公式,对不上。0 金融基础时,公式比“术语 + 凭感觉”靠谱十倍。
> 📌 **要点**:章末 spec 把整章概念封装成可调用对象。`StaticUniverse(name="global-macro-etf")` 把“投资宇宙”从概念变成可命名、可快照、可传给下游 spec 的研究对象——q3-q9 的每一份策略 spec 都要从一个 universe 起手。第一次出场就给它一个语义明确的 name,不要写 `name="u1"` 这种凑数的名字。
#### 起草结果呈现
> **结果呈现**:相关性热力图 + 两句相关性解读(「沪深300 与 纳指100:X.XX」「沪深300 与 黄金:X.XX」)+ 三指标对比表(用 `pd.DataFrame.to_string` 打印,避免 markdown 表格漂移)+ 累计收益率对比图 + 一句结论「数据说了算:分散风险的关键不是多买几个,而是买涨跌不同步的」+ 投资宇宙构建结果 `「投资宇宙构建完成:{snapshot.symbols}」`。
完整 spec 在 [`specs/spec-03-correlation.md`](https://github.com/xingwudao/xquant-learning/blob/main/q2-what-to-buy/specs/spec-03-correlation.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了两张图、一组相关性数字和一个对比表格。
`518880.SS` 是黄金ETF,在 A 股上市,买它就等于间接持有黄金。不需要去金店买金条,通过黄金ETF 就能参与黄金市场。三只 ETF 都在 A 股交易,交易日历完全一致。
这个实验引入了两个重要的新概念。
**日收益率(Daily Return** ——今天的价格比昨天涨了还是跌了,涨跌幅度是多少。比如今天收盘价 105,昨天 100,日收益率就是 +5%。为什么要先算日收益率?因为我们关心的不是“今天价格是多少”,而是“今天比昨天变了多少”。价格本身没有可比性——三只 ETF 单价各不相同——但日收益率把它们拉到了同一个尺度上。
**相关性(Correlation** ——两个东西的涨跌有多“同步”。数值范围从 -1 到 1:1 表示完全同步涨跌,0 表示毫无关系,-1 表示完全反着来。相关性就像两个人走路的默契度——1 是齐步走,0 是各走各的,-1 是你往左他就往右。我们需要的是接近 0 的——各走各的,这样一个跌的时候另一个不一定跟着跌。
热力图用颜色深浅表示相关程度——颜色越红越“同步”,越蓝越“反着来”,白色或浅色代表“关系不大”。对角线上全是 1.00——这是自己跟自己比,当然完全同步。
实验还设计了两种方案来对比:单押方案(100% 全部买沪深300)和等权组合(沪深300、纳指100、黄金各买 1/3)。除了年化波动率,这里还引入了一个新指标:
**最大回撤(Max Drawdown** ——从最高点到最低点跌了多少,衡量“最惨的时候有多惨”。比如你的账户从 100 万涨到 120 万,又跌回 90 万,最大回撤就是 (120-90)/120 = 25%。这个指标直接关系到你晚上能不能睡好觉——回撤越大,心理压力越大。
三资产之间的相关性热力图如图 2-4 所示。
![三资产相关性热力图](images/q2-what-to-buy/04-correlation-heatmap.png)
单押与等权组合的核心指标对比如表 2-3 所示。
**表 2-3 单押沪深300 vs 三资产等权组合的指标对比**
| 指标 | 单押沪深300 | 三资产等权组合 |
|------|------------|---------------|
| 累计收益率 | -1.78% | 89.50% |
| 年化波动率 | 18.11% | 12.36% |
| 最大回撤 | -42.16% | -16.49% |
累计收益率走势如图 2-5 所示。
![单押 vs 等权组合累计收益率](images/q2-what-to-buy/05-portfolio-comparison.png)
### 读懂这些结果
先看图 2-4 热力图和相关性数字。沪深300与纳指100的相关性是 0.26——有一定联动,但远不是同步涨跌。黄金与两个股市的相关性分别只有 0.09 和 0.05,几乎接近 0,说明黄金与股市基本没有联动——股市涨的时候黄金不一定涨,股市跌的时候黄金也不一定跌。“避险资产”这个说法,数据给出了支持。
再看表 2-3 对比。这里的重点不是“等权组合赚了 89.50% 而单押只有 -1.78%”——虽然收益差距确实很大,但不同时间段跑可能结果不同。真正值得关注的是另外两个指标:
- **波动率从 18.11% 降到 12.36%**。颠簸程度降了三分之一。原来坐的是过山车,现在换成了大巴车——同样是在路上走,但体验完全不同。
- **最大回撤从 -42.16% 降到 -16.49%**。最惨的时候,单押方案从最高点跌了 42%——你的 100 万变成了 58 万。而三资产组合最多只跌了 16%——100 万变成 84 万。回撤缩小了六成,晚上能多睡几个小时。
图 2-5 的累计收益率对比更直观:灰色虚线(单押沪深300)大起大落,绿色实线(三资产等权组合)走得更稳、更平滑。不是没有波动,但波动的幅度小了很多。
**数据说了算:分散风险的关键不是多买几个,而是买涨跌不同步的。**
回顾一下我们走过的三步:个股噪音大,换成 ETF(组合对抗波动);ETF 太多,用 GDP 锚定中美;中美可能同跌,加入低相关的黄金。每一步都是“先猜后验”——先有直觉判断,再用数据验证。三步走完,三只 ETF 选定:**沪深300ETF、纳指100ETF、黄金ETF**。
三步走下来,标的选好了。但别急——回头看看,你刚才到底做了什么?从几千只标的一路筛选到 3 只 ETF,这个过程本身,其实已经是量化交易中一个重要的专业步骤。
---
## 2.6 回头看:你刚才做了什么?
三步筛选下来,你从全市场几千只标的中选出了 3 只 ETF。这个“策略考虑的标的范围”,在量化交易中有一个专门的名字:**投资宇宙(Universe)** ——你的策略只在这个范围内寻找机会,范围之外的标的一概不看。
构建投资宇宙不是偷懒,恰恰相反,它让后续每一步都更有效:
- **降低噪声**:2.3 的实验验证过,把个股排除后,信号更干净。低质量的标的越早剔除,后面的分析越不容易被干扰。
- **缩小范围**:从几百只 ETF 缩到 3 只,后续做策略研究时计算量可控,每一个标的都值得你认真对待。
- **风控前置**:选标的本身就是第一道风控——流动性差的、数据质量低的、跟你的策略逻辑不相关的,在这一步就被挡在门外了。不用等到回测时才发现“这个标的根本不该出现在我的组合里”。
你可能没有意识到,但你刚才做的事情,和华尔街的量化基金经理做的第一步是一样的——先定义投资宇宙,再谈策略。
---
## 2.7 本章总结
三只 ETF 选定之后,我们把本章的核心概念、策略进化路径和最重要的收获一起收口。**到这里,你完成了飞轮的“候选层”——主练动作仍是“做”:把“选什么”从拍脑袋变成了“用 GDP 和相关性两个具体指标筛”,剩下的飞轮三层从下一章开始接力。**
### 概念速查表
本章涉及的核心概念汇总如表 2-4 所示,方便随时回查。
**表 2-4 第二章核心概念速查**
| 概念 | 含义 | 类比 |
|------|------|------|
| 指标(Indicator) | 从原始价格数据中算出来的衍生数字 | SMA 就是一种指标——均线 |
| 归一化(Normalization) | 把不同起点的价格调整到同一起跑线 | 让两个选手从同一条起跑线出发 |
| 日收益率(Daily Return) | 今天比昨天涨跌了多少 | 每天量体重,看涨了还是跌了 |
| 相关性(Correlation) | 两个东西的涨跌有多同步,-1 到 1 | 两个人走路的默契度 |
| 年化波动率(Annualized Volatility | 价格波动的剧烈程度 | 过山车 vs 高铁 |
| 最大回撤(Max Drawdown) | 从最高点到最低点跌了多少 | 最惨的时候有多惨 |
| 投资宇宙(Universe) | 策略考虑的标的范围 | 白板上钉的名牌 |
### 策略进化路径
三步筛选下来,路径一目了然:
全市场几千只标的 → Step 1:个股噪音大,选 ETF(组合对抗波动)→ Step 2:GDP 锚定,锁定中美 → Step 3:低相关性,加入黄金 → 投资宇宙:沪深300ETF、纳指100ETF、黄金ETF
每一步的决策与教训如表 2-5 所示。
**表 2-5 第二章策略进化路径**
| 步骤 | 决策 | 教训 |
|------|------|------|
| Step 1 | 个股 → ETF | 组合对抗波动,个股噪音太大 |
| Step 2 | 几百只 ETF → 中美 | 选择要有数据支撑的理由 |
| Step 3 | 中美 → 中美+黄金 | 分散风险靠“不同步”,不靠“数量多” |
### 本章最重要的收获
三步筛选本身并不复杂——选 ETF、看 GDP、算相关性,每一步你都能理解。但真正值钱的不是这三步的结论,而是贯穿始终的那个思维习惯:**先猜后验,数据说了算。**
每一步我们都是先有一个直觉猜测,然后用数据去验证。猜对了,数据给你信心;猜错了,数据让你及时转向。这个习惯比任何具体的标的选择都重要——市场会变,标的会换,但“用数据验证直觉”这个方法经得起时间检验。
### 带走的问题
标的选好了,但关键问题还没回答:
- **沪深300、纳指100、黄金,各买多少?** 等分?还是有更聪明的分法? → 第三章
- **什么时候买入、什么时候卖出?** → 第四章
- **怎么验证这个组合确实有效?** → 第五章
答案都在后面。继续。
> 本章所有代码的可运行版本见 `notebooks/q2-what-to-buy.ipynb`
---
## 拓展阅读
本章正文聚焦“为什么从 ETF 开始 + 怎么构建投资宇宙”。下面三节是动手之前可以省略的视野铺垫。
### 投资市场上还有什么可以买?
ETF 不是唯一的资产类型——它只是新手最该先碰的那个。投资市场里还有这些可以买卖的资产:
- **股票(Stock)** —— 买一家公司的一小块所有权。公司赚钱了你分红,公司涨价了你的股份也值钱了。风险:单个公司可能经营不善甚至倒闭。
- **ETF(交易所交易基金)** —— 一篮子股票的打包组合。比如沪深300ETF(510300)打包了 A 股市值最大的 300 家公司。买一份 ETF = 同时持有 300 家公司,风险分散。
- **债券(Bond)** —— 就是一张“借条”。你买国债 = 借钱给国家,到期还本付息。风险低,收益也相对低。
- **期货 / 期权** —— “衍生品”,自带杠杆,风险极高,可能亏损超过本金。**新手先别碰。**
### 在哪里交易?
**交易所(Exchange** 是个巨大的菜市场——只是买卖的不是菜,而是股票、基金、债券。中国有三个主要交易所:上交所(代码以 6 开头)、深交所(代码以 0 或 3 开头)、北交所(代码以 8 开头)。
但你不需要跑去交易所。现代人都通过**券商** App 交易——券商是你和交易所之间的“中介”,常见的有华泰、中信、招商、东方财富等。开户和入金不急,等到第七章确认策略靠谱了再动。
### 市场上的几种交易方式
我们课程用的是趋势交易(均线、动量)。其实市场上的交易方式不止这一种,常见三类如表 2-6 所示。
**表 2-6 市场上的三种主流交易方式**
| 方式 | 通俗理解 | 时间尺度 |
|------|----------|----------|
| **价值投资** | 找到好公司,便宜时买入,持有数年 | 月线、年线 |
| **趋势交易** | 发现涨的趋势就跟上,反转就撤 | 日线、周线 |
| **统计套利** | 两个相关的东西价格差异,赚差价 | 日内~几天 |
价值投资和趋势交易的核心区别在于**时间尺度**——同一只标的,月线看到的是长期价值,日线看到的是短期趋势。同一个世界,不同的视角。不管哪种方式,**先构建投资宇宙、再做分析**这个原则都一样——投资宇宙是所有策略的起点。
+420
View File
@@ -0,0 +1,420 @@
# 第三章:给 ETF 分钱:3 种分法实测
第二章我们从全市场几千只标的中,一步步筛选出了三只 ETF——沪深300、纳指100、黄金,构建了自己的投资宇宙。选标的的问题解决了,但紧接着一个新问题摆在面前:三只 ETF,各买多少?
这听起来像个分钱小问题——平均分一下不就完了吗?别急着下这个结论。同样三只 ETF、同一段历史,**只是分法不同,累计收益能差 1 倍以上、最大回撤能差三分之一**:等权 80%、风险平价 100%、动量排名 180%;回撤从 -16% 到 -12% 又到 -15%。“买什么”决定了你站在哪条赛道上,**“买多少”决定了赛道上你能不能跑下去**——这就是为什么一整章都在解决这个问题。
### 路线图
我们正在走一条完整的策略构建之路:
**选什么标的(第二章 已完成)→ 每个买多少(第三章 本章)→ 什么时候买卖(第四章)→ 怎么验证有效(第五章)**
**这一章你在飞轮的“组合层”,主练“做”——把“各买多少”从凭感觉变成有据可依的规则。** 本章解决“买多少”,分三步探索三种分钱方式,最后你来判断哪种更合理。三节分别对应的方法如表 3-1 所示。
**表 3-1 第三章三节问题与方法对照**
| 节 | 问题 | 方法 |
|----|------|------|
| 3.1 | 最简单的回答:各买三分之一 | 等权 |
| 3.2 | 波动大的少买点,波动小的多买点 | 风险平价 |
| 3.3 | 涨得好的多买点,跌的别买 | 动量排名 |
(操作流程见前言“怎么使用这本书”。每一节按相同流程:阅读 → 复制 spec 给 AI → 看结果与解读。)
---
## 3.1 最简单的回答:各买三分之一
三只 ETF,各买多少?最自然的回答:平均分,每只买三分之一。不偏不倚,公平合理。毕竟,如果你不确定哪只会涨得更好,平均分至少不会犯大错——对吧?
我们来验证一下这个直觉。
### 动手实验 1:等权组合回测
这个实验用到 open-xquant 的几个模块:`EqualWeightOptimizer`(等权优化器——把钱平均分)、`Engine`(回测引擎——模拟历史买卖)、`SimBroker`(模拟券商)。你不需要了解它们的代码,spec 会告诉 AI 怎么用。
我们一起把这份 spec 写出来。这次重点看三件新东西:**oxq 模块的“先读后写”**、**权重和=1.0 的关键断言**、**跨市场组合的共同交易日处理**。
#### 起草上下文 + 任务
这是 q3 的第一份 spec,直接接续第二章——三只 ETF 已经选好了,现在只关心权重。两段都很短:
> **上下文**:第二章选好了 3 只 ETF(沪深300、纳指100、黄金),现在要回答:每只各买多少?
>
> **任务**:在 notebook `q3-how-much.ipynb` 中创建代码,跑等权组合回测,并展示三只 ETF 各自的表现差异。
#### 起草要求
q3 起 open-xquant 框架正式上场。spec 主体的第一件事不是写代码,而是要求 AI 先读源码——这样它调的是真实接口,不会用过时或编造的写法。
> **要求**
>
> 1. 阅读以下 oxq 模块的源码(按用途分四组):
> - 引擎与策略:`oxq.core.Engine` / `Strategy`
> - 数据:`oxq.data.YFinanceDownloader` / `LocalMarketDataProvider`
> - 仓位优化:`oxq.portfolio.optimizers.EqualWeightOptimizer`
> - 信号、指标、规则:`oxq.signals.Threshold` / `oxq.indicators.RollingVolatility` / `oxq.rules.RebalanceFrequencyRule` / `oxq.universe.StaticUniverse` / `oxq.trade.SimBroker`
> 2. 下载 3 只 ETF`SYMBOLS = ("510300.SS", "513100.SS", "518880.SS")`,起 `2021-01-01`)。
> 3. 计算共同交易日:中美交易日历不同步,要把三只 ETF 的索引取交集,后续所有图都用这个对齐索引。
> 4. 用 `EqualWeightOptimizer` + `Threshold` 信号(column=“close”, threshold=0, relationship=“gt”)组装策略,再平衡 `RebalanceFrequencyRule(interval_days=10)`。
> 📌 **要点**:跨市场组合必须显式处理共同交易日。沪深300 走 A 股日历,纳指100 走美股日历,圣诞节美股休市但 A 股开市,反过来春节 A 股休市但美股开市。**spec 不点名,AI 大概率不会想到这一步**——画出来的净值曲线会是锯齿状。这是 q3 起跨市场章节的领域必备配置块,跟 q1 的字体三选一一样,要写进 spec。
#### 起草结果呈现
结果呈现段把权重检查、对比表、净值曲线一次锁死,让 AI 输出可以直接对照书里的图表。
> **结果呈现**
>
> 1. **权重合法性检查**`weights = ew_weights_df.iloc[-1]; assert abs(weights.sum() - 1.0) < 1e-6, f"权重和应为 1.0,实际 {weights.sum()}"; assert (weights >= 0).all(), "等权权重不应为负"`。
> 2. 打印权重分配 + 10 万元换算(每只约 33,333 元)。
> 3. 打印 4 行对比表:沪深300 / 纳指100 / 黄金 / 等权组合,三列指标(累计收益率、年化波动率、最大回撤)。
> 4. 净值曲线图:3 条灰色线(用 `--` / `:` / `-.` 三种线型区分单只持有)+ 1 条蓝色实线加粗(等权组合),归一化到 100,标题「各买三分之一:等权组合 vs 单只持有」。
> 📌 **要点**:仓位分配 spec 的**必查**断言是「权重和 = 1.0 ± 1e-6」。这条 assert 在 q1 单标的策略里不需要(只有一个标的,要么买要么不买),但只要进入“分钱”场景,这条就必须有。**没有这条断言,AI 输出 0.998 或 1.003 你都会以为正常**——但下游回测会按这个权重直接乘资金,误差会一路传染到最后的收益率。三个 spec 都要复用这条模板。
完整 spec 在 [`specs/spec-01-equal-weight.md`](https://github.com/xingwudao/xquant-learning/blob/main/q3-how-much/specs/spec-01-equal-weight.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了一组权重数字、一张对比表和一张净值图。我们先认识两个新概念。
**权重(Weight** ——“这笔钱怎么分”,每只标的分到总资金的百分之多少。你有 10 万元,三只 ETF 各 1/3 权重,就是每只买 33,333 元。权重就像分蛋糕——蛋糕总共 100%,每人分多少取决于权重。
**信号(Signal** ——根据指标对市场做出的判断:要不要动、怎么动。信号只表达意愿,不负责执行。第一章的 Crossover 信号说的是“买还是不买”——价格上穿均线就买、下穿就卖。这里的 EqualWeightOptimizer 说的是“各买多少”——三只 ETF 各 1/3。判断的内容升级了,但本质都是信号:观察市场、做出判断。至于判断之后怎么执行——下多少单、什么价格成交——那是后面的事。
回测中设定了每 10 个交易日(大约两周)重新调整一次持仓,让实际权重回到目标权重。为什么需要再平衡?因为三只 ETF 涨跌幅不同,持有一段时间后,涨得多的那只实际占比会变大,偏离了最初的 1/3。再平衡就是定期“拨乱反正”。
### 实验结果
等权就是等权,三只各占三分之一,每只买 33,333 元。但等分钱之后跑出来的效果如何?三只 ETF 单独持有与等权组合的对比如表 3-2 所示。
**表 3-2 三只 ETF 单独持有 vs 等权组合表现**
| 标的 | 累计收益率 | 年化波动率 | 最大回撤 |
|------|-----------|-----------|---------|
| 沪深300ETF | -1.78% | 18.11% | -42.16% |
| 纳指100ETF | 106.09% | 22.66% | -27.50% |
| 黄金ETF | 188.54% | 15.83% | -16.77% |
| **等权组合** | **80.75%** | **11.73%** | **-16.79%** |
净值曲线对比如图 3-1 所示。
![各买三分之一:等权组合 vs 单只持有](images/q3-how-much/01-equal-weight-nav.png)
### 读懂这些结果
先看图 3-1 的大趋势:等权组合那条蓝色实线,稳稳地走在三条灰色线之间——不是最好的,也不是最差的。黄金一路领涨,纳指100先跌后涨、波折不断,沪深300整体低迷。组合把三者的表现“平均”了一下,走出了一条居中的路线。
但你可能已经注意到了:黄金单独拿的累计收益率是 188.54%,比等权组合的 80.75% 高了一倍多。那为什么不全买黄金?
看波动率那列。**年化波动率(Annualized Volatility** 衡量的是价格上下波动的剧烈程度——第二章我们认识过这个指标。波动率大意味着你的账户大起大落:今天涨 3%、明天跌 4%,反复折腾。现实中大部分人受不了这种颠簸——涨的时候开心,连跌几天就开始怀疑“是不是该卖了”,一卖可能就错过了后面的反弹。**波动率小,意味着你能在低点不慌、在涨势中不慌——账户里看着一时少了 5%、10%,你不会马上卖掉。** 不慌,才能真正拿到那段收益。
再看一个更关键的问题。三只 ETF 的年化波动率从 15.83%(黄金)到 22.66%(纳指100),差了好几个百分点。但等权信号说“不管你多颠簸,都买一样多的钱”。想想这意味着什么:波动最大的纳指100,虽然只花了 1/3 的钱,但它每天的涨跌幅度最大,对组合整体的影响也最大——它一跌,组合就被拖着跌;它一涨,组合的涨幅也主要靠它撑着。**平均分钱,并不等于平均分影响力。** 波动大的那只,用同样的钱产生了更大的影响力,**拉着整个组合的涨跌走**。
有没有更聪明的分法?比如,波动大的少买点,波动小的多买点——让每只 ETF 对组合的“影响力”真正平均?
---
## 3.2 波动大的少买点,波动小的多买点
上一节的结论很清楚:平均分钱,并不等于平均分影响力。波动大的纳指100,用同样的钱却产生了更大的影响力,把整个组合的涨跌拉着走。那反过来想——既然波动大的影响力太大,能不能给它少分点钱?波动小的那只影响力不够,多分点钱给它?这样三只 ETF 对组合的“拉扯力度”应该更均匀。
这种“按波动率倒数分配权重、让每个资产对组合波动的贡献相等”的方法,有一个正式的名字:**风险平价(Risk Parity)** ——“风险”指的是波动率,“平价”指的是让每个资产的风险贡献相等。让每个资产对组合波动的贡献相等——就像把三个发动机的功率调到一样大,整个组合不会被某一个拖着走。桥水基金的“全天候策略”用的就是这个思路。下面我们用一个生活化的类比直接走进它。
想象你要把一笔钱交给三个人帮你打理。第一个人成熟稳重,赚了不炫亏了不慌,你看着他的账户心里很踏实;第二个人喜怒无常,今天信心爆棚明天就想清仓,你的心跟着他上上下下;第三个人介于两者之间。你会怎么分钱?肯定是稳重的那个多管点,情绪化的那个少管点。三只 ETF 也一样——波动率就是它们的“情绪起伏程度”。波动小的稳重,多买;波动大的情绪化,少买。
### 动手实验 2:风险平价组合回测
我们一起把这份 spec 写出来。这次重点看两件新东西:**signal.required_indicators 在信号上挂指标**、**差量式 spec 让差异点单点暴露**。
#### 起草上下文 + 任务
这是 q3 的第二份 spec,接续 spec-01 的等权回测结果。上下文一句话承接,任务一句话点明:
> **上下文**:在 `q3-how-much.ipynb` 中已有等权组合回测结果。学员已看到三只 ETF 波动率差异很大,等权分配让波动大的 ETF 主导了组合涨跌。当前问题:能不能按波动来分——波动大的少买,波动小的多买?
>
> **任务**:在 notebook 中新建代码单元格,用风险平价信号运行回测,对比等权组合。
#### 起草要求
风险平价要用波动率,但波动率是怎么传给优化器的?oxq 的做法是**在信号上挂指标依赖**——这是 oxq 框架的关键模式,把指标声明和信号绑在一起,再把公共配置抽出做成对照实验:
> **要求**
>
> 1. 阅读 `oxq.portfolio.optimizers.RiskParityOptimizer`(若 spec-01 未读 `Threshold`,补一下)。
> 2. 在 `Threshold` 信号上注册 vol 指标:`signal_rp.required_indicators = {"vol": (RollingVolatility(), {"column": "close", "period": 20})}`。这样回测引擎跑到信号时会自动算 vol 列,优化器再读这一列分配权重。
> 3. 用 `RiskParityOptimizer(volatility_col="vol")` 装配优化器,打印最新一天与等权权重的对比 + 10 万元换算。
> 4. 抽出公共配置 `COMMON = dict(universe=universe, ...)`,让等权和风险平价两个策略只在 `portfolio` 一行不同——`Strategy(name="equal-weight", **COMMON, portfolio=EqualWeightOptimizer())` vs `Strategy(name="risk-parity", **COMMON, portfolio=RiskParityOptimizer(...))`。
> 📌 **要点**oxq 用 `signal.required_indicators` 把“信号需要哪些指标”声明在信号本身上——q3 三个 spec 都依赖这条语法。**spec 要给到具体写法**(“在信号上注册”+ 字典字面量),AI 才能照抄;只写描述,它要么抄到 strategy 上,要么挂到 universe 上,接口对不上就报错。同样的精确度还体现在 COMMON 字典的设计上:把公共部分抽出来,让等权与风险平价的唯一差异落在 portfolio 一行——这是仓位分配章三联 spec 的主线设计,**写了 COMMON 就要兑现**,否则这条教学就失效。
#### 起草结果呈现
这一段把“权重检查 + 视觉契约”两件事一起锁——分析话术按实际数据走向描述,避免提前替结果下定义。
> **结果呈现**
>
> 1. **权重检查**:复用 spec-01 的 assert 模板,加一条`assert rp_w.nunique() > 1, "风险平价权重不应全相等(否则退化为等权)"`。
> 2. 权重对比表 + 10 万元换算 + 指标对比表。
> 3. 净值曲线对比图(figsize 12×6,等权灰虚 + 风险平价蓝实) + 权重历史堆叠面积图(figsize 12×4,展示三只 ETF 权重随时间变化)。
> 4. 末尾分析话术按数据动态描述方向(若波动率反升说明 vol 估计期偏短,不要硬写“降到”)。
> 📌 **要点**:仓位分配章的标志可视化是**权重历史堆叠面积图**——它让学员一眼看到“权重不是一成不变的”。等权下三条带等宽,风险平价下面积带宽随时间变化。spec 必须明确画这张图,figsize 12×4,堆叠面积,y 轴 0-1。这是 q3 起会反复出现的视觉模板。
完整 spec 在 [`specs/spec-02-risk-parity.md`](https://github.com/xingwudao/xquant-learning/blob/main/q3-how-much/specs/spec-02-risk-parity.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了新的权重数字、一张对比表、两张图。
风险平价信号的用法和 3.1 的等权信号完全一样——只是换了一个信号类,其余不变。但钱真的分得不一样了:等权是三等分,风险平价则是波动小的多分、波动大的少分。同样 10 万元,沪深300ETF 从 33,333 元涨到了 45,510 元,黄金ETF 从 33,333 元降到了仅 11,239 元。
策略组装和上一节几乎一模一样——整个策略定义只有组合优化器那一行不同,从 `EqualWeightOptimizer()` 换成 `RiskParityOptimizer()`。其余的投资宇宙、信号、指标、再平衡频率、回测引擎全部复用。当框架搭好之后,换一种“分钱方式”就只是换一个优化器。
### 实验结果
两种方案的权重对比如表 3-3 所示。
一个细节先点透:表 3-3 的权重按**最近 20 天的滚动波动率**算,与表 3-2 那行**全期年化波动率**不是同一组数字——短期与长期波动可以差很多。
**表 3-3 等权 vs 风险平价权重对比**
| 标的 | 等权权重 | 风险平价权重 |
|------|---------|------------|
| 沪深300ETF | 33.3% | 45.5% |
| 纳指100ETF | 33.3% | 43.3% |
| 黄金ETF | 33.3% | 11.2% |
指标层面,两种方案的差异如表 3-4 所示。
**表 3-4 等权 vs 风险平价指标对比**
| 指标 | 等权组合 | 风险平价组合 |
|------|---------|------------|
| 累计收益率 | 80.75% | 99.67% |
| 年化波动率 | 11.73% | 10.81% |
| 最大回撤 | -16.79% | -12.26% |
净值曲线对比如图 3-2 所示,权重随波动率变化的历史轨迹如图 3-3 所示。
![等权 vs 风险平价](images/q3-how-much/02-ew-vs-rp-nav.png)
![风险平价:权重如何随波动率调整](images/q3-how-much/03-rp-weight-history.png)
### 读懂这些结果
先看图 3-2 的净值曲线。蓝色实线(风险平价)走得比灰色虚线(等权)更平滑,而且终点更高——风险平价不仅更稳,还赚得更多。
再看指标表里的数字。波动率从 11.73% 降到 10.81%,最大回撤从 -16.79% 收窄到 -12.26%——最惨的时候少跌了 4.5 个百分点。收益率反而更高了(80.75% → 99.67%)——波动小了,收益没丢,甚至更好。这不是巧合:波动小意味着你更容易在低点不慌,不会在大跌时恐慌卖出,反而能享受到后面的反弹。
最后看图 3-3 的权重历史。你会发现一个有意思的现象:三只 ETF 的权重不是固定的,而是随时间在动态调整。某段时间黄金ETF 波动率飙升,它分到的钱就自动减少;某段时间沪深300ETF 变得平稳了,它分到的钱就自动增加。这就是节首讲的“按波动率倒数分配”在跑——市场自己在告诉你谁该多买谁该少买,你不用做任何主观判断。
风险平价比等权更稳了。但你有没有注意到,它只看波动率,完全不看涨跌。如果一只 ETF 正在猛涨,风险平价不会多买它;如果一只正在跌,也不会少买它。能不能更贪心一点——让涨得好的多买点?
---
## 3.3 涨得好的多买点,跌的别买
风险平价让组合更稳了,但回头想想,它的判断标准只有一个——波动率。波动大的少买,波动小的多买,仅此而已。至于一只 ETF 是在涨还是在跌?它完全不关心。黄金如果正在猛涨,风险平价不会因此多买一分钱;沪深300如果正在暴跌,它也不会少买一分钱。它只看颠簸程度,不看方向。
那能不能更激进一点:涨得好的多买,跌的干脆不买?
还记得第二章我们提过几种交易方式吗?其中一种叫趋势交易——跟着涨势走。动量排名就是趋势交易思想的量化表达:过去一段时间涨得猛的,我认为它还会继续涨,多买;跌的,我认为它还会继续跌,不买。简单粗暴,但逻辑清晰。
### 动手实验 3:动量排名组合回测
我们一起把这份 spec 写出来。这次重点看两件新东西:**多指标依赖链(mom + vol → ram**、**排序型 spec 的过滤后归一化**。
#### 起草上下文 + 任务
这是 q3 三联 spec 的最后一份,从“加权(永远全持有)”跃迁到“排序(动量为负的不买)”。上下文 + 任务一气呵成:
> **上下文**:在 `q3-how-much.ipynb` 中已有等权组合和风险平价组合的回测结果。学员已理解:等权不看任何指标,风险平价只看波动率。当前问题:风险平价更稳了,但它完全不看涨跌。能不能让涨得好的多买点?
>
> **任务**:在 notebook 中新建代码单元格,用动量排名信号运行回测,与前两种方案三方对比。
#### 起草要求
动量排名比风险平价多一层——它要算 ram = mom / vol**ram 依赖 mom 和 vol 两个指标**。oxq 的 required_indicators 支持这种链式依赖,再加一个排序型优化器收尾:
> **要求**
>
> 1. 阅读 `TopNRankingOptimizer` / `Momentum` / `Ratio` 三个 oxq 模块。
> 2. 在信号上注册三个指标(指标依赖链):
> ```python
> signal_tn.required_indicators = {
> “mom”: (Momentum(), {“column”: “close”, “period”: 20}),
> “vol”: (RollingVolatility(), {“column”: “close”, “period”: 20}),
> “ram”: (Ratio(), {“col_a”: “mom”, “col_b”: “vol”}),
> }
> ```
> 顺序很重要——ram 依赖 mom 和 vol,所以 mom 和 vol 要先注册。
> 3. 用 `TopNRankingOptimizer(score_col="ram", n=3, filter_negative=True)` 装配优化器。`n=3` 是因为候选池就 3 只 ETF,这里等于“全选 + 过滤负值”;`filter_negative=True` 是动量排名的硬规则——**正在跌的不买**。
> 4. 复用 spec-02 的 COMMON 字典,只把 portfolio 换成 `TopNRankingOptimizer(...)`。
> 📌 **要点**:多指标依赖链是 oxq 信号系统的关键能力。spec-02 注册 1 个指标(vol),spec-03 注册 3 个且 ram 依赖前两个——**spec 必须把注册顺序写清楚**,而不是只列指标名。AI 写代码时如果先注册 ram 再注册 mom/vol,运行时 ram 算不出来就直接 NaN——你看不到报错,但权重全错。
> 📌 **要点**:排序型 spec(动量排名)与加权型 spec(等权、风险平价)的本质区别是:可以**放弃部分标的**。所以“权重和=1.0”的断言要改成“未被过滤的活跃权重和=1.0”——`active_w = tn_w[tn_w > 1e-6]; assert abs(active_w.sum() - 1.0) < 1e-6`。这是排序型 spec 的活跃权重检查,加权型 spec 不需要这一步。
#### 起草结果呈现
这一段的两个着力点:用排序型断言代替原来的“权重和=1.0”硬话术,并把“动量为负时面积图出空白”这个特殊视觉点写进 spec。
> **结果呈现**
>
> 1. **权重检查**:用排序型断言(过滤后归一化)。
> 2. 三方权重对比表 + 三方指标对比表(三种信号同台 PK)。
> 3. 三条净值曲线图(等权灰虚 + 风险平价蓝实 + 动量排名橙实)。
> 4. 动量排名权重历史堆叠面积图,**注意:动量为负时权重为 0,面积会出现空白**——把这个视觉特征预先写进 spec。
> 5. 分析话术按实际数据动态描述方向,**不要硬写“更大”或“更深”**——市场不可预测,spec 不应假设结果方向。
> 📌 **要点**:可视化 spec 不仅要描述要画什么,还要把**特殊视觉点**预先点出——动量为负时面积图会出空白,这种“反常”的视觉如果 spec 不预告,AI 可能用 `fillna(0.33)` 把空白补上,反而抹掉了排序型策略最重要的视觉信号。这是仓位分配章的高级 spec 写法:**视觉契约不只规定颜色形状,还要规定“该出现的空白也要出现”**。
完整 spec 在 [`specs/spec-03-momentum-ranking.md`](https://github.com/xingwudao/xquant-learning/blob/main/q3-how-much/specs/spec-03-momentum-ranking.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了新的权重对比、一张三方指标表、两张图。这个实验引入了两个新概念。
**动量(Momentum** ——过去 N 天的涨跌趋势。正数 = 涨势,负数 = 跌势。就像看一辆车的速度表——动量为正说明车在加速前进,为负说明在倒退。这里用的是 20 日动量,也就是看过去一个月的涨跌方向。
但单看动量有个问题:波动大的标的天然动量值更大——它涨 5% 和波动小的标的涨 2%,可能只是因为它“脾气大”,不一定代表趋势更强。所以我们把动量除以波动率,得到**风险调整动量(ram = mom / vol)** ——每份波动带来的动量。这样不同脾气的标的之间才能公平比较。
**TopNRankingOptimizer** ——按 ram 值排名分配权重。ram 为负的标的直接权重设为 0——不买。剩下的按 ram 大小归一化分配权重。涨得越猛且波动越小,分到的钱越多。如果三只 ETF 里有两只 ram 为负,那所有的钱就全部集中到唯一 ram 为正的那只上。
### 实验结果
三种信号的权重对比(最新一天)如表 3-5 所示。
**表 3-5 三种信号最新一天权重对比**
| 标的 | 等权 | 风险平价 | 动量排名 |
|------|------|---------|---------|
| 沪深300ETF | 33.3% | 45.5% | 0.0% |
| 纳指100ETF | 33.3% | 43.3% | 0.0% |
| 黄金ETF | 33.3% | 11.2% | 100.0% |
差异一目了然。等权三等分,风险平价按波动调整,而动量排名直接把沪深300和纳指100的权重归零——近期它们在跌,不买。所有的钱全部涌向唯一在涨的黄金ETF。
三种信号的指标对比如表 3-6 所示。
**表 3-6 三种信号指标对比**
| 指标 | 等权 | 风险平价 | 动量排名 |
|------|------|---------|---------|
| 累计收益率 | 80.75% | 99.67% | 179.96% |
| 年化波动率 | 11.73% | 10.81% | 15.54% |
| 最大回撤 | -16.79% | -12.26% | -15.77% |
三条净值曲线同框对比如图 3-4 所示,动量排名的权重历史如图 3-5 所示。
![三种信号对比:等权 vs 风险平价 vs 动量排名](images/q3-how-much/04-three-signals-nav.png)
![动量排名:涨的多买,跌的不买](images/q3-how-much/05-momentum-weight-history.png)
### 读懂这些结果
先看图 3-4 的净值曲线,三条线同框对比。橙色实线(动量排名)终点最高——累计收益 179.96%,远超风险平价的 99.67% 和等权的 80.75%。但这笔超额收益不是白来的:动量排名的波动率是 15.54%,高于风险平价的 10.81%;最大回撤 -15.77%,也深于风险平价的 -12.26%。**收益更高,但颠簸更大、回撤更深——这就是激进策略的代价。**
别急着下结论。记得第一章的教训吗?同一个策略换个时间段,结果可能完全不同。动量排名在这几年的数据上表现优异,是因为市场恰好有几段清晰的趋势——比如黄金从 2023 年开始的持续上涨。**换段时间,动量排名的判断可能跟不上市场反转的节奏——这种"数据上看着好、换个时段就出问题"的现象,第五章会专门讨论,本章先不下结论。** 没有完美的分法——风险平价更稳,动量排名更激进——哪个更适合你,要看你打算拿多久、能承受多大波动、能不能扛得住一段时间的回撤。第五章我们会用更系统的方法判断“一个策略到底好不好”。
再看图 3-5 的权重历史。对比 3.2 的风险平价权重图,动量排名的变化剧烈得多——某只 ETF 动量转负,权重直接从几十个百分点归零,所有钱涌向其他标的;等它动量转正,权重又突然拉满。你还会发现图中有些时段出现了空白——三只 ETF 的权重加起来不到 100%,甚至全部归零。这说明那段时间三只 ETF 的动量全是负的,没有一只在涨,信号的判断是“都别买”。风险平价的权重变化则平缓得多,三只 ETF 始终都持有,只是比例在缓慢调整。两种信号的性格完全不同:一个求稳,一个求进。风险平价像一个谨慎的管家,均匀分配、避免偏科;动量排名像一个激进的赌徒,押注赢家、果断弃子。
三种信号,三种结果。到这里,你可能已经有了一个模糊的感觉:刚才做的事情好像不简单——选信号、跑回测、比指标,这不就是在做量化研究吗?
回头看:前言里说量化第一步是“**做**”——把动作写成可重复执行的规则。三次实验里,你已经做了三次:等权、风险平价、动量排名,每一次都是把“怎么分钱”从凭感觉变成了一段 spec 能让 AI 重跑的规则。这就是“做”。具体每一种规则做了什么?我们回头看看。
---
## 3.4 回头看:你刚才做了什么?
三次实验,你只换了一样东西——信号。数据、标的、回测引擎、再平衡频率全部一样,只有信号不同,结果就完全不同。
你刚才做的事情,在量化交易的框架里有一个正式的名字:**信号对比(Signal Comparison)** ——保持其他条件不变,只切换信号,观察结果的差异。这是量化研究中最基础、也最重要的操作之一。信号只管判断,不管执行——但判断之后,谁来动手?这是规则(Rules)的事,第四章会讲。
我们来看看你现在走到了哪里:第二章解决了“买什么”(Universe),第三章解决了“买多少”(Signal),第四章将解决“什么时候”(Rules)。
第一章里的 Crossover 信号说的是“买还是不买”——价格上穿均线就买、下穿就卖,要么买要么不买。第三章的三种信号说的是“每只买百分之多少”——从 0% 到 100%,可以精确到小数点。判断的粒度更细了,但本质没变:都是信号,都是观察市场之后做出的判断。三种信号、三种性格,对照如表 3-7 所示。
**表 3-7 三种信号的性格对照**
| 信号 | 看什么 | 性格 |
|------|--------|------|
| EqualWeightOptimizer | 什么都不看 | 佛系 |
| RiskParityOptimizer | 波动率 | 稳重 |
| TopNRankingOptimizer | 涨跌趋势 | 激进 |
“性价比”这个问题——三种信号收益和波动都不同,能不能用一个数字综合衡量?这是下一节要回答的事。
---
## 3.5 夏普比率:一个数字概括“性价比”
三种信号的收益率和波动率都不同。等权收益最低,风险平价更稳且收益更高,动量排名收益最高但波动也最大。有没有一个数字能把收益和波动综合起来,告诉你“每承受一份颠簸,能赚多少收益”?
有。**夏普比率(Sharpe Ratio)** ——年化收益率除以年化波动率,衡量的是“性价比”。就像买东西看“每块钱能买到多少克”,夏普比率看的是“每承受一份颠簸能赚到多少收益”。数字越大,说明你承受的颠簸越值得。三种信号的夏普比率对比如表 3-8 所示。
**表 3-8 三种信号的夏普比率**
| 指标 | 等权 | 风险平价 | 动量排名 |
|------|------|---------|---------|
| 年化收益率 | 11.96% | 13.97% | 20.80% |
| 年化波动率 | 11.73% | 10.81% | 15.54% |
| 夏普比率 | 1.08 | 1.35 | 1.42 |
看最后一行。三种方案的夏普比率依次递增:等权 1.08,风险平价 1.35,动量排名 1.42。风险平价比等权高出不少,说明按波动调整确实提升了性价比。动量排名最高,但优势不如收益率差距那么大——因为它的波动率也更高,部分抵消了收益优势。不过,夏普比率有三个你需要知道的局限:
1. 它把上涨和下跌的波动一视同仁——但你可能根本不介意往上的颠簸。
2. 同一个策略换个时间段,夏普比率可能完全不同——它只反映特定历史区间的表现。
3. 当波动率为 0 时它会失效——持有现金的夏普比率是无穷大,但显然不是好策略。
结论:夏普比率适合在相同条件下横向对比几种方案的性价比,但别把决策押在这一个数字上。怎么综合多个指标来判断一个策略到底好不好?这是第五章的主题。
---
## 3.6 本章总结
三次实验,三种信号,从“各买三分之一”到“按波动调整”再到“按涨势分配”——你一步步把“买多少”这个问题从拍脑袋变成了有据可依。信号就是策略的大脑,换一个大脑,整个策略的表现就完全不同。
还有一个细节值得带走:本章三种信号下回测都设定了每 10 个交易日重算权重——这叫**再平衡(Rebalancing)**,又名**调仓**。“多久重算一次 / 多久调一次”是另一个独立的设计选择,第四章会专门讲。
### 概念速查表
本章涉及的核心概念汇总如表 3-9 所示,方便随时回查。
**表 3-9 第三章核心概念速查**
| 概念 | 含义 | 类比 |
|------|------|------|
| 权重(Weight) | 每只标的分到总资金的百分之多少 | 一笔钱怎么分 |
| 信号(Signal) | 根据指标对市场做出的判断——要不要动、怎么动。只表达意愿,不负责执行 | 你看完天气预报后的判断:“带伞”。带不带是另一回事 |
| 风险平价(Risk Parity) | 按波动率倒数分配权重,让每只影响力均匀 | 成熟稳重的多管钱,情绪化的少管钱 |
| 动量(Momentum) | 过去 N 天的涨跌趋势,正=涨势,负=跌势 | 最近的势头 |
| 动量排名(TopNRankingOptimizer | 按涨势强弱分配权重,跌的不买 | 趋势交易的量化表达 |
| 夏普比率(Sharpe Ratio) | 年化收益 / 年化波动,衡量性价比 | 每份颠簸赚多少 |
| 再平衡/调仓(Rebalancing) | 定期重新计算权重并调整仓位 | 定期重新分工 |
### 策略进化路径
第二章产出了投资宇宙(沪深300、纳指100、黄金)。本章在此基础上探索了三种分钱方式:Step 1 各买 1/3EqualWeightOptimizer)——简单,但波动大的把组合拉着走;Step 2 按波动调整(RiskParityOptimizer)——更稳,每只影响力均匀;Step 3 按涨势调整(TopNRankingOptimizer)——趋势交易的量化表达。第三章产出:三种信号对比 + 信号就是策略的大脑。
### 本章最重要的收获
走完三个实验后,本章最值得带走的五条认知如表 3-10 所示。
**表 3-10 第三章主线认知**
| 认知 | 来源 |
|------|------|
| 平均分钱 ≠ 平均分影响力 | 3.1 |
| 波动率小 = 你能在低点不慌 | 3.1 |
| 按波动调整让组合更稳 | 3.2 |
| 同样的策略,只换信号,结果完全不同 | 3.3 |
| 夏普比率适合对比,但别只看一个指标 | 3.4 |
### 带走的问题
做完本章,你应该带着两个问题进入后续章节——它们将在下两章被一一回答:
- 信号只负责判断“想怎么分”,但**判断完了谁来执行?按什么规则下单?** → 第四章
- 动量排名在这段数据上全面胜出,但**换一段时间还行吗?怎么区分真本事和运气?** → 第五章
第二个问题正是本书口号“**做要规则、看要指标、还要怀疑你的指标**”中“疑”的入口——动量排名的累计 179.96% 看起来很漂亮,但你已经隐隐感到不对劲:换段时间还能不能这样?这种警觉,是后面五章要反复训练的主线动作。
> 本章所有代码的可运行版本见 `notebooks/q3-how-much.ipynb`
+446
View File
@@ -0,0 +1,446 @@
# 第四章:给策略加规则:调仓、止损、止盈
第三章我们用三种信号解决了“买多少”——等权、风险平价、动量排名,各有各的性格。但有个细节你可能没注意:不管用哪种信号,策略都是每 10 个交易日自动调仓一次——重新把持仓比例调回目标权重。为什么是 10 天?5 天行不行?一个月呢?
更关键的问题是:**如果在两次调仓之间,某只 ETF 突然暴跌 20%,你只能干等到下一个调仓日才能反应。** 这合理吗?
> **🧭 本章边界**:章名虽然叫“什么时候买卖”,但本章**不做入场择时**——什么时候第一次进场、用什么信号判断“该买了”,那些是第三章信号已经回答的事。本章聚焦的是**已经持仓之后**的三个执行节奏:多久做一次再平衡(4.1)、跌到什么程度先撤(4.2)、涨到什么程度走人(4.3)。
>
> **🧱 一个新元概念:规则(Rule)。** 第三章信号说的是“想怎么分钱”——是策略的大脑。本章引入的是**规则**——策略的手脚:感知持仓状态(涨了多少、跌了多少、上次调仓多久了),按预设条件触发动作。信号管“想怎么动”,规则管“什么时候动手”。两者分工明确,缺一不可。从本章起,你会反复看到 `RebalanceFrequencyRule` / `StopLossRule` / `TakeProfitRule` 这些 Rule 类——它们就是规则的具体实现。
### 路线图
我们正在走一条完整的策略构建之路:
**选什么标的(第二章 已完成)→ 每个买多少(第三章 已完成)→ 什么时候买卖(第四章 本章)→ 怎么验证有效(第五章)**
**这一章你仍在飞轮的“组合层”,主练“做”——把“什么时候动手”也写成可重复执行的规则。** 本章解决“什么时候买卖”,分三步揭示三个认知,最后你会发现:不是所有“听起来合理”的规则都有效。三节的问题与方法对照如表 4-1 所示。
**表 4-1 第四章三节问题与方法对照**
| 节 | 问题 | 方法 |
|----|------|------|
| 4.1 | 调仓频率怎么选? | 周/月/季度对比 + 引入交易成本 |
| 4.2 | 亏了要不要先跑? | 止损规则 |
| 4.3 | 赚了要不要走? | 止盈规则 |
本章使用第三章的风险平价(RiskParity)作为基础信号——它始终持有全部 3 只 ETF,方便我们观察不同规则的效果。
(操作流程见前言“怎么使用这本书”。每一节按相同流程:阅读 → 复制 spec 给 AI → 看结果与解读。)
---
## 4.1 调仓频率有多重要?
第三章每 10 个交易日调一次仓——这个“10”是拍脑袋定的。调得更勤(每周)会更好吗?调得更懒(每季度)会更差吗?
直觉告诉我们:调得越勤,反应越快,效果应该越好。真的吗?
### 动手实验 1:不同调仓频率对比
本节用到两个新模块:`RebalanceFrequencyRule`(设定多少天调仓一次)和 `PercentageFee`(模拟交易手续费)。
我们一起把这份 spec 写出来。这次重点看两件新东西:**参数化扫描怎么列**,以及**辅助函数封装作为后续 spec 的接口**。
#### 起草上下文 + 任务
> **上下文**:本章探讨“什么时候买卖”。第三章用了三种信号(等权、风险平价、动量排名)。本章选择风险平价(RiskParity)作为基础信号——它始终持有全部 3 只 ETF,方便我们观察止损和止盈规则的效果。第三章每 10 个交易日调一次仓,但为什么是 10 天?调得更勤或更懒,结果会怎样?
>
> **任务**:在 notebook `q4-when-to-trade.ipynb` 中创建代码,对比不同调仓频率的效果,并首次引入交易成本。
#### 起草要求:参数化扫描 + 辅助函数封装
要求段是这份 spec 的灵魂——把“对比 4 种频率”和“先后跑两轮成本”两件事拧成一份 spec。
> **要求**
>
> 1. 先读 oxq 源码:`PercentageFee` / `SimBroker` / `RebalanceFrequencyRule` / `RiskParityOptimizer` / `Engine` / `Strategy`
> 2. 复用第三章的 SYMBOLS、START、RiskParity 信号等基础配置
> 3. 定义辅助函数 `run_backtest(frequency, fee_model=None, order_rules=None)` 封装策略构建和回测执行,**供后续 spec 复用**
> 4. 定义 4 种调仓频率:`FREQUENCIES = [5, 10, 21, 63]`(约每周 / 两周 / 每月 / 每季度)
> 5. 第一轮回测**不含成本**`fee_model=None`)→ 第二轮回测**含成本**`PercentageFee(rate=Decimal("0.001"), min_fee=Decimal("5"))`
> 6. 含成本版本画净值曲线对比图(figsize 12x6,4 条线,归一化到起点 = 100)
> 7. 分析按实际数据动态描述(不要硬写“更高”或“更低”)
> 📌 **要点**:参数化扫描的关键是把“被对比的维度”列成一个数组(`FREQUENCIES = [5, 10, 21, 63]`+ 一个 for 循环跑完。spec 里把数组写出来,AI 就不用猜要对比哪些值。后面 spec-02 / spec-03 也会沿用同样的写法——`STOP_THRESHOLDS = [...]`、`TP_THRESHOLDS = [...]`。这是研究型工作中最常见的 spec 套路。
> 📌 **要点**:辅助函数 `run_backtest` 是为 spec-02 / spec-03 留的接口。整章三份 spec 都通过它跑回测,spec-02 把止损规则塞进 `order_rules` 参数,spec-03 再把止盈塞进去。**spec 链有共享逻辑时,把它封装成函数放在源头 spec,后续 spec 直接调用——这是接续型 spec 链的核心架构。**
#### 起草结果呈现
> **结果呈现**
>
> 1. 无成本对比表(4 行 6 列)
> 2. 含成本对比表(4 行 7 列,多一列“总手续费”)
> 3. 含成本净值曲线对比图
> 4. 分析文字(按实际数据描述方向)
完整 spec 在 [`specs/spec-01-rebalance-frequency.md`](https://github.com/xingwudao/xquant-learning/blob/main/q4-when-to-trade/specs/spec-01-rebalance-frequency.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了两张对比表和一张净值图。我们来解读结果。
四种频率分别对应每周(5 个交易日)、每两周(10)、每月(21)、每季度(63)。代码跑了两轮回测:第一轮不含成本,第二轮含成本(万分之一佣金,最低 5 元)。同样的策略,唯一的差别是有没有扣手续费。
**交易成本(Transaction Cost** ——每次买卖都要付的费用,主要是付给券商的**佣金**和(部分市场)付给政府的**印花税**。本章回测里只模拟了佣金部分(万分之一,最低 5 元),没有加印花税——实际交易的成本会比回测里更高。记住这一点:**回测里的成本是乐观估计,现实只会更贵。** A 股 / 美股 / 港股具体费率有差别,详情见章末「拓展阅读」。
在之前的第二章和第三章里,我们的回测都没有考虑交易成本,就像在一个“免手续费”的理想世界里做实验。从现在开始,我们把手续费加回来,看看现实世界会怎样。
### 实验结果
不含成本时四种频率的对比如表 4-2 所示。
**表 4-2 不含交易成本时四种调仓频率的对比**
| 频率 | 累计收益率 | 年化波动率 | 最大回撤 | 夏普比率 | 交易次数 |
|------|-----------|-----------|---------|---------|---------|
| 5 | 101.22% | 10.90% | -12.32% | 1.34 | 744 |
| 10 | 106.80% | 11.19% | -12.44% | 1.36 | 372 |
| 21 | 95.22% | 11.35% | -14.33% | 1.24 | 177 |
| 63 | 105.56% | 11.85% | -14.79% | 1.28 | 57 |
加上手续费后的对比如表 4-3 所示。
**表 4-3 含交易成本(万分之一佣金)时四种调仓频率的对比**
| 频率 | 累计收益率 | 年化波动率 | 最大回撤 | 夏普比率 | 交易次数 | 总手续费 |
|------|-----------|-----------|---------|---------|---------|---------|
| 5 | 93.36% | 10.91% | -12.97% | 1.27 | 744 | 4,665 元 |
| 10 | 101.68% | 11.19% | -12.81% | 1.31 | 372 | 3,022 元 |
| 21 | 91.93% | 11.35% | -14.50% | 1.21 | 177 | 2,052 元 |
| 63 | 104.09% | 11.86% | -14.85% | 1.27 | 57 | 926 元 |
含成本下的净值曲线对比如图 4-1 所示。
![调仓频率对比(含交易成本)](images/q4-when-to-trade/01-frequency-comparison.png)
### 读懂这些结果
先看表 4-2。四种频率的夏普比率差异不大,从 1.24 到 1.36——不含成本时,每 10 天调仓的夏普比率最高(1.36)。
再看表 4-3。加上手续费后,每 10 天调仓的夏普比率仍然最高(1.31),排名没变。但有一组数字值得关注——最右边两列。交易次数从频率 5 的 744 笔骤降到频率 63 的 57 笔,相差 13 倍;总手续费从 4,665 元降到 926 元,相差 5 倍多。**频率越高,交易越多,成本越大——这是铁律。**
对比表 4-2 与表 4-3 中频率 5 的行:不含成本时累计收益 101.22%,含成本时跌到了 93.36%,手续费吃掉了将近 8 个百分点的收益。而频率 63 呢?从 105.56% 只降到 104.09%,手续费只吃掉了 1.5 个百分点——因为它只交易了 57 笔。
最后看净值图。四条线走势相似,终点差异不算大——频率的选择有影响,但不是决定性的。更重要的发现是:**交易不是免费的**。每次调仓——不管是买还是卖——都要付佣金、交税(根据政策)。在不含成本的“理想世界”里看起来差不多的策略,加上成本后差距就拉开了。
**数据说了算:频率的选择是个权衡——反应速度 vs 交易成本。**
但频率不是最大的问题。想象一下:你刚调完仓,第二天某只 ETF 暴跌 15%。下一次调仓要等好几天——这段时间你只能眼睁睁看着亏损扩大。能不能加一个“紧急出口”——不管调仓日到没到,亏到一定程度就先跑?
---
## 4.2 亏了能不能先跑?
定期调仓的硬伤很明显:两次调仓之间如果市场暴跌,你只能干等。
直觉告诉我们:亏了先跑(卖出止损),总比硬扛着不动好。但“跑”也不是免费的——每次卖出再买回来,都要付手续费。到底值不值?先猜后验。
### 动手实验 2:止损规则对比
我们一起把这份 spec 写出来。这次重点看两件新东西:**累积接续怎么把上一份 spec 的最优结果流到这一份**,以及**为什么这件事必须 Rule 才能做**。
#### 起草上下文 + 任务
> **上下文**:在 `q4-when-to-trade.ipynb` 中已有 RiskParity 策略在不同调仓频率下的回测结果,学员已看到交易成本的影响,理解了“交易不是免费的”。当前问题:定期调仓有个硬伤——中间暴跌只能干等。能不能加个保护?
>
> **任务**:在 notebook 中新建代码单元格,给策略加上止损规则,对比不同止损阈值的效果。
#### 起草要求:累积接续 + Signal vs Rule
止损是 Rule 而不是 Signal——这是 oxq 框架的关键设计,spec 要把这件事掰开讲。
> **要求**
>
> 1. 先读 oxq 源码:`StopLossRule`(注意它作为 rule 传入 `Engine.run(rules=[...])`,不再是 Strategy 字段)
> 2. 解释 StopLossRule 工作原理:以 `avg_cost x (1 - threshold)` 为止损价提交 stop SELL 挂单 → SimBroker 后续每个 bar 检查是否触发。**为什么 Signal 做不到**:Signal 只看市场数据,不知道你的买入价是多少;只有 Rule 能感知持仓状态
> 3. **用代码动态选**含成本后夏普最高的频率作为基准(不要硬编码):`BEST_FREQ = max(FREQUENCIES, key=lambda f: results_with_fee[f].sharpe_ratio())`
> 4. 定义 4 组对比:无止损 / threshold=0.05 / 0.10 / 0.15
> 5. 调用 spec-01 的 `run_backtest`,把止损规则作为 `order_rules=[StopLossRule(threshold=T)]` 传入
> 6. 净值曲线对比图(无止损灰色虚线 vs 3 种阈值彩色实线),并在无止损曲线上用浅红色阴影标注最大回撤时段
> 7. 分析按实际数据动态描述,要点出阈值太紧(5%)和太松(15%)的各自问题
> 📌 **要点**:累积接续的关键是**用代码现场算出最优值**而不是硬编码上一份 spec 的结果。`BEST_FREQ = max(FREQUENCIES, key=...)` 这一行让 spec-02 永远跟着 spec-01 的真实数据走——上一份跑出来什么频率最优,这一份就用什么。如果硬编码 `BEST_FREQ = 10`,数据更新后排名变了,基准就错了。**接续型 spec 之间应该用变量传结果,不应该用数字传结果。** **对你的练习**:以后你写第二份 spec 复用第一份的最优值时,宁可写一行 `BEST = max(...)`,也别硬编码具体数字——这样你的 spec 链就能跟着数据自动跑。
> 📌 **要点**:信号(Signal)和规则(Rule)分工不同——这是 oxq 框架的认知基石,后面 q5 / q7 调度多种 Rule 都靠它。spec 要求第 2 条把这层区分写进去,目的是让 AI 在 notebook 里也输出这段解释,给学员第二次曝光。**核心概念应该跨 spec / notebook / book 三处复述,不能只指望 book markdown 单方面承担。** **对你的练习**:以后你 spec 里出现一个不熟的核心概念,要求 AI 在生成的 notebook 里也用一两行打印解释一下——这样你跑代码的时候,notebook 自己就成了你的第二份“小词典”。
#### 起草结果呈现
> **结果呈现**
>
> 1. StopLossRule 工作原理说明(挂单机制 + Signal vs Rule 的区别)
> 2. 指标对比表(4 行 7 列)
> 3. 净值曲线对比图(含回撤阴影标注)
> 4. 分析文字
完整 spec 在 [`specs/spec-02-stop-loss.md`](https://github.com/xingwudao/xquant-learning/blob/main/q4-when-to-trade/specs/spec-02-stop-loss.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了止损规则的工作原理说明、一张对比表和一张带回撤标注的净值图。我们来解读结果。
本节引入了两个新概念。
**止损(Stop Loss** ——预设一个底线:如果从买入价跌了 X%,自动卖出。就像你去赌场之前跟自己说“今晚最多输 500 块,输到了就走人”——止损就是这个“输到了就走”的自动化版本。“活着才能赚钱”——保住本金,才有机会等到下一次上涨。
**挂单(Pending Order** ——不是立即交易,而是“预设条件,满足了再执行”。止损规则在你持有某只 ETF 时,自动向券商挂一个条件单:“如果跌到某个价格以下,就帮我卖掉。”你不用每天盯盘,系统帮你盯着。
代码从 Step 1 的结果中自动选出含成本后夏普比率最高的频率(10 天),然后在这个基准上叠加止损规则。四组对比:无止损、5% 止损、10% 止损、15% 止损。
这里有一个关键的新概念。**规则(Rule)** ——策略的“执行层”。第三章的信号(Signal)负责判断“想怎么分”,但信号不知道你的持仓情况——它只看市场数据(波动率、动量),不知道你什么时候买的、买了多少钱、现在是赚还是亏。规则不同——规则能感知持仓状态:知道“我什么时候买的、现在亏了多少”。止损就是这种“看持仓”的判断,只有 Rule 能做。
### 实验结果
止损规则的对比结果如表 4-4 所示(基准频率:每 10 天,含交易成本)。
**表 4-4 止损阈值对比(含交易成本)**
| 止损阈值 | 累计收益率 | 年化波动率 | 最大回撤 | 夏普比率 | 交易次数 | 总手续费 |
|---------|-----------|-----------|---------|---------|---------|---------|
| 无止损 | 101.68% | 11.19% | -12.81% | 1.31 | 372 | 3,022 元 |
| 5% | 100.78% | 10.71% | -13.25% | 1.36 | 389 | 3,903 元 |
| 10% | 100.32% | 11.02% | -13.08% | 1.32 | 378 | 3,270 元 |
| 15% | 95.45% | 11.14% | -14.99% | 1.26 | 374 | 3,071 元 |
净值曲线对比与最大回撤标注如图 4-2 所示。
![止损规则对比(含交易成本)](images/q4-when-to-trade/02-stop-loss-comparison.png)
### 读懂这些结果
先看图 4-2 的大趋势。三条彩色实线(有止损)和灰色虚线(无止损)走势接近——有止损的线在浅红色阴影区域(最大回撤时段)并没有比无止损的线跌得更浅。这是个值得停下来想一想的反直觉信号。
再看表 4-4。三种止损阈值的最大回撤都比无止损更深一点:5% 止损 -13.25%vs 无止损 -12.81%)、10% 止损 -13.08%、15% 止损 -14.99%——阈值设得越紧,回撤反而没收窄。但夏普比率方向不一样:5% 止损夏普 1.36 反而比无止损的 1.31 高一点,因为它把年化波动率从 11.19% 压到了 10.71%——分母(波动率)下降抵消了分子(收益略降)的影响。
为什么三种止损都没把回撤压住?这不是 bug,而是止损在**持续下跌的市场**中的副作用——**锯齿效应**。
2022 年市场一路缓慢下跌,不是一天暴跌 10%,而是今天跌一点、明天跌一点,连着跌好几个月。5% 止损在这种环境下反复触发,形成一个循环:持仓 —— 跌 5% 触发止损卖出(亏损 + 手续费)—— 调仓日买回来(手续费)—— 继续跌 5% 又触发止损(亏损 + 手续费)—— 调仓日又买回来......
每一轮“卖-买”付两笔手续费,但并没有真正躲开下跌——因为买回来之后市场继续跌。止损只是让你“分批亏”而非“一次亏到底”,额外的交易成本累积起来,反而让总回撤略微加深。从交易次数也能看出端倪:5% 止损多了 17 笔交易,多花了 881 元手续费——这些都是锯齿效应的代价。
**止损对一次性大跌有效(跌到位就跑),对持续缓慢下跌有害(反复卖出又买回来)。在 2021–2025 这段持续震荡下跌的数据里,三种阈值都被锯齿效应反复收割——这不代表止损永远无效,而是说在这个市场环境 + 这套基准上,紧的止损没占到便宜。**
**数据说了算:止损“保护本金”的逻辑没错——夏普比率确实因为波动率下降而抬了一点;但“保护”在这段数据上的含义是“压低波动”而不是“压低回撤”。阈值怎么选、保护的是什么,要看数据怎么说。**
止损解决的是亏损方向。涨的方向呢——下一节继续看。
---
## 4.3 赚了要不要走?
亏的方向有止损兜底了。涨的方向呢——市场给你 10%,你拿着不动,要不要主动伸手去摘?万一涨上去又跌回来,岂不是白赚一场?
直觉告诉我们:赚了就走(卖出止盈),落袋为安,总比看着利润蒸发好。真的吗?先猜后验。
### 动手实验 3:止盈规则对比
我们一起把这份 spec 写出来。这次 spec 的结构和前两份**完全一样**——一样的对比表、一样的净值图、一样的辅助函数。我们要演示的是:**同样的 spec 模板,不同的规则,结果可能截然不同**——结果是什么方向,跑完再揭晓。
#### 起草上下文 + 任务
> **上下文**:在 `q4-when-to-trade.ipynb` 中已有不同调仓频率的对比结果、止损规则的效果(保护了本金,但有成本)。当前问题:止损帮你控制了亏损。反过来——赚了一定程度,要不要主动卖掉、落袋为安?
>
> **任务**:在 notebook 中新建代码单元格,给策略加上止盈规则,对比不同止盈阈值的效果。
#### 起草要求:累积接续到第三层 + 微观证据
这份 spec 在 spec-01(最优频率)+ spec-02(最优止损)的基础上,叠加第三层规则。
> **要求**
>
> 1. 先读 oxq 源码:`TakeProfitRule`(挂单机制和 `StopLossRule` 类似,只是方向相反——以 `avg_cost x (1 + threshold)` 为止盈价提交 limit SELL 挂单)
> 2. 累积接续:用代码动态选 spec-02 中夏普最高的止损阈值,与 spec-01 的最优频率叠加作为基准 → `BEST_SL = max([sl for sl in STOP_THRESHOLDS if sl is not None], key=lambda sl: results_sl[sl].sharpe_ratio())`
> 3. 定义 4 组对比:不止盈 / threshold=0.10 / 0.20 / 0.30
> 4. 调用 spec-01 的 `run_backtest`,把止损和止盈一起塞进 `order_rules=[StopLossRule(threshold=BEST_SL), TakeProfitRule(threshold=TP)]`
> 5. 净值曲线对比图(不止盈灰色虚线 vs 3 种阈值彩色实线)
> 6. **打印交易记录**(取止盈阈值最小的那组,展示前 10 笔),让学员看到 stop / limit / market 三种 order\_type 在同一回测里共存
> 7. 分析按实际数据动态描述,要点出止损 vs 止盈的对照(止损截断亏损,逻辑清晰;止盈截断利润,越紧伤害越大),并收尾“让利润奔跑,截断亏损”这句行业老话
> 📌 **要点**:累积接续到第三层——`BEST_FREQ`spec-01+ `BEST_SL`(本份现场算)+ 4 组止盈阈值。每加一层规则,都是在前一层最优配置之上加。**接续型 spec 链有两种模式:简单复用前一份的产出,以及把前 N 份的最优结果层层叠加。后者就是“累积接续”,适合“逐步给策略加规则”这类研发场景。**
> 📌 **要点**:这是一份**对照实验 spec**——前两份 spec 给学员造的预期是“加规则就会变好”,这一份要做的是把同样的对比表、同样的净值图、同样的辅助函数复用一次,只换一种规则,看数据怎么说。**写对照实验 spec,关键是不要在 spec 里预先暗示结果方向,让数据自己说话。** **对你的练习**:写一份你不知道答案的对照实验 spec 时,反复检查任务段——里面有没有“应该哪个赢”的暗示词(比如“展示 X 的优势”、“证明 Y 有效”)。把这类词都改成中性的“对比 / 评估”,数据才能给你一个干净的答案。这是量化研发里最重要的一类 spec:用数据告诉自己有些规则该不该用。
> 📌 **要点**:要求第 6 条让学员**亲眼看到** stop / limit / market 三种 order\_type 的交易记录前 10 笔。前面所有对比都是宏观指标,这 10 行把规则机制下沉到单笔交易级别。**宏观指标 spec 应该配套微观证据——光看一个总数字不够,要把机制的痕迹露出来给学员看。**
#### 起草结果呈现
> **结果呈现**
>
> 1. TakeProfitRule 工作原理说明
> 2. 指标对比表(4 行)
> 3. 净值曲线对比图
> 4. 交易记录(前 10 笔,含 order\_type 列)
> 5. 分析文字
完整 spec 在 [`specs/spec-03-take-profit.md`](https://github.com/xingwudao/xquant-learning/blob/main/q4-when-to-trade/specs/spec-03-take-profit.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了止盈规则的工作原理说明、一张对比表、一张净值图和一段交易记录。我们来解读结果。
**止盈(Take Profit** ——预设一个目标:如果从买入价涨了 X%,自动卖出锁定利润。就像你在闲鱼上卖东西,挂一个“到了这个价就成交”的价格——止盈就是给你的持仓挂一个“到了就卖”的目标价。和止损一样,也是通过挂单机制实现的:你不用盯盘,系统帮你盯着。
基准配置是 Step 1 和 Step 2 选出的最优组合:每 10 天调仓 + 5% 止损。在这个基础上,分四组测试:不止盈、10% 止盈、20% 止盈、30% 止盈。止损和止盈可以同时生效。
### 实验结果
止盈规则的对比结果如表 4-5 所示(基准:每 10 天调仓 + 5% 止损,含交易成本)。
**表 4-5 止盈阈值对比(含交易成本)**
| 止盈阈值 | 累计收益率 | 年化波动率 | 最大回撤 | 夏普比率 | 交易次数 | 总手续费 |
|---------|-----------|-----------|---------|---------|---------|---------|
| 不止盈 | 100.78% | 10.71% | -13.25% | 1.36 | 389 | 3,903 元 |
| 10% | 72.72% | 9.70% | -9.47% | 1.18 | 431 | 6,703 元 |
| 20% | 83.47% | 10.04% | -13.25% | 1.26 | 409 | 5,215 元 |
| 30% | 104.32% | 10.23% | -13.25% | 1.45 | 396 | 4,306 元 |
净值曲线对比如图 4-3 所示。
![止盈规则对比(含交易成本)](images/q4-when-to-trade/03-take-profit-comparison.png)
交易记录(10% 止盈,前 10 笔)如表 4-6 所示。
**表 4-6 10% 止盈策略的前 10 笔交易记录**
| 日期 | 标的 | 方向 | 数量 | 成交价 | 订单类型 | 手续费 |
|------|------|------|------|--------|---------|--------|
| 2021-02-01 | 沪深300ETF | BUY | 5,221 | 4.8952 | market | 25.56 |
| 2021-02-01 | 纳指100ETF | BUY | 32,274 | 0.8620 | market | 27.82 |
| 2021-02-01 | 黄金ETF | BUY | 12,204 | 3.8200 | market | 46.62 |
| 2021-02-22 | 沪深300ETF | BUY | 164 | 5.0562 | market | 5.00 |
| 2021-02-22 | 纳指100ETF | BUY | 32 | 0.8974 | market | 5.00 |
| 2021-02-22 | 黄金ETF | SELL | 262 | 3.6750 | market | 5.00 |
| 2021-02-26 | 黄金ETF | SELL | 11,942 | 3.6130 | market | 43.15 |
| 2021-03-08 | 沪深300ETF | SELL | 115 | 4.5957 | market | 5.00 |
| 2021-03-08 | 纳指100ETF | SELL | 241 | 0.8378 | market | 5.00 |
| 2021-03-08 | 黄金ETF | BUY | 12,453 | 3.5190 | market | 43.82 |
### 读懂这些结果
这个结果可能出乎你的意料——**多数止盈阈值降低了收益率,但 30% 止盈反而把收益和夏普都抬了上去。**
先看表 4-5。不止盈时累计收益 100.78%,加了 10% 止盈后骤降到 72.72%,少赚了 28 个百分点;20% 止盈降到 83.47%,也是少赚。但 30% 止盈给了一个反弹——累计收益 104.32%,比不止盈还高 3.5 个百分点;夏普比率从 1.36 升到 1.45,也是四组里最高的。“落袋为安”在不同阈值上给出了完全不同的结论。
为什么 10% / 20% 止盈都在拖后腿?看看我们用的是什么信号。风险平价组合里有三只 ETF,各自承担相同的风险。当某只 ETF 表现好、价格上涨时,它正在为你赚钱。10% / 20% 这种紧的止盈恰好在这个时候把它卖掉——**你亲手切断了正在帮你赚钱的持仓。** 阈值越紧,“切断”得越早,伤害越大。
那 30% 止盈为什么反而抬了一手?阈值放到 30% 之后,止盈极少被触发——表 4-5 里 30% 止盈的交易次数 396 笔,只比不止盈多 7 笔;多花的手续费也只有 403 元。(这 7 笔含止盈触发的卖出 + 触发后下次调仓日的买回;具体止盈触发了几次,可在 notebook 的交易记录里数 `limit` 行。)但少量触发恰好抓住了几次“涨过头又掉头”的极端波动,把波动率从 10.71% 压到 10.23%——分母(波动)小了一点,分子(收益)反而稳了一点,夏普就抬上去了。**这是一个边界值的故事**:阈值太紧,把“正在赚钱的仓”切断;阈值放得够远,止盈基本不动手,少数被触发的恰好是涨过头又掉头的几次,把波动尾部削掉了一点——这就是 30% 止盈在四组里夏普反而最高的原因。
再看手续费那列。不止盈时 3,903 元,10% 止盈跳到 6,703 元——多花了 2,800 元。止盈每触发一次就是一笔额外交易——这一项就把手续费推高了 72%。
表 4-6 的交易记录也很有说明力。注意“订单类型”那列:前 10 笔全是 `market`——也就是再平衡买卖。这意味着这个基准虽然同时挂了 5% 止损 + 10% 止盈两种条件单(一份 `stop` 挂单 + 一份 `limit` 挂单),但前 10 笔里两种都还没被触发。**学员在等的“止盈触发”——`limit` 类型——在 ETF 这种宽基上很少被命中**,但每命中一次都会切断正在涨的持仓。
对比一下 Step 2 的止损:
- **止损截断亏损**——下跌中越拿越亏,跑了是对的
- **止盈截断利润**——上涨中越拿越赚,跑了通常是错的(除非阈值放得够远)
**注意:这个结论是在 2021-2025 年这段数据 + 我们用的风险平价组合上得出的。** 行业里的老话**“让利润奔跑,截断亏损”(Let profits run, cut losses short** 也是同一个方向——你不知道还会跌多少,先跑为敬;但涨到头没人提前知道,一只 ETF 涨了 10% 后可能还会涨 50%,你在 10% 就卖了,等于亲手把机会让出去。但这句话**不是定理**——换一段时间、换一类标的(比如做短线动量、抓波段反转的策略),止盈在某些场景下可能反而帮了忙。本章给你的不是“止盈一定不要加”这种死结论,而是**“加规则之前,先用数据验证它在你的策略 + 你的时段上到底是帮忙还是添乱”**这个工作方法——这才是带得走的东西。
**数据说了算:不是所有“听起来合理”的规则都有效。每加一条规则,都要用数据验证它到底是帮忙还是添乱——而且要在不同阈值上都看一遍,避免用单一参数的结果下死结论。**
---
## 4.4 回头看:你刚才做了什么?
这一章你做了三件事——给策略加调仓节奏、加止损护栏、加止盈出口。三步走下来,每一步都让你撞了一次“加规则不一定变好”的小墙。下面把三次撞墙打包成三个认知。
三步实验,三个认知升级:
1. **调仓频率**:交易不是免费的。频率越高,交易越多,成本越大。选频率就是在“反应速度”和“交易成本”之间做权衡。
2. **止损规则**:保护本金的逻辑听起来稳——跌破底线就跑。但在 2021–2025 这段持续震荡下跌的数据里,三种阈值都没把回撤压住,只把波动压低了。代价是额外交易和手续费——而且要分清楚“保护”在你的数据上到底是压回撤还是压波动。
3. **止盈规则**:逻辑不像止损那么稳。10% / 20% 阈值都降低了收益率,但 30% 阈值反而把收益和夏普都抬了上去——同一类规则,不同阈值给出反向结论。“落袋为安”听起来合理,但数据告诉我们要谨慎。
你刚才做的事情,在量化交易的框架里叫做**规则设计(Rule Design)** ——在信号的基础上,添加执行层面的保护和约束。我们来看看现在走到了哪里:
- **第二章: 买什么** —— Universe(投资宇宙)
- **第三章: 买多少** —— Signal(信号)——告诉策略每只买百分之多少
- **第四章: 什么时候** —— Rules(规则)——决定何时执行、如何保护 **← 你在这里**
本章把“规则”补进了第三章的“信号”——信号想分钱,规则才动手。两者分工明确,缺一不可。
**一个更深的认知:不是所有“合理”的规则都有效。** 止损的逻辑听起来站得住脚——截断亏损,保护本金,但在我们这段数据上“保护”的含义是压波动而不是压回撤。止盈的逻辑就更模糊——你怎么知道涨到头了?而且换个阈值结论可能反过来。加规则之前,先问两个问题:
- 这条规则的逻辑站得住脚吗?
- 换个参数、换个时段,结论还一样吗?
第二个问题,正是下一章(第五章)要回答的。
---
## 4.5 本章总结
三步实验,从“多久调一次”到“亏了跑不跑”再到“赚了走不走”——你一步步给策略加上了执行层面的规则,也第一次体验到了交易成本的威力和“规则越多越好”这个直觉的破灭。**本章主练的还是“做”——三步都把“动还是不动”从凭感觉变成了可重复执行的规则;而止盈在不同阈值上方向反过来的那一刻,正是“看要指标、还要怀疑你的指标”在飞轮组合层的第一次小预演——下一章你会正式把“看”练成主线。**
### 策略进化路径
本章接着第三章的产出,在 RiskParity 信号上逐步叠加规则——每加一条都用数据验证,而不是凭直觉。
- **第三章产出**RiskParity 信号 + 每 10 天调仓
- Step 1: 调频率 —— 频率有影响 + 交易成本不可忽视
- Step 2: 加止损 —— 压低了波动,但回撤被锯齿效应反复收割
- Step 3: 加止盈 —— 多数阈值降低收益,30% 阈值反而抬高夏普
- **第四章产出**:完整的规则配置 + “每条规则都要在不同阈值上验证”的认知
### 概念速查表
本章涉及的核心概念汇总如表 4-7 所示,方便随时回查。
**表 4-7 第四章核心概念速查**
| 概念 | 含义 | 类比 |
|------|------|------|
| 交易成本(Transaction Cost) | 每次买卖都要付的费用——佣金(付给券商)+ 税(付给政府) | 去银行换汇要收手续费 |
| 止损(Stop Loss) | 预设底线:从买入价跌了 X% 就自动卖出 | “今晚最多输 500,输到了就走” |
| 止盈(Take Profit) | 预设目标:从买入价涨了 X% 就自动卖出 | “到了这个价就成交” |
| 挂单(Pending Order) | 预设条件,满足了再执行的订单 | “到价自动触发” |
| 锯齿效应(Whipsaw) | 在持续下跌的市场里反复“卖出—回涨—又跌—又卖”,每来回一次付一笔成本 | 像锯子来回切,越切越薄 |
| 规则(Rule) | 策略的执行层——感知持仓状态,决定何时动手 | 信号是大脑,规则是手脚 |
| 让利润奔跑,截断亏损 | 止损截断亏损(通常有帮助),别用止盈截断利润 | 交易经典格言 |
### 本章最重要的收获
走完三个实验后,本章最值得带走的核心认知归纳如表 4-8 所示。
**表 4-8 第四章核心认知**
| 认知 | 来源 |
|------|------|
| 交易不是免费的——每次买卖都有成本 | 4.1 |
| 调仓频率是反应速度与交易成本的权衡 | 4.1 |
| 止损压低了波动,但回撤可能被锯齿效应反复收割 | 4.2 |
| 信号看市场,规则看持仓——两者角色不同 | 4.2 |
| 止盈结论随阈值反向——单一参数不能下死结论 | 4.3 |
| 不是所有“合理”的规则都有效,要在不同阈值上用数据验证 | 4.3 |
| 让利润奔跑,截断亏损(经验,不是定理) | 4.2 + 4.3 对比 |
| 先猜后验,数据说了算 | 贯穿全章 |
### 带走的问题
读完本章,你应该带着两个挥之不去的疑问进入下一章——它们正是第五章要回答的:
- 策略看起来不错,但**怎么知道不是恰好在这段时间表现好?** 换个时间段结果还一样吗?→ 第五章
- 回测结果漂亮就能赚钱吗?**怎么避免自欺欺人?** → 第五章
> 本章所有代码的可运行版本见 `notebooks/q4-when-to-trade.ipynb`
---
## 拓展阅读
本章正文聚焦“调仓频率 / 止损 / 止盈”三个执行规则,下面这段是动手之前可以省略的视野铺垫——读完正文后回头看,反而吸收得更快。
### A 股 / 美股 / 港股交易成本对照
不同市场的费率结构有所不同。读者按自己实际交易的市场对照即可,回测里只要把它们都算进 `PercentageFee` 模型就好。三个市场的常见费率对照如表 4-9 所示。
**表 4-9 三大市场常见交易成本对照**
| 市场 | 佣金(付给券商) | 税(付给政府) | 备注 |
|------|----------------|--------------|------|
| A 股 | 万分之一 ~ 万分之三,最低 5 元 | 卖出收千分之一印花税(买入不收) | 还有过户费(沪市)等小项目,多数券商给打包简化 |
| 美股 | 按笔(如 0.5 ~ 1 美元)或按股(每股 0.005 美元起),各家不同 | 没有印花税;卖出有 SEC 费 + FINRA TAF(按金额比例,极小) | 多数零佣金券商以 PFOF(订单流付费)或点差变相补偿 |
| 港股 | 万分之五左右,部分券商更低 | 买卖双向各 0.13% 印花税 | 还有 0.005% 证监会征费、0.0027% AFRC 征费等小项目 |
**怎么估你的实际成本?** 把“佣金 + 税 + 滑点(先按 0.1% 估)”加起来,就是你单次往返的预算。回测里把这个数填进 `PercentageFee.rate`,结果会更接近实盘。
### 为什么“让利润奔跑,截断亏损”是格言而不是定理
本章 4.3 你已经看到,10% / 20% 这种紧的止盈阈值都让累计收益下降了——多数情况下“落袋为安”输给了“让利润奔跑”;只有放到 30% 这种很少触发的阈值,才偶尔把波动压低、夏普反弹。但这并不意味着任何场景下都不该止盈。**这条格言适用的是“中长周期、跟趋势”的策略,比如本章用的风险平价 + 中频再平衡。** 如果你跑的是短线波段、做反转、或者高杠杆策略,止盈往往是必要的——不止盈就可能在一次回调里把所有浮盈吐光。换句话说:**格言是经验的提炼,不是定理;它把概率告诉你,但需要你结合策略类型 + 阈值去用。**
+735
View File
@@ -0,0 +1,735 @@
# 第五章:策略好不好?4 个评估角度
第四章我们给策略加上了规则——调仓频率、止损、止盈,学会了“不是所有合理的规则都有效”。现在手上有一个“看起来不错”的策略组合:RiskParity + 5% 止损,年化收益、夏普比都还行。
但“看起来不错”到底是什么意思?年化 15% 好不好?夏普 0.8 够不够?**跟什么比?**
更关键的问题:**这些数字是好几年平均出来的。如果某一年亏了 20%,你扛得住吗?**
### 路线图
我们正在走一条完整的策略构建之路:
**选什么标的(第二章 已完成)→ 每个买多少(第三章 已完成)→ 什么时候买卖(第四章 已完成)→ 怎么验证有效(第五章 本章)→ 如何避免自欺欺人(第六章)**
**这一章你跨进了飞轮的”归因层”——主练动作从前几章的”做”切到”看”。指标是”看”的眼睛,本书的口号”做要规则、看要指标”中”看要指标”指的就是这一层的训练。** 本章给策略做一次”全面体检”,分四步检查四个评估维度,对照如表 5-1 所示。
**表 5-1 第五章四节问题与方法对照**
| 节 | 问题 | 方法 |
|----|------|------|
| 5.1 | 跟什么比? | 基准对比 + 风险调整指标 |
| 5.2 | 每年都有收益吗? | 分年拆解 |
| 5.3 | 收益结构什么样? | 收益分布 + 水下曲线 |
| 5.4 | 参数动一动崩不崩? | 参数敏感性扫描 |
铁律不变:**先猜后验,数据说了算。**
本章用第三、四章的四个策略作为“被检查对象”,4 个实验都按统一流程跑——见前言“怎么使用这本书”。四个候选策略对照如表 5-2 所示。
**表 5-2 第五章评估的四个候选策略**
| 策略 | 来源 | 特点 |
|------|------|------|
| EqualWeight | 第三章等权 | 最简单,无参数 |
| RiskParity | 第三章风险平价 | 稳健,低敏感 |
| TopNRanking | 第三章动量排名 | 高收益但高波动 |
| RP+止损5% | 第四章最终产出 | RiskParity + 5% 止损 |
> 提示:本章 📌 要点比前几章长——因为 q5 spec 涉及金融口径、视觉契约、同方法不同目的等高密度知识,每条都把“为什么、不这样会怎样”写透。读到要点段不必赶——慢一点消化更划算。
---
## 5.1 跟什么比?
第四章的策略年化收益 15%、夏普 0.8——好不好?
直觉告诉我们:收益高就是好,夏普越大越好。但“好”是跟什么比?如果同期市场涨了 20%,你忙活半天不如躺平。
第一章已经讲过 **Beta**(跟着市场赚的钱)和 **Alpha**(你靠技能多赚的钱)。本章要做的事,就是把 第三、四章 产出的策略放到“跟谁比”的语境里——搞清楚它赚的是 Beta 还是 Alpha。一个策略只赚到 Beta,买指数 ETF 就够了;只有创造出 Alpha,策略才有真正的价值。
在开始之前,先复习四个你已经见过的“基础体检指标”——它们在第一、二、三章都已经登场过,第一次看到时回来查表 5-3 就好。
**表 5-3 四个基础体检指标速查(第一、二、三章已登场)**
| 指标 | 白话含义 |
|------|---------|
| 累计收益率 | 从头到尾总共赚了多少 |
| 年化收益率 | 平均每年赚多少 |
| 年化波动率 | 价格颠簸程度——数字越大越像坐过山车 |
| 最大回撤 | 最惨时从最高点跌了多少——衡量“最坏情况有多坏” |
这四个指标已经够看出策略“赚没赚 / 颠不颠 / 摔得惨不惨”。但它们各自看一面——你还需要一把“综合尺”把收益和风险揉到一起。这把尺第三章已经提过一次(夏普比率);本章实验跑完后我们**把它和另外两把“换种风险定义”的尺子(卡玛 / 索提诺)一起请上场**。先做实验,再上尺子。
### 动手实验 1:策略 vs 买入持有基准
我们一起把这份 spec 写出来。这次重点看三件新东西:**金融指标的年化口径必须显式**、**oxq 评估与构造模块都要白话点名**、以及**防 LLM 幻觉的标准句**。
> 先看完三段演示再回到运行结果——这一节信息会比前面密一些,因为 5.1 是 q5 信息量最大的一节。
#### 起草上下文 + 任务
第五章是“看”的一章——把前几章产出的策略放进体检台。上下文不引入新的策略,只把第三、四章的产物集合起来:
> **上下文**:本章评估策略的有效性。第三、四章产出了 EqualWeight、RiskParity、TopNRanking、RiskParity+5% 止损四个策略,回测指标看起来不错。但“不错”是跟什么比?年化 15% 好不好,取决于同期市场涨了多少。
>
> **任务**:在 notebook `q5-how-to-validate.ipynb` 中,把四个策略和“等权买入持有”基准放在一起比较,引入多种风险调整指标。
> 📌 **要点**:第五章不引入新策略,只把前几章的产物集中送进体检台。spec 的上下文段把“被检查对象”明确点名——这种“集合体检”型 spec 的好处是:学员的认知负担只在“评估方法”这一条线上,不必再消化新的策略逻辑。
#### 起草要求:模块点名 + 年化口径
要求段做两件硬约束。第一件:**白话点名 oxq 评估端和构造端要用哪几个模块**——AI 自己去查接口细节。第二件:**指标的年化方法必须显式锁定**——否则跨次/跨工具的数字对不上账。
> **要求**
>
> 1. 评估端使用 oxq 的 `RunResult`,指标的**计算口径**显式锁定:
> - 年化方法:**252 个交易日**(非 365 自然日)
> - 夏普比:`annualized_return / annualized_volatility`,无风险利率 `rf=0`
> - 卡玛比:`annualized_return / abs(max_drawdown)`
> - 索提诺比:`annualized_return / downside_volatility`(下行 = 收益 < 0 的标准差)
> - 基准的夏普比要用同一口径(252 + rf=0)
> 2. 构造端使用 oxq 的三个组合优化器——等权(`EqualWeightOptimizer`)、风险平价(`RiskParityOptimizer`)、动量排名(`TopNRankingOptimizer`),按各自的构造参数和所需指标实例化
> 3. 数据快照固定:`START = "2021-01-01"``END = "2025-12-31"`(本章数字以此为基准,不要用 `today`)
> 4. 定义辅助函数 `run_strategy(portfolio, indicators=None, freq=10, stop_loss=None)` 供后续 spec 复用
> 5. 运行四个策略 + 构造“等权买入持有”基准(每只 ETF 归一化到 1,等权平均,之后不交易)
> 📌 **要点**:金融指标的年化口径(252 vs 365、`rf=0`、下行波动定义)是**专业默契,但 spec 必须写明**。零基础学员不知道这层约定,同一个数字在不同文献里口径不同。如果 spec 不锁,你跑出 1.43,同学跑出 1.27,谁也不知道差在哪——指标 spec 的“精度”恰恰落在这些看不见的小字上。
> 📌 **要点**:**评估端 + 构造端两层接口分别点名**——要求 1 锁定评估端(`RunResult` 的指标计算),要求 2 列出构造端(三个优化器)。零基础学员只需要知道"调谁",AI 会自己去查接口。spec 把"用哪些模块"白话点清楚就够了,剩下的对照工作交给 AI。
#### 起草结果呈现:动态描述方向
最后一段最关键的不是“画几张图”,而是**怎么让 AI 在不知道结果方向的情况下写出准确的分析文字**:
> **结果呈现**
>
> 1. 策略 vs 基准对比表(5 行 × 8 列):策略名 / 累计收益 / 年化收益 / 波动率 / 最大回撤 / 夏普比 / 卡玛比 / 索提诺比;最后一行是等权买入持有基准
> 2. 超额收益(Alpha)表
> 3. 净值曲线对比图(figsize 12×6,4 实线 + 1 灰虚线,归一化到 100)
> 4. 分析文字(**根据实际数据动态描述方向,不要硬写“更高”或“更低”**):哪些策略跑赢了基准?三把尺子的冠军是不是同一个?
> 📌 **要点**:**「根据实际数据动态描述方向,不要硬写“更高”或“更低”」是 q5 整章四份 spec 的标准句**。LLM 喜欢“看似中立其实瞎写”——不锁这一句,它会编一段听上去合理但和数据对不上的结论。把这句话当成 q5 的标配,任何要 AI 写“分析文字”的 spec 都该带上。
完整 spec 在 [`specs/spec-01-benchmark-and-metrics.md`](https://github.com/xingwudao/xquant-learning/blob/main/q5-how-to-validate/specs/spec-01-benchmark-and-metrics.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了一张对比表、一张超额收益表和一张净值曲线图。
这个实验做了什么?把 第三、四章 产出的四个策略(等权、风险平价、动量排名、风险平价+止损)全部重新跑一遍含成本的回测,同时构造一个“买入持有”基准——等权买入三只 ETF 后不再做任何交易,代表“什么都不做”的结果。然后把五条净值曲线画在一起,用七个指标(累计收益、年化收益、波动率、最大回撤、夏普比、卡玛比、索提诺比)逐一比较,算出每个策略相对基准的超额收益(Alpha)。
**运行结果**
四个策略与基准的指标对比如表 5-4 所示。
**表 5-4 四个策略 vs 等权买入持有基准的指标对比**
| 策略 | 累计收益 | 年化收益 | 波动率 | 最大回撤 | 夏普比 | 卡玛比 | 索提诺比 |
|------|---------|---------|--------|---------|--------|--------|---------|
| EqualWeight | 84.37% | 12.33% | 12.49% | -16.86% | 1.05 | 0.73 | 0.92 |
| RiskParity | 98.01% | 13.77% | 11.16% | -12.81% | 1.29 | 1.08 | 1.16 |
| TopNRanking | 153.03% | 18.71% | 15.77% | -17.77% | 1.27 | 1.05 | 1.11 |
| RP+止损5% | 106.23% | 14.59% | 10.61% | -13.07% | 1.43 | 1.12 | 1.33 |
| 等权买入持有 | 94.82% | 14.39% | 13.00% | -17.24% | 1.10 | — | — |
超额收益 / Alpha(策略累计 — 基准累计 94.82%)如表 5-5 所示。
**表 5-5 四个策略相对等权买入持有基准的 Alpha**
| 策略 | Alpha |
|------|-------|
| EqualWeight | -10.45% |
| RiskParity | +3.18% |
| TopNRanking | +58.20% |
| RP+止损5% | +11.41% |
净值曲线对比如图 5-1 所示。
![策略 vs 买入持有基准](images/q5-how-to-validate/01-strategy-vs-benchmark-nav.png)
### 结果解读
第一个认知升级:**“好不好”必须有参照物。**
**基准(Benchmark** —— 用来做比较的参照物。这里的基准是“什么都不做”——等权买入三只 ETF 后不再交易。就像考试的“及格线”,不跟基准比,你根本不知道自己的策略是好还是坏。
年化 14% 听起来很棒,但等权买入持有同期也赚了 14.39%。EqualWeight 忙活了一堆操作,累计收益 84.37%,反而没跑赢“什么都不做”的 94.82%——你忙活半天不如躺平。
跑赢基准的部分叫 **超额收益(Alpha**——回忆一下 Alpha:第一章用电梯/楼梯类比讲过,Beta 是跟着市场赚的钱(被电梯抬着上楼),Alpha 是比市场多赚的钱(靠你自己爬楼梯爬出来的部分),是你的技能回报。这一章把它放回”有了基准之后才能算”的语境再用一次——上面对比表里的”基准累计 94.82%”就是那部电梯,每个策略减去基准的差额才是 Alpha。EqualWeight 的 Alpha 是 -10.45%,说明它不但没创造技能回报,反而因为频繁调仓的交易成本”亏”了。TopNRanking 的 Alpha 最高(+58.20%),但先别急着下结论——看看其他指标怎么说。
第二个发现:**不同的尺子量出不同的结论。**
实验跑完了,**正式介绍三把“综合尺”**——它们各自把收益和风险揉到一起,但对“风险”的定义不同:
- **夏普比率(Sharpe Ratio)** —— 第三章见过:年化收益除以年化波动率。把“收益高”和“颠簸大”折成一个数字。
- **卡玛比(Calmar Ratio)** —— 年化收益除以最大回撤。每承受 1% 的最大回撤,能换来多少年化收益。如果你最怕“一次跌太深”,卡玛比是最直接的尺子。
- **索提诺比(Sortino Ratio** —— 只用**下行波动率**做分母。上涨波动不是风险——涨得猛不应该被惩罚,所以比夏普比更“公平”。
简单记忆:**夏普看“颠不颠”,卡玛看“摔多惨”,索提诺只关心“亏的部分”。**
把这三把尺子放进表 5-4 之后回头看——三把尺子的冠军都是 RP+止损5%:夏普比 1.43、卡玛比 1.12、索提诺比 1.33。TopNRanking 虽然收益最高,但三把尺子都不是冠军。这次三把尺子碰巧给出了同一个冠军,但不是每次都这么巧——不同的尺子关注不同的风险维度,完全可能给出不同的答案。你最怕什么风险,就用什么尺子。
但这些都是好几年平均出来的数字。**平均年化 14% 听着不错——但如果其中一年亏了 20%,你扛得住吗?**
---
## 5.2 每年都有收益吗?
刚才的指标覆盖了 2021 年初到 2026 年 3 月,整整 5 年多、1250 个交易日。这么长时间的平均值,可能掩盖某些年份的亏损。就像考试平均 80 分,但其中一科考了 30 分。
直觉告诉我们:平均好就是好吧,哪年赚哪年亏不重要?
真的吗?拆开看看。
### 动手实验 2:逐年收益拆解
我们一起把这份 spec 写出来。这次重点看两件新东西:**接续型 spec 共享 notebook 状态**、以及**时间维度切片要避免重复回测**。
#### 起草上下文 + 任务
spec-02 不需要重新介绍策略——它直接接着 spec-01 在同一个 notebook 里跑。上下文段把“已经有什么”列清楚就够了:
> **上下文**:在 `q5-how-to-validate.ipynb` 中已有四个策略 vs 基准的整体指标对比、`results` 字典、`run_strategy` 辅助函数、`TNR_PORTFOLIO` 等变量。学员已了解基准、超额收益(Alpha)、卡玛比/索提诺比等概念。
>
> **任务**:按年度拆解策略表现,并深挖 TopNRanking 在负收益年份的异常。
> 📌 **要点**:q5 章四份 spec 共享同一个 notebook 状态——spec-02 直接读 spec-01 留下的 `results`/`run_strategy`/`TNR_PORTFOLIO`。**接续型 spec 的上下文段不必再声明,只列“已经有什么”**。这样 AI 不会重新下载数据、不会重新构建策略、不会假设一个不存在的变量。
#### 起草要求:时间切片 + 缓存复用
要求段做两件事:① 按年度切片计算指标;② 深挖 TopNRanking 在负收益年份——**这里是最容易让数字打脸的地方**:
> **要求**
>
> 1. 用 oxq `RunResult` 的日收益序列(`daily_returns()`)和净值曲线(`equity_curve`)做时间切片
> 2. 按年度拆解四策略 + 基准:用 `daily_returns()` 按年分组,计算年收益率(累乘)、年波动率、年夏普比、年内最大回撤。**跳过样本不足的年份**`if len(year_data) < 20: continue`,避开新年初只有几个交易日的边界)
> 3. 打印逐年对比表(pandas DataFrame,存入变量 `annual_df` 供后续 spec 复用)
> 4. 画逐年收益柱状图(figsize 14×6,每年一组 5 根柱子,与 spec-01 颜色一致)
> 5. 深挖 TopNRanking 负收益年份——扫描 `mom_periods = [5, 10, 15, 20, 25, 30]`**每个窗口只跑一次回测,结果存入 `deep_dive[period]`**,从字典里直接读,不要重新调用 `run_strategy` 做“是否所有窗口都亏”的二次判断
> 📌 **要点**:**时间维度切片不要重复回测**。“按年度拆”听起来要每年都跑一遍——错。整段时间只跑一次,得到日收益序列后按 `index.year` 分组算指标。深挖也一样:6 个动量窗口跑 6 次,**不要因为后面要二次判断又跑 6 次**。spec 不点名“结果存 dict 复用”,AI 写出的代码大概率会跑 12 次。
> 📌 **要点**:**最小样本过滤是隐式约定**。年内 < 20 个交易日不进表,是金融统计的默契。spec 不写明,AI 可能用 1 月份的 5 天数据计算“年化”得出离谱数字——表上突然多出一行“2026 年化 +800%”,学员看不出哪里错了。
#### 起草结果呈现
> **结果呈现**
>
> 1. 逐年收益对比表(DataFrame,行=年份,列=4 策略+基准,负收益用 `*` 标注)
> 2. 逐年收益柱状图
> 3. TopNRanking 异常年份深挖表(行=动量窗口,列=负收益年份)
> 4. 分析文字(**根据实际数据动态描述方向**):哪些策略“每年都赚”?深挖结论是“参数没选对”还是“策略结构性问题”?
完整 spec 在 [`specs/spec-02-annual-breakdown.md`](https://github.com/xingwudao/xquant-learning/blob/main/q5-how-to-validate/specs/spec-02-annual-breakdown.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了逐年对比表、柱状图和一份 TopNRanking 深挖分析。
这个实验做了什么?把 Step 1 的整体指标拆成逐年成绩单——按年份分组计算每个策略和基准的年收益率、年波动率、年内最大回撤,看看哪些策略“每年都赚”、哪些有负收益年份。然后针对 TopNRanking 在负收益年份做深挖:换遍 6 种动量窗口(5/10/15/20/25/30 天)重新跑回测,看这一年的亏损是参数选错了,还是策略本身的结构性问题。
**运行结果**
四个策略 + 基准的逐年收益拆解如表 5-6 所示(`*` 标注负收益年份)。
**表 5-6 四策略与基准逐年收益对比**
| 年份 | EqualWeight | RiskParity | TopNRanking | RP+止损5% | 基准 |
|------|------------|------------|-------------|----------|------|
| 2021 | 3.76% | 1.03% | -10.09% * | 3.04% | 4.09% |
| 2022 | -12.72% * | -8.55% * | 3.04% | -8.63% * | -13.44% * |
| 2023 | 17.04% | 15.03% | 23.48% | 15.67% | 21.21% |
| 2024 | 27.55% | 30.12% | 24.50% | 30.65% | 28.31% |
| 2025 | 31.29% | 39.01% | 62.02% | 40.74% | 32.10% |
| 2026 | 3.88% | 3.00% | 9.65% | 3.00% | 5.24% |
逐年收益的柱状对比如图 5-2 所示。
![逐年收益对比](images/q5-how-to-validate/02-annual-return-comparison.png)
深挖 TopNRanking 在 2021 年的负收益——把动量窗口换遍,结果如表 5-7 所示。
**表 5-7 TopNRanking 2021 年不同动量窗口的年收益**
| 窗口 | 2021 |
|------|------|
| period=5 | -9.48% |
| period=10 | -7.97% |
| period=15 | -5.08% |
| period=20 | -10.09% |
| period=25 | -11.09% |
| period=30 | -9.14% |
所有动量窗口在 2021 年都亏钱。
### 结果解读
**分段检验(Segment Analysis** —— 把回测结果按时间段拆开检查。整体指标是“平均分”,逐年拆开才是“每科成绩单”——整体及格不等于每科都及格。
拆开后两个重要发现:
**第一,没有哪个策略“年年赚钱”。** EqualWeight、RiskParity 和 RP+止损5% 在 2022 年都出现了负收益,同期基准也亏了 -13.44%——策略和基准一起亏,说明策略并没有真正提供“下跌保护”。这是资产配置策略的特点:它能分散风险,但不能消除市场整体下跌带来的损失。
**第二,TopNRanking 的负收益不是参数问题。** 2021 年,TopNRanking 是唯一亏钱的策略(-10.09%),而其他三个策略和基准都是正收益。换遍所有动量窗口(5 天到 30 天),在 2021 年全部亏钱。这是**动量策略的结构性弱点**。
**动量滞后性** —— 动量指标基于“过去涨了多少”来判断买什么。但当市场反复上下波动、没有明确方向时,过去涨得好的恰恰可能是“已经涨到头了”的那个。动量指标看的是后视镜——它有天然的滞后性。2021 年的市场就是典型的来回波动环境,动量策略在这种环境下注定吃亏,换什么参数都救不了。
**数据说了算:逐年拆开,才能看到整体指标掩盖的问题。不同策略类型有不同的市场环境偏好。**
但年度还是太粗了——知道了哪年亏钱,但赚钱的年份里,是每月稳稳地赚,还是靠某几个月的暴利覆盖其他月份的亏损?
---
## 5.3 收益结构什么样?
年度表现解释了“什么时候赚、什么时候亏”,但没解释“怎么赚的”——是每个月稳稳地赚一点,还是靠几次大赚覆盖很多次小亏?
同样赚钱的两个策略,赚法可能完全不同。你能接受“一年中 8 个月亏钱、靠 4 个月大赚拉回来”的策略吗?
另外,回撤不只是一个数字——最大回撤 -15% 但 3 个月恢复,和最大回撤 -10% 但 1 年没恢复,哪个更痛苦?
### 动手实验 3:月度分布 + 回撤深度
我们一起把这份 spec 写出来。这次重点看两件新东西:**视觉契约要写到颜色与子图布局**、以及**关键阈值(包括看似微小的 -0.001)必须显式**。
#### 起草上下文 + 任务
接续 spec-02 的发现:胜率全时段都差不多,但 2021 年有人亏 10% 有人赚 3%。这一份 spec 把“为什么差距这么大”拆开看:
> **上下文**notebook 里已有四策略 vs 基准的整体对比、逐年拆解、TopNRanking 在反复波动市场中的结构性弱点。
>
> **任务**:分析月度收益分布和历史回撤事件——同样赚钱的策略,赚法可能完全不同。
#### 起草要求:阈值显式 + 视觉契约
第一组要求做月度分布。第二组做回撤事件——**回撤事件的“什么算事件”阈值是这份 spec 最容易被遗漏的精度点**:
> **要求**
>
> 1. 用 oxq `RunResult` 的月度收益(`monthly_returns()`)和回撤序列(`drawdown_series()`)做收益结构分析
> 2. 月度分布分析(四策略):胜率、平均盈利月、平均亏损月、盈亏比、盈亏因子、最长连赢/连亏(**月收益 = 0 算亏损**)
> 3. 打印月度统计对比表 + 画月度收益分布直方图(figsize 14×82×2 子图,20 binsx=0 处红色虚线)
> 4. 回撤深度分析——识别每次回撤事件,**阈值锁定 `THRESHOLD = -0.001`**(即跌幅 ≥ 0.1% 才算事件,避开数值噪声生成假事件):起点 = dd 跌穿阈值的那天,恢复 = dd 反弹回阈值之上的那天,未恢复则标“—”
> 5. 打印每个策略前 5 大回撤事件 + 回撤汇总
> 6. 画水下曲线(figsize 14×104 子图垂直,**与 spec-01 同色继承**EW=#4472C4 / RP=#ED7D31 / TNR=#70AD47 / RP+止损=#FFC000
> 7. 拆解 TopNRanking 2021 年月度——证明胜率 50% 但盈亏比 0.47 的“胜率陷阱”
> 📌 **要点**:**视觉契约的“阈值”包括看不见的数字**。回撤事件的 `THRESHOLD = -0.001` 看似微不足道,但它直接决定有多少行进表——`-0.001` 可能给你 50 个事件,`-0.01` 可能只剩 10 个。spec 不锁阈值,AI 选不同值得出完全不同的“前 5 大回撤”,学员两次跑结果对不上账,根本不知道差在哪。**任何“看起来微小”的阈值都是 spec 必写项**。
> 📌 **要点****跨 spec 颜色继承**。spec-01 的净值曲线用了 4 个颜色,spec-03 的水下曲线必须沿用——否则学员从“绿色 = TNR 净值线”到“水下曲线全是蓝色”,无法联结哪条是 TNR。q5 章一开始就锁的“视觉编码”(颜色、figsize、布局)要在每份后续 spec 显式继承,不能假设 AI 会自动记住。
#### 起草结果呈现
> **结果呈现**:月度统计表(4 行)+ 月度直方图(2×2+ TopNRanking 2021 年逐月明细 + 四策略 2021 月度对比 + 前 5 大回撤事件表 + 回撤汇总表 + 水下曲线图(4 子图)+ 分析文字(**根据实际数据动态描述方向**)
完整 spec 在 [`specs/spec-03-return-distribution-and-drawdown.md`](https://github.com/xingwudao/xquant-learning/blob/main/q5-how-to-validate/specs/spec-03-return-distribution-and-drawdown.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了月度统计表、分布直方图、2021 年胜率陷阱分析、回撤事件表和水下曲线图。
这个实验做了什么?从两个维度拆解“怎么赚的钱”。第一个维度是月度收益分布——把每个策略的日收益按月汇总,统计胜率(赚钱月占比)、盈亏比(平均赚多少 vs 平均亏多少)、最长连赢/连亏月数等。还专门拆解了 TopNRanking 2021 年的逐月明细,和其他三个策略对比同年的胜率和盈亏比,解释为什么胜率差不多、结局却完全不同。第二个维度是回撤深度——识别每次从高点下跌到恢复的完整事件,记录深度、下跌天数、恢复天数,画出水下曲线。
内容比较多,我们分两个维度来看。
**维度一:月度收益分布**
四个策略的月度统计指标如表 5-8 所示。
**表 5-8 四策略月度收益统计对比**
| 策略 | 胜率 | 平均盈利月 | 平均亏损月 | 盈亏比 | 盈亏因子 | 最好月 | 最差月 | 连赢 | 连亏 |
|------|------|----------|----------|--------|---------|--------|--------|------|------|
| EqualWeight | 63.5% | 2.54% | -1.63% | 1.55 | 2.70 | 9.52% | -7.82% | 8 | 3 |
| RiskParity | 61.9% | 2.79% | -1.56% | 1.79 | 2.91 | 11.29% | -6.09% | 11 | 4 |
| TopNRanking | 61.9% | 3.88% | -2.21% | 1.75 | 2.85 | 9.88% | -8.36% | 11 | 4 |
| RP+止损5% | 63.5% | 2.77% | -1.55% | 1.79 | 3.12 | 11.74% | -5.52% | 11 | 4 |
四个策略的月度收益分布直方图如图 5-3 所示。
![月度收益分布](images/q5-how-to-validate/03-monthly-return-distribution.png)
乍一看,四个策略的胜率都差不多——62% 左右。但 Step 2 告诉我们 TopNRanking 在 2021 年亏了 10%,而其他策略同年都是正收益。胜率差不多,为什么结局完全不同?拆解 2021 年的月度数据就能看清原因。
**拆解 TopNRanking 2021 年——胜率的陷阱**
TopNRanking 在 2021 年的逐月收益如表 5-9 所示。
**表 5-9 TopNRanking 2021 年逐月明细**
| 月份 | 收益 |
|------|------|
| 2021-01 | 0.00% |
| 2021-02 | -7.29% |
| 2021-03 | -1.44% |
| 2021-04 | +1.93% |
| 2021-05 | +1.54% |
| 2021-06 | -1.78% |
| 2021-07 | +1.17% |
| 2021-08 | +2.28% |
| 2021-09 | -4.30% |
| 2021-10 | +1.12% |
| 2021-11 | +0.75% |
| 2021-12 | -4.05% |
把四个策略放在 2021 这一年同一张表里对比,胜率与盈亏比的差异如表 5-10 所示。
**表 5-10 四策略 2021 年月度胜率与盈亏比对比**
| 策略 | 胜率 | 平均盈利月 | 平均亏损月 | 盈亏比 | 年收益 |
|------|------|----------|----------|--------|--------|
| EqualWeight | 50% | 2.03% | -1.37% | 1.48 | 3.76% |
| RiskParity | 58% | 1.76% | -2.20% | 0.80 | 1.03% |
| TopNRanking | 50% | 1.46% | -3.14% | 0.47 | -10.09% |
| RP+止损5% | 58% | 1.77% | -1.82% | 0.97 | 3.04% |
看上面的 2021 年对比——TopNRanking 的胜率 50%,跟 EqualWeight 一样。但它赚的月份平均只赚 1.46%,亏的月份平均亏 3.14%,盈亏比只有 0.47。翻译一下:赚的时候赚得少,亏的时候亏得狠。结果:虽然一半的月份赚钱,但亏损月把所有利润全吃掉还倒贴了,全年 -10.09%。
同年的 EqualWeight 胜率也是 50%,但盈亏比 1.48——赚的月份平均赚 2.03%,亏的月份平均只亏 1.37%。同样的胜率,完全不同的结局。
这就是 **胜率的陷阱** —— 大多数人天然觉得“赢的次数越多越好”,但胜率本身意义不大。胜率 90%,每次赚 1 元;亏的那 10% 每次亏 20 元。10 次交易赚 9 元、亏 20 元,净亏 11 元。高胜率,亏钱。
真正决定收益的是 **盈亏比(Win/Loss Ratio** —— 平均每次赚多少除以平均每次亏多少。优秀策略的秘诀是“小亏大赚”:用止损果断控制亏损幅度(小亏),赶上趋势时让利润奔跑(大赚)。追求高胜率,往往是捡了芝麻丢了西瓜。
**盈亏因子(Profit Factor** —— 总盈利除以总亏损的绝对值。大于 1 说明赚的比亏的多,大于 2 算优秀。四个策略的盈亏因子都在 2.7-3.1 之间,都算优秀。但注意这是全时段的数字——拆到具体年份(比如 2021),TopNRanking 的盈亏因子会远低于 1。
**维度二:回撤深度**
**前 5 大回撤事件**
EqualWeight 的前 5 大回撤事件如表 5-11 所示。
**表 5-11 EqualWeight 前 5 大回撤事件**
| # | 开始 | 最低点 | 恢复 | 深度 | 下跌天数 | 恢复天数 |
|---|------|--------|------|------|---------|---------|
| 1 | 2021-11-22 | 2022-10-28 | 2023-06-16 | -16.86% | 340 | 231 |
| 2 | 2021-02-18 | 2021-03-09 | 2021-11-15 | -9.88% | 19 | 251 |
| 3 | 2025-02-20 | 2025-04-07 | 2025-05-06 | -9.22% | 46 | 29 |
| 4 | 2026-01-30 | 2026-02-02 | 未恢复 | -7.95% | 3 | — |
| 5 | 2024-07-18 | 2024-08-05 | 2024-09-26 | -7.82% | 18 | 52 |
最大回撤: -16.86% / 平均恢复: 17天 / 最长恢复: 251天
RiskParity 的前 5 大回撤事件如表 5-12 所示。
**表 5-12 RiskParity 前 5 大回撤事件**
| # | 开始 | 最低点 | 恢复 | 深度 | 下跌天数 | 恢复天数 |
|---|------|--------|------|------|---------|---------|
| 1 | 2021-11-22 | 2022-10-28 | 2023-05-19 | -12.81% | 340 | 203 |
| 2 | 2021-02-22 | 2021-03-09 | 2021-07-15 | -7.98% | 15 | 128 |
| 3 | 2025-02-20 | 2025-04-07 | 2025-04-16 | -6.72% | 46 | 9 |
| 4 | 2026-01-30 | 2026-02-02 | 未恢复 | -6.02% | 3 | — |
| 5 | 2024-07-19 | 2024-08-05 | 2024-09-26 | -5.94% | 17 | 52 |
最大回撤: -12.81% / 平均恢复: 15天 / 最长恢复: 203天
TopNRanking 的前 5 大回撤事件如表 5-13 所示。
**表 5-13 TopNRanking 前 5 大回撤事件**
| # | 开始 | 最低点 | 恢复 | 深度 | 下跌天数 | 恢复天数 |
|---|------|--------|------|------|---------|---------|
| 1 | 2021-02-22 | 2022-06-01 | 2023-03-20 | -17.77% | 464 | 292 |
| 2 | 2026-01-30 | 2026-02-02 | 未恢复 | -14.39% | 3 | — |
| 3 | 2024-07-10 | 2024-09-04 | 2024-12-17 | -14.02% | 56 | 104 |
| 4 | 2023-07-20 | 2023-12-05 | 2024-02-23 | -9.34% | 138 | 80 |
| 5 | 2025-04-23 | 2025-04-28 | 2025-06-30 | -6.48% | 5 | 63 |
最大回撤: -17.77% / 平均恢复: 14天 / 最长恢复: 292天
RP+止损5% 的前 5 大回撤事件如表 5-14 所示。
**表 5-14 RP+止损5% 前 5 大回撤事件**
| # | 开始 | 最低点 | 恢复 | 深度 | 下跌天数 | 恢复天数 |
|---|------|--------|------|------|---------|---------|
| 1 | 2021-11-22 | 2022-10-21 | 2023-05-19 | -13.07% | 333 | 210 |
| 2 | 2021-02-22 | 2021-03-09 | 2021-05-18 | -6.06% | 15 | 70 |
| 3 | 2026-01-30 | 2026-02-02 | 未恢复 | -6.02% | 3 | — |
| 4 | 2024-07-19 | 2024-08-05 | 2024-09-26 | -5.94% | 17 | 52 |
| 5 | 2025-02-20 | 2025-04-07 | 2025-04-11 | -5.57% | 46 | 4 |
最大回撤: -13.07% / 平均恢复: 13天 / 最长恢复: 210天
四个策略的水下曲线如图 5-4 所示,可以直观看到“在水下度过的时间”。
![水下曲线](images/q5-how-to-validate/04-underwater-curves.png)
### 结果解读
收益结构揭示了“怎么赚的钱”——两个关键维度。
**胜率是陷阱,盈亏比才是关键。** 四个策略全时段的胜率都在 62% 左右,看不出差别。但拆到 2021 年,TopNRanking 的盈亏比只有 0.47——每赚 1 块钱,对应要亏 2 块多。同年 EqualWeight 盈亏比 1.48,赚的月份能覆盖亏的月份。同样的胜率,截然不同的结局。真正决定收益的不是赢的次数,而是赢的时候赚多少。
**回撤不只是深度,还有恢复时间。** TopNRanking 的最大回撤 -17.77%,从 2021 年 2 月开始下跌,直到 2023 年 3 月才恢复——整整两年在“水下”。RiskParity 最大回撤 -12.81%,恢复用了 203 天。RP+止损5% 最大回撤 -13.07%,恢复用了 210 天。
**水下曲线(Underwater Curve** —— 每天距离历史最高点跌了多少。它直观展示一个残酷的事实:你有多少时间是在“还没回本”的状态中度过的。回撤不只是一个数字——它是一段煎熬。深度和恢复时间同样重要。
**回撤恢复时间** —— 从最低点爬回前高需要多久。回撤 -15% 但 3 个月恢复,vs 回撤 -10% 但 1 年没恢复——后者对实际持有体验的伤害更大。比起回撤深度,恢复时间更影响你能不能拿得住。
**数据说了算:赚钱的方式和赚钱一样重要。** 别被高胜率迷惑,关注盈亏比;别只看回撤深度,还要看恢复时间。
评估够全面了吗?其实还有一个隐藏问题——你用的参数(动量窗口 20 天、波动率窗口 20 天、止损阈值 5%)是回测出来的“最优值”。但如果把 20 天前后扰动一下,比如改成 15 天或 25 天,结果还差不多吗?如果稍微一动就天差地别,那这个“最优”在未来的数据上恐怕也靠不住。
---
## 5.4 参数动一动,崩不崩?
前面三步的评估都基于特定参数——动量窗口 20 天、波动率窗口 20 天、止损阈值 5%。这些参数是回测跑出来的,在历史数据上表现最好。
但问题是:在最优参数附近前后扰动一下——比如动量窗口从 20 天改成 15 天或 25 天——结果是差不多,还是会差很多?如果附近的参数表现都还行,说明这个”最优”是真的稳;如果稍微一动夏普就从 1.27 跌到 0.85,那历史上的”最优”很可能在未来数据上不再最优。
直觉告诉我们:找到最优参数就行了。真的吗?
### 动手实验 4:参数敏感性扫描
我们一起把这份 spec 写出来。这次重点看两件新东西:**与 q1/spec-06 同样的扫描技术、不同的教学目的**——必须显式区分;以及**“高原 vs 山峰”的视觉对比要靠 y 轴统一来呈现**。
#### 起草上下文 + 任务
学员第二次见到参数扫描——上次是第一章用来“找最优参数”,这次是用来“判稳健”。这种“老技术新目的”必须在上下文段就讲清楚,否则学员会困惑为什么又来一次:
> **上下文**:notebook 里已有四策略的整体指标、逐年收益、月度分布、回撤分析。所有分析都基于特定参数(period=20, freq=10, threshold=0.05)。
>
> 当前问题:如果参数稍微变一下,结论还成立吗?
>
> **注意**:这里的参数扫描和第一章 spec-06 不同——
> - 第一章是“用扫描找最优参数”(目的:挑值)
> - 本章是“用扫描判稳健性”(目的:判形状)
>
> 同样的技术,完全不同的目的。本章不是要找出比 20 更好的窗口,而是要判断“在 20 附近的所有值,结果是不是都差不多”。
>
> **任务**:扫描 RiskParity 波动率窗口、TopNRanking 动量窗口、止损阈值三个维度,对比“高原型”和“山峰型”。
> 📌 **要点**:**同方法不同目的,必须在 spec 里显式区分**。参数扫描在第一章是过拟合的诱因(找漂亮数字),在第五章是过拟合的解药(看是不是高原)。如果 spec 不点名“这次不是为了找最优”,学员会以为“哦又是一次找最优”,下一章 q6 讲过拟合时就接不上。**工具的语义会随章节变化——spec 必须写出当前章节用它干什么**。
#### 起草要求:扫描点设计 + 极差对比
> **要求**
>
> 1. RiskParity 波动率窗口扫描 `vol_period = [10, 15, 20, 25, 30, 40]`:中心 20 是 spec-01 选定值,左右对称展开 ±10/±15,加 40 看远端外推;不试 5(5 天太短无法稳定估计波动率)
> 2. TopNRanking 动量窗口扫描 `mom_period = [10, 15, 20, 25, 30, 40]`(波动率窗口固定 20
> 3. 止损阈值扫描 `thresholds = [0.02, 0.03, 0.05, 0.07, 0.10, 0.15, 0.20]` + 无止损(共 8 行表,无止损作为最后一行,“止损阈值”列填“无”)
> 4. 计算每组的夏普比极差(max - min),打印对比 + 标注“高原型”(极差小)vs“山峰型”(极差大)
> 5. 画三子图折线(figsize 15×5,1×3 布局),**y 轴范围统一**——这样 RP 的“平”和 TNR 的“尖”才能立刻可见
> 6. 输出“山顶营地”隐喻:高原型像一片平坦的高原(站在哪都差不多高),山峰型像一座尖峰(偏一步就掉下去)
> 📌 **要点**:**参数列表的设计必须有可解释的对称性**。`[10, 15, 20, 25, 30, 40]` 不是随便挑——中心 20 是当前选定值,左右展开看附近,加远端 40 看外推。spec 不解释扫描点的来历,学员仿写时只会傻乎乎地照抄数字,遇到自己的策略不知道该挑哪些值扫。
> 📌 **要点**:**y 轴统一是“高原 vs 山峰”对比的视觉钥匙**。三个子图各自缩放,看起来都“波动很大”(每个图都填满了画布);统一 ylim 后,RP 是平的、TNR 是尖的,视觉冲击力立现。**对比图必须共享坐标轴**——这是参数敏感性可视化的固定动作。
#### 起草结果呈现
> **结果呈现**RiskParity 波动率窗口表(6×5+ TopNRanking 动量窗口表(6×5)+ 止损阈值表(8×5)+ 夏普比极差对比 + 折线敏感性图(1×3)+ 分析文字(**根据实际数据动态描述方向**),用“山顶营地”隐喻收束
完整 spec 在 [`specs/spec-04-parameter-sensitivity.md`](https://github.com/xingwudao/xquant-learning/blob/main/q5-how-to-validate/specs/spec-04-parameter-sensitivity.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了三组参数敏感性表和一张折线对比图。
这个实验做了什么?分三组做参数扰动测试。第一组:保持其他参数不变,把 RiskParity 的波动率窗口从 10 天到 40 天逐个试一遍,看夏普比变化多大。第二组:对 TopNRanking 做同样的事,把动量窗口从 10 天到 40 天逐个试。第三组:固定 RiskParity 策略,把止损阈值从 2% 到 20% 逐个试(外加无止损对照)。然后算出每组的夏普比极差(最高减最低),判断策略是“高原型”(参数怎么调都差不多)还是“山峰型”(偏一点就掉到水里)。
**运行结果**
RiskParity 波动率窗口扫描结果如表 5-15 所示。
**表 5-15 RiskParity 波动率窗口敏感性**
| 窗口(天) | 累计收益 | 夏普比 | 卡玛比 | 回撤 |
|----------|---------|--------|--------|------|
| 10 | 104.32% | 1.33 | 1.13 | -12.80% |
| 15 | 102.65% | 1.33 | 1.13 | -12.64% |
| 20 | 98.01% | 1.29 | 1.08 | -12.81% |
| 25 | 96.76% | 1.28 | 1.06 | -12.93% |
| 30 | 109.29% | 1.39 | 1.10 | -13.49% |
| 40 | 111.45% | 1.40 | 1.05 | -14.37% |
夏普比极差: 0.13
TopNRanking 动量窗口扫描结果如表 5-16 所示。
**表 5-16 TopNRanking 动量窗口敏感性**
| 窗口(天) | 累计收益 | 夏普比 | 卡玛比 | 回撤 |
|----------|---------|--------|--------|------|
| 10 | 104.21% | 1.00 | 0.72 | -19.91% |
| 15 | 83.31% | 0.85 | 0.73 | -16.78% |
| 20 | 153.03% | 1.27 | 1.05 | -17.77% |
| 25 | 144.73% | 1.21 | 1.10 | -16.46% |
| 30 | 105.42% | 1.02 | 0.87 | -16.64% |
| 40 | 101.54% | 1.00 | 0.74 | -19.03% |
夏普比极差: 0.42
止损阈值的扫描结果如表 5-17 所示(基于 RiskParity 策略,调仓频率 10 天)。
**表 5-17 止损阈值敏感性**
| 止损阈值 | 累计收益 | 夏普比 | 最大回撤 | 交易次数 |
|---------|---------|--------|---------|---------|
| 2% | 125.87% | 1.63 | -9.65% | 402 |
| 3% | 118.80% | 1.56 | -11.62% | 394 |
| 5% | 106.23% | 1.43 | -13.07% | 387 |
| 7% | 100.90% | 1.36 | -13.26% | 382 |
| 10% | 106.16% | 1.40 | -12.02% | 375 |
| 15% | 99.30% | 1.32 | -12.51% | 371 |
| 20% | 97.04% | 1.29 | -13.23% | 370 |
| 无止损 | 98.01% | 1.29 | -12.81% | 369 |
夏普比极差: 0.35
三组参数的夏普比极差汇总如表 5-18 所示。
**表 5-18 参数敏感性极差对比**
| 参数组 | 夏普比极差 | 类型 |
|--------|----------|------|
| RiskParity | 0.13 | 高原型(参数不敏感) |
| TopNRanking | 0.42 | 山峰型(参数敏感) |
| 止损阈值 | 0.35 | 山峰型(参数敏感) |
三组参数的夏普比折线对比如图 5-5 所示。
![参数敏感性对比](images/q5-how-to-validate/05-parameter-sensitivity.png)
### 结果解读
最后一个认知升级:**好策略不怕参数变。**
**参数敏感性(Parameter Sensitivity** —— 改一个参数值,策略表现变化有多大。这是策略体检中不可或缺的一项。
看 RiskParity:波动率窗口从 10 天调到 40 天,夏普比在 1.28-1.40 之间,极差只有 0.13。不管你选哪个窗口,结果都差不多。这叫 **高原型(Plateau** —— 像一片平坦的高原,站在哪里都差不多高。
再看 TopNRanking:动量窗口从 10 天调到 40 天,夏普比在 0.85-1.27 之间,极差 0.42——是 RiskParity 的 3 倍多。窗口选 20 天时夏普 1.27,选 15 天时只有 0.85,掉了 1/3。这叫 **山峰型(Peak** —— 像一座尖峰,只有山尖上最高,稍微偏一步就掉下去。
想象你在山上找营地。高原型策略像一片平坦的高原——站在哪里都差不多高。山峰型策略像一座尖峰——只有山尖上最高,稍微偏一步就掉下去。你选哪个?
**好策略不依赖某个”魔法参数”。** 如果换一个参数值夏普就**从 1.27 跌到 0.85**(掉了 1/3),说明你可能只是运气好选对了那个值。RiskParity 的有效性来自底层逻辑——“波动大的少买”,不管你用 10 天还是 40 天窗口来衡量波动,排序关系不会剧变。TopNRanking 的高收益则部分依赖于”恰好选对了参数 20”。
这也呼应了 Step 2 的发现:TopNRanking 在某些年份负收益、对参数敏感,它的高收益可能有运气成分。RiskParity 虽然收益不如 TopNRanking 高,但参数稳健、年度一致性好——这才是更可靠的策略。
**选策略就像选营地——要选高原,不选尖峰。**
回头看:还记得第一章末尾你亲眼看到的那次“参数扫描里冠军在样本外掉到了水下”吗?那就是山峰型在参数 + 时间两个维度上同时露出的脸——你当时亲手撞了一次墙,从此对回测多一份警觉。
**这一章的高原型 vs 山峰型,正是“做要规则、看要指标、还要怀疑你的指标”这句口号里“看 → 疑”的桥梁——下一章你会把“疑”练成主线。**
---
## 5.5 回头看:你刚才做了什么?
**这一节是“看”的操作流程总图——把四步实验铺成你以后每次接到一份回测都能照着走的检查路径。** 四步实验,一张完整的“策略体检地图”:
- **Step 1: 跟什么比?** -- 基准 + Alpha(超额收益)+ 多维指标(夏普比/卡玛比/索提诺比)
- **Step 2: 每年都有收益吗?** -- 分段检验 + 策略类型弱点(动量滞后性)
- **Step 3: 收益结构什么样?** -- 胜率陷阱/盈亏比/盈亏因子 + 回撤深度/恢复时间
- **Step 4: 参数动一动?** -- 参数敏感性 + 高原型 vs 山峰型
从今以后,拿到任何策略的回测结果,你都可以用这四步做一次“全面体检”,五个维度的体检问题如表 5-19 所示。
**表 5-19 策略体检维度速查**
| 维度 | 问题 |
|------|------|
| 基准 | 跑赢了吗? |
| 年度 | 每年都赚吗? |
| 分布 | 赚法健康吗? |
| 回撤 | 最坏到什么程度? |
| 参数 | 参数改了还行吗? |
**从“看收益率”到“全面体检”——这是本章最重要的认知升级。**
### 四个最容易让你“被自己骗”的陷阱
每个维度对应一种最容易踩的陷阱——它们各有不同名字,但本质都是“指标好看,结果不靠谱”。下次别人(或者你自己)拿一份“漂亮”的回测来,先用这四个 lens 扫一遍:
1. **基准陷阱**——只看年化收益,没和“什么都不做”对比。EqualWeight 累计 84% 看着还行,等权买入持有同期 95%——你忙了半天反而落后。
2. **整体陷阱**——只看多年累计,掩盖了某些年份的大亏。TopNRanking 累计第一,但 2021 年是唯一亏钱的策略(-10.09%),其他三个策略同期都是正收益——普通人在那年就放弃了。
3. **胜率陷阱**——胜率 90% 不等于赚钱。一次大亏可以抹平十次小赚,真正决定结果的是盈亏比和盈亏因子。
4. **山峰陷阱**——某个参数最优,稍微一偏夏普就掉一大截。这往往不是规律,是巧合——和第一章末尾你撞上的那堵墙是同一种东西。
**四个陷阱合起来,就是“看要指标”这一层在跟你强调的事——指标越多越好;但每个指标只看一面,你得把四面合起来看,才能少被自己骗。**
---
## 5.6 本章总结
### 策略进化路径
**第四章产出:RiskParity + 止损 + 三个对比策略**
- Step 1: 跟基准比 -- 不是所有策略都跑赢了“什么都不做”
- Step 2: 逐年拆 -- 某些年份亏损,动量策略有结构性弱点
- Step 3: 看分布 -- 胜率是陷阱,盈亏比才是关键
- Step 4: 动参数 -- 高原型稳,山峰型脆
**第五章产出:完整的策略评估方法论 + “体检清单”**
### 概念速查表
本章涉及的核心概念汇总如表 5-20 所示,方便随时回查。
**表 5-20 第五章核心概念速查**
| 概念 | 含义 | 类比 |
|------|------|------|
| 基准(Benchmark) | 用来做比较的参照物——“什么都不做”时你能赚多少 | 考试的“及格线” |
| 超额收益 / Alpha | 策略收益减去基准收益——超出及格线的部分才是你的本事 | 第一章电梯/楼梯类比,本章放回“基准之后才能算”的语境 |
| 卡玛比(Calmar) | 年化收益 / 最大回撤 | 每承受 1% 最大回撤换多少收益 |
| 索提诺比(Sortino) | 只用下行波动率做分母 | 上涨波动不算风险 |
| 分段检验 | 把回测按年份拆开 | 整体及格不等于每科都及格 |
| 动量滞后性 | 动量指标基于“过去涨了多少” | 后视镜——过去涨不代表未来继续涨 |
| 胜率的陷阱 | 胜率本身意义不大 | 胜率 90% 也可能亏钱 |
| 盈亏比(Win/Loss Ratio | 平均盈利 / 平均亏损 | 真正决定收益的指标 |
| 盈亏因子(Profit Factor | 总盈利 / 总亏损 | 大于 1 赚钱,大于 2 优秀 |
| 水下曲线(Underwater) | 每天距离历史最高点跌了多少 | “你在水下待了多久” |
| 回撤恢复时间 | 从最低点爬回前高要多久 | 比深度更影响持有体验 |
| 参数敏感性 | 改参数后结果变化多大 | 好策略不怕参数变 |
| 高原型 vs 山峰型 | 高原:参数怎么调都差不多。山峰:偏一点夏普极差大 | 找营地选高原不选尖峰 |
### 本章核心认知
走完四步实验,沉淀下来的认知如表 5-21 所示。
**表 5-21 第五章核心认知**
| 认知 | 来源 |
|------|------|
| “好不好”必须有参照物——基准 | Step 1 实验 |
| 跑赢基准的部分才是 Alpha——你的技能回报 | Step 1 实验 |
| 夏普比、卡玛比、索提诺比是三把不同的尺子 | Step 1 实验 |
| 整体指标可能掩盖年度亏损——分段检验 | Step 2 实验 |
| 动量策略在反复震荡的市场中有结构性弱点——滞后性 | Step 2 深挖 |
| 胜率是陷阱——真正决定收益的是盈亏比,优秀策略的秘诀是“小亏大赚” | Step 3 实验 |
| 回撤不只是深度,还有恢复时间——水下曲线 | Step 3 实验 |
| 好策略不怕参数变——高原型 >> 山峰型 | Step 4 实验 |
| 先猜后验,数据说了算 | 贯穿全章 |
### 带走的问题
- 体检工具给了你客观数据。但**回测本身可信吗?** 回测能调参数让收益翻倍——这是真的好,还是自欺欺人? → 第六章
- 如果你在 2023 年跑回测发现某个策略 2021-2023 表现好,你怎么知道它 2024 年还会好? → 第六章
带着这两个问题进入下一章——它们正是本书口号“**做要规则、看要指标、还要怀疑你的指标**”中“**疑**”那一步要训练的核心。这一章你已经把“看”练了一遍,下一章我们专门把“疑”练成主线。
> 本章所有代码的可运行版本见 `notebooks/q5-how-to-validate.ipynb`
---
## 拓展阅读
本章正文聚焦“四步策略体检”。下面三段是动手之前可以省略的视野铺垫——读完正文回头看,你会更清楚指标背后的量化惯例。
### 为什么“年化”用 252 这个数字
正文反复说“年化收益率”、“年化波动率”——背后都隐含一个换算系数:**252**。
A 股一年大约 250-252 个交易日(节假日不开盘),美股 252 个,全球主流市场都接近这个数。所以“日 → 年”的换算是:**年化收益率 ≈ 平均日收益率 × 252**;**年化波动率 ≈ 日收益率标准差 × √252**(注意是开根号,不是直接乘)。比如一个策略日波动率 1%,年化波动率 ≈ 1% × √252 ≈ 15.87%。回测里所有“年化”指标都用了这个换算——不是定理,但是行业惯例。
### 夏普比率 vs 信息比率(IR)
夏普比率 = 年化收益 / 年化波动。但你可能在论文或基金报告里见过另一个相似的指标——**信息比率(Information Ratio, IR**。
二者长得很像,区别在分子:**夏普比率分子是策略的“全部收益”,IR 分子是“策略相对基准的超额收益(Alpha)”**。也就是说,IR 衡量的是“主动管理的性价比”——剔除了 Beta,纯看 Alpha 部分。基金经理之间互相比 IR 比夏普比率更公平,因为大家都被市场抬着走,IR 才能看出谁真的有本事。本书课程默认用夏普比率(更直观),但你以后看到 IR 不要感到陌生——它只是把“分子换成 Alpha”的夏普比率。
### 索提诺比的“MAR”参数
正文说索提诺比 = 年化收益 / 下行波动率。但什么算“下行”?业界常用的定义是:**收益低于某个阈值(叫 MARMinimum Acceptable Return,最低可接受收益率)的那部分波动**。
最常见的两种 MAR 选法:①**MAR = 0**——把“亏损”定义为“日收益率 < 0”,最简单;②**MAR = 无风险利率**(如美债 5%)——把“没跑赢国债”也算“下行”,更严格。本书计算时用的是 MAR = 0(更直观),但你看到不同来源的索提诺比数值有差异时,先确认它们用的是哪种 MAR——否则比的不是同一个东西。
+578
View File
@@ -0,0 +1,578 @@
# 第六章:看清好看的回测:别被自己骗了
第五章我们给策略做了一次全面体检——基准对比、逐年拆解、收益分布、参数敏感性。现在手上有一套完整的评估方法论。
但所有体检结果都基于一个前提:**回测结果是真实的。**
真的吗?
回顾第五章的发现:TopNRanking 策略在 2021 年亏了 10%,换遍所有动量窗口都救不回来——这是动量策略的结构性弱点。但同样的策略在 2023-2025 的好市场上表现不错。这引出一个危险的念头:**如果我们在好市场上精心挑选参数,找到“收益最好”的组合,信心满满地投入资金——未来再遇到差市场会怎么样?**
回测可以调参数让收益翻倍——这是真的好,还是自欺欺人?
### 路线图
**选什么标的(第二章)→ 每个买多少(第三章)→ 什么时候买卖(第四章)→ 怎么验证有效(第五章)→ 如何避免自欺欺人(第六章)**
**这一章你仍在飞轮的“归因层”,但主练动作切到了“疑”——本书口号“做要规则、看要指标、还要怀疑你的指标”中“还要怀疑你的指标”,就是这一章要让你练成肌肉记忆的事。** 第一章你已经亲手撞过一次墙——参数扫描里“冠军”在样本外掉到水下;这一章我们给你**四把铲子**,让那种墙以后看见就能挖开:样本内/外、Walk-forward、交叉验证、规则负担。本章解决第五步——识别过拟合陷阱,学会检验策略的真实有效性。四步对应的问题与方法如表 6-1 所示。
**表 6-1 第六章四节问题与方法对照**
| 步骤 | 问题 | 方法 |
|------|------|------|
| Step 1 | 能不能找到收益最好的参数? | 参数优化 + 样本内/样本外 |
| Step 2 | 市场在变,参数也该跟着变吧? | Walk-forward 分析 |
| Step 3 | 一条路径够可靠吗? | 交叉验证 |
| Step 4 | 规则越多回测越好——是好事吗? | 规则负担 |
铁律不变:**先猜后验,数据说了算。**
(操作流程见前言“怎么使用这本书”。)
---
## 6.1 能不能找到收益最好的参数?
**和第一章 1.5 节的关系:** 第一章末尾你扫过 24 个一维参数(均线窗口),用 60-40 切分做样本外测试,已经撞过一次过拟合的墙。这一章接着上一次的脚印继续走——**同一种切法,但量级跃迁**:参数从 1 维涨到 4 维(动量 / 波动 / 调仓 / 止损),组合数从 24 涨到 490。维度多了之后,过拟合的样子会变什么样?这一节给你答案。
第五章用了默认参数——动量窗口 20 天、波动率窗口 20 天、调仓频率 10 天。但这些是随便选的,不是天经地义的。
直觉告诉我们:**多试几组参数,选收益最高的就行了。** 几百种组合里总有比默认值更好的吧?
这个想法太自然了。就像买手机——把市面上所有型号的参数列出来,逐项比较,选评分最高的那款。参数也一样,把所有组合都试一遍,选出表现最好的——这叫**参数优化**,也叫**网格搜索(Grid Search)**——一个参数组合就像是一个格子,挨个格子试一遍,总能找到最优解。
真的这么简单吗?试试看。
### 动手实验 1:参数优化与样本内/样本外
这个实验用到 open-xquant 的参数优化工具:`GridSearch`(网格搜索——把所有参数组合都试一遍)和 `WalkForward`(前推分析——模拟“边走边调”的真实场景)。
我们一起把这份 spec 写出来。这次重点看三件新东西:**维度跃迁**(从一维到四维参数)、**时段语义切分**(好市场挑参数 / 差市场验证)、**业务约束的可读字符串写法**。
#### 起草上下文 + 任务
第一章 1.5 节用一行“前 60% / 后 40%”切了一刀——这是结构性切分。这一节不一样:每段时间要带金融含义。
> **上下文**:本章探讨参数优化和过拟合。第五章看了默认参数(动量 20 / 波动率 20 / 调仓 10)的 plateau vs peak,但学员从没试过“从一堆参数里找最好的”。承接第五章的叙事——TopNRanking 在 2017-2020 好市场上参数选得好,2021 年差市场会怎么样?
>
> **任务**:在 `q6-avoid-overfitting.ipynb``oxq.optimize.GridSearch` 跑参数网格搜索,用样本内 / 样本外方法检验过拟合。
> 📌 **要点**:数据切分要带“市场含义”。第一章的 60/40 是按比例切的,这里换成“2017-2020 好市场 / 2021-2023 差市场”——后者把每段对应的市场行情说清楚,学员才能看懂“为什么是这两段”。**任何切分型 spec,起止日期都要四个全锁死(IS_START / IS_END / OOS_START / OOS_END),否则今年跑、明年跑切出来不一样,过拟合证据就站不住。**
#### 起草要求:维度跃迁与业务约束
第一章扫过 24 个一维参数(均线窗口)。这一节直接跳到 4 维——动量 / 波动率 / 调仓频率 / 止损阈值。
> **要求**
>
> 1. 数据切割:样本内 `2017-01-01 ~ 2020-12-31`、样本外 `2021-01-01 ~ 2023-01-31`,四个端点全锁死
> 2. 用 `ParameterSet` 定义参数空间:动量 / 波动率窗口 `[10, 15, 20, 25, 30]`、调仓频率 `[5, 10, 15, 21]`、止损阈值 `[0.03, 0.05, 0.07, 0.10, 0.15, 0.20]`
> 3. 分两组跑:无止损组(3 维)+ 含止损组(4 维),约束后总组合数约 490
> 4. 业务约束用可读字符串写:`add_constraint("RebalanceFrequencyRule.interval_days <= mom.period")``<= vol.period`
> 5. 调仓频率不应超过信号窗口——这是领域知识,不是技术约束
> 6. 用 `GridSearch` 跑两组参数空间,在样本内、样本外各跑一次
> 📌 **要点**:维度跃迁后要分组管理。一维扫描随便扫,四维直接扫会爆——5×5×4×6 = 600 还要约束过滤。**分两组跑(无止损 70 组合 + 含止损 420 组合)既能控制总数,又能让止损这一维在结果里被单独看清——也为后面“规则负担”那一节埋了伏笔。**
> 📌 **要点**:业务约束用可读字符串,不要用 lambda。`"RebalanceFrequencyRule.interval_days <= mom.period"` 这种 DSL 字符串比 `lambda p: p["RebalanceFrequencyRule.interval_days"] <= p["mom.period"]` 更适合 0 基础学员看。**约束的“为什么”要写到 spec 注释里——“信号 20 天更新一次,调仓 60 天才看一次 = 在用过期信号做决策”——这是把领域知识嵌入 spec 的标准写法。**
#### 起草结果呈现
> **结果呈现**
>
> 1. 打印 Top 20 表(按样本内夏普排名)+ 样本外对比 + 全局相关系数 + 衰减分布
> 2. 散点图(样本内 vs 样本外夏普,y=x 对角线虚线,Top 20 红色高亮)
> 3. Top 20 柱状图(每组双柱:样本内 / 样本外)
> 4. 总结打印按实际数据动态描述:Top 20 平均衰减、对角线下方点的占比、过拟合定义
完整 spec 在 [`specs/spec-01-parameter-optimization.md`](https://github.com/xingwudao/xquant-learning/blob/main/q6-avoid-overfitting/specs/spec-01-parameter-optimization.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了网格搜索的完整结果。我们来看看代码做了什么。
数据被切成了两段:**样本内(In-Sample** 用 2017-2020 四年做优化,**样本外(Out-of-Sample** 用 2021-2023 做验证。为什么选这么早的数据?因为样本内需要足够长(至少 3-4 年),才能覆盖牛熊周期,避免只在单一行情下调参。时间顺序也必须正确——先优化,后验证,就像真实投资中你只能用历史数据做决策,然后面对未来。
**样本内**就是你看着答案复习的模拟考试卷;**样本外**就是真正的考试——你从没见过的题目。
四个维度的参数组合——动量窗口 5 种 × 波动率窗口 5 种 × 调仓频率 4 种 × 止损阈值(有/无),加上约束过滤(调仓频率不能超过信号窗口——用过期信息做决策没有意义),最终产生 490 种有效组合。
`GridSearch` 把 490 种参数组合逐个跑回测,按夏普比排名。然后用同样的参数在样本外再跑一遍,看“真正的考试”成绩如何。
### 运行结果
490 种组合全部跑完,先看样本内 Top 20 的“样本外真相”——样本内夏普 Top 20 及其样本外表现(部分)如表 6-2 所示。
**表 6-2 样本内夏普 Top 20 及其样本外表现(部分)**
| # | mom | vol | freq | sl | 样本内夏普 | 样本外夏普 | 衰减 |
|---|-----|-----|------|----|-----------|-----------|------|
| 1 | 15 | 10 | 10 | 0.03 | 1.39 | 0.50 | -0.88 |
| 2 | 15 | 15 | 10 | 0.03 | 1.34 | 0.49 | -0.85 |
| 3 | 15 | 25 | 10 | 0.03 | 1.30 | 0.38 | -0.92 |
| 4 | 15 | 20 | 10 | 0.03 | 1.29 | 0.39 | -0.90 |
| 5 | 30 | 10 | 5 | 0.03 | 1.25 | 0.02 | -1.23 |
| 7 | 30 | 10 | 10 | 0.03 | 1.24 | -0.07 | -1.31 |
| 14 | 15 | 10 | 10 | 无 | 1.18 | 0.01 | -1.17 |
**样本内 Top 20 统计:** 样本内夏普均值 1.22(范围 1.15 ~ 1.39),样本外夏普均值 0.20(范围 -0.09 ~ 0.50),平均衰减 -1.02。
**全部 490 个组合统计:** 样本内/样本外夏普相关系数 0.04,样本外比样本内差的比例 100%,夏普衰减均值 -0.82、中位数 -0.88。
散点图与 Top 20 柱状图如图 6-1 所示。
![490 个参数组合:样本内 vs 样本外夏普比](images/q6-avoid-overfitting/01-is-vs-oos-scatter-and-top20.png)
### 结果解读
第一个认知升级:**参数优化是一把双刃剑。**
先看数字。样本内 Top 20 的平均夏普 1.22——这在量化里属于很拿得出手的成绩。但拿到样本外(2021-2023,没见过的数据),平均夏普跌到了 0.20。衰减了 83%。
再看散点图。每个点代表一种参数组合,对角虚线是“样本外 = 样本内”的理想状态。如果所有点都在对角线上,说明样本内的好成绩在样本外也能兑现。但现实是:**100% 的点都在对角线以下**——没有一种参数组合的样本外表现达到样本内水平。
更扎心的是:样本内夏普和样本外夏普的相关系数只有 0.04——几乎没有关系。样本内排名第一的组合,样本外并不一定最好。
这就是**过拟合(Overfitting)**——你的策略只会死读书,课后习题全都会做,结果高考考砸了。它“记住了”过去数据的特征(噪音),而不是发现了真正的规律。
柱状图更直观:蓝色柱子(样本内)高高在上,红色柱子(样本外)矮了一大截。有些组合样本外甚至为负——在“真正的考试”中亏了钱。
**数据说了算:参数优化后,必须在没见过的数据上验证。样本内的好成绩不算数。**
**这就是第一把铲子:样本内 / 样本外切分。** 它不能直接帮你赚钱——但它能让你在第一时间看到“漂亮回测背后到底有没有真东西”。第一章那次“被自己骗”的肉身记忆,到这里第一次有了一件可以挂在墙上的工具。
怎么办?我们把数据切成了“优化”和“验证”两段,只验证了一次。但只验证一次够吗?市场在变,参数也该跟着变吧?
---
## 6.2 市场在变,参数也该跟着变吧?
Step 1 把数据切成了“优化”和“验证”两段,发现了过拟合问题。但我们只切了一次、只验证了一次。
市场每年都在变。2020 年选的参数,2023 年还适用吗?能不能模拟“每隔一段时间重新优化参数”的过程?
直觉告诉我们:**市场每年不一样,参数当然应该定期更新。** 用最近的数据重新优化,应该比死守一组参数更好。
这个想法叫 **Walk-forward(前推验证)分析**——“边走边调”。在历史数据上模拟“每隔一段时间重新优化参数”的过程:用过去两年数据选参数,在接下来半年验证;然后窗口往前滑半年,再选参数、再验证……如此反复。
有两种滑动方式:
- **滚动式(Rolling**——固定数据窗口宽度,窗口左边位置不断往右移动。好处是只看最近的市场,坏处是丢弃了更早的数据。就像只翻最近两年的课本复习
- **锚定式(Anchored**——固定数据窗口左边位置,窗口宽度不断拉长。好处是利用了所有历史数据,坏处是旧数据可能不再适用。就像从高一课本开始全部复习
试试看——用两种不同的方式“边走边调”。
### 动手实验 2Walk-forward 分析
我们一起把这份 spec 写出来。这次重点看两件新东西:**循环型 spec 的端点全锁**(窗口起止 + 长度 + 步长四个参数都不留浮动)、**对照实验式 spec 的并列结构**(让 Rolling / Anchored 自己跑出对比)。
#### 起草上下文 + 任务
> **上下文**:在 `q6-avoid-overfitting.ipynb` 中已有 490 个组合的网格搜索结果 + 样本内/样本外对比,过拟合现象已暴露。当前问题:数据只切了一次。市场在变,2023 年选的参数 2025 年还适用吗?能不能模拟“定期重新优化”的过程?
>
> **任务**:在 notebook 新建单元格,用 `oxq.optimize.WalkForward` 实现 Walk-forward 分析,对比 Rolling 和 Anchored 两种方式。
#### 起草要求:循环窗口的四个端点
循环型 spec 比单次 spec 脆弱得多——单次 spec 漏掉一个日期可能没事,循环型 spec 漏掉就会让“折数”在不同时间跑出不同结果。
> **要求**
>
> 1. 复用 Step 1 的参数空间(无止损组 70 组合)和约束(`RebalanceFrequencyRule.interval_days <= mom.period` / `<= vol.period`
> 2. 数据范围:`DATA_START="2017-01-01"``ANALYSIS_END="2026-05-06"`——四个端点全锁死,不要用 `pd.Timestamp.now()`
> 3. Rolling Walk-forward:训练窗口 `"2Y"`、测试窗口 `"6M"``anchored=False`
> 4. Anchored Walk-forward:同上,`anchored=True`(训练窗口从起点不断扩大)
> 5. `train_period="2Y"` 的依据:覆盖一个完整中期周期;`test_period="6M"` 的依据:足够长以观察策略表现,又足够短以让多个折落进数据范围
> 📌 **要点**:循环型 spec 的窗口端点要四个全锁死——**起始日 + 结束日 + 窗口长度 + 步长**。漏掉任何一个,折数都会随时间漂移。如果用 `pd.Timestamp.now()` 当结束日,半年后跑出来折数从 15 变成 16,参数稳定性的结论可能就反过来了。**任何带 `for` 循环的 spec 都要回头检查这四个端点。**
> 📌 **要点**:循环窗口的“为什么”也要写。`"2Y" / "6M"` 不是随手挑的——背后有“覆盖一个完整中期周期”和“折数 ≥ 5 才有统计意义”两个判断。**领域知识里的窗口长度选择,跟参数选择本身一样重要——把判断写进 spec 注释,别让 AI 揣摩。**
#### 起草结果呈现
> **结果呈现**
>
> 1. 用 `to_dataframe()` 打印每折详情:训练时段、验证时段、最优参数、样本内/样本外夏普
> 2. 汇总:`oos_sharpe_ratio()``deterioration()`、各折参数集合
> 3. 对比图(figsize 14×6,上下两栏):上栏每折样本外夏普(Rolling 红 / Anchored 蓝两条线),下栏参数稳定性线图(mom.period / interval_days 各折取值)
> 4. 标题「Walk-forward 分析:Rolling vs Anchored」
> 5. 打印分析按实际数据动态描述:Anchored 是否锁定参数、Rolling 是否乱跳、衰减对比
完整 spec 在 [`specs/spec-02-walk-forward.md`](https://github.com/xingwudao/xquant-learning/blob/main/q6-avoid-overfitting/specs/spec-02-walk-forward.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了 Rolling 和 Anchored 两种 Walk-forward 的完整结果。
训练窗口 2 年,验证窗口 6 个月。每次用 2 年数据选出最优参数,在接下来 6 个月验证。然后窗口滑动,重复这个过程。最终得到 15 折结果——15 次“选参数 → 验证”的记录。
### 运行结果
两种方式的对比如表 6-3 所示。
**表 6-3 Rolling vs Anchored 对比**
| 方式 | 样本外夏普 | 衰减 | 唯一参数组合 |
|------|-----------|------|-------------|
| Rolling | 0.14 | -36.2% | 7/15 |
| Anchored | 0.13 | -23.5% | 5/15 |
Anchored 参数更稳定。
关键发现:Rolling 在 15 折(每次滑动算一折,一共滑了 15 次)中选了 7 种不同的参数组合——平均两折就换一种。而 Anchored 只有 5 种,且其中 7 折选了相同的参数(动量窗口 20、波动率窗口 15、调仓频率 5)——单一组合占了近一半折数。
Anchored 的逐折参数一览如表 6-4 所示。
**表 6-4 Anchored Walk-forward 逐折参数与样本外夏普**
| Fold | mom | vol | freq | 样本外夏普 |
|------|-----|-----|------|-----------|
| 1 | 15 | 15 | 5 | 2.50 |
| 2 | 15 | 15 | 5 | 0.71 |
| 3 | 20 | 15 | 5 | -0.13 |
| 4 | 15 | 10 | 5 | 0.68 |
| 5 | 30 | 15 | 5 | -2.49 |
| 6 | 20 | 15 | 5 | -1.83 |
| 7 | 20 | 15 | 5 | 1.42 |
| 8 | 20 | 15 | 5 | -0.67 |
| 9 | 20 | 15 | 5 | 1.91 |
| 10 | 20 | 15 | 5 | -1.75 |
| 11 | 30 | 15 | 5 | 1.99 |
| 12 | 20 | 20 | 5 | 1.22 |
| 13 | 30 | 15 | 5 | 1.23 |
| 14 | 20 | 15 | 5 | 1.31 |
| 15 | 30 | 15 | 5 | 0.98 |
mom=20, vol=15, freq=5)这组参数前后出现了 7 次,是 Anchored 的最频繁选择。其余四种组合——(15, 15, 5)、(30, 15, 5)、(15, 10, 5)、(20, 20, 5)——各出现 1 到 4 次。换句话说:动量窗口在 15-30 之间换、波动率窗口大多停在 15、调仓频率 15 折全选 5。Rolling 的波动更宽——动量窗口在 15-30 之间跳、波动率窗口从 10 一直到 30,没有明确偏好。
每折样本外夏普走势与参数稳定性对比如图 6-2 所示。
![Walk-forward 分析:Rolling vs Anchored](images/q6-avoid-overfitting/02-walk-forward-rolling-vs-anchored.png)
### 结果解读
第二个认知升级:**参数稳定性比参数最优更重要。**
先看上图。两条实线是样本外夏普——Rolling(红色)和 Anchored(蓝色)都有起伏,但起伏的方向大体一致。下图更关键:Rolling 的参数线在 mom=15-30、vol=10-30 间跳来跳去,Anchored 的两条线被压在更窄的区间内(mom 多在 20、vol 多在 15)。
**参数稳定性(Parameter Stability**——好的参数组合应该在不同时间窗口中保持一致。如果每换一个窗口参数就变,说明“最优参数”取决于你看哪段市场,这个参数可能只是在拟合噪音。
Anchored 在 15 折中 7 折都选了(mom=20, vol=15, freq=5)这一组——15 次里 7 次是同一个答案;剩下的折在四组里跳,但波动率窗口大多稳在 15、调仓频率 15 折全是 5。不管训练集从 2 年扩大到 9 年,参数选择都被限制在一个窄区间——这是一个好信号。而 Rolling 在 15 折里选出 7 种不同组合,每两折就换一次,说明它在追逐短期市场特征,而不是捕捉长期规律。
样本内到样本外的衰减比例:Rolling 衰减 36.2%Anchored 衰减 23.5%。Anchored 衰减更小,因为它用了更多数据训练,不容易被噪音骗。
Walk-forward 的核心价值:不是找到一个“永远最优”的参数,而是检验你的优化方法在历史上是否靠谱。如果历史上每次优化后验证都不错,你对下一次优化会更有信心。
**数据说了算:参数稳定 = 更可信。如果最优参数总在变,你怎么知道现在选的参数下个月还是最优?**
**这就是第二把铲子:Walk-forward。** 它把“换段时间还稳定吗”从一种焦虑变成了一项可以滚动跑的检查——参数稳定的策略你可以拿得动,参数到处变的策略你可以早早放弃。
但 Walk-forward 只用了一种切法——如果数据切的方式不同,结论还一致吗?
---
## 6.3 一条路径够可靠吗?
Walk-forward 模拟了“边走边调”,但我们只用了一种切法。如果换一种切数据的方式,结论还一样吗?
做科学实验有个基本原则:**一次实验不够,要重复多次。** 只做一次实验就下结论,可能只是碰巧。同样的道理——只验证一次(Step 1)或只用一种切法(Step 2)都不够可靠。
直觉告诉我们:**好策略不管怎么切都好。** 如果 5 次验证里 4 次都选了同样的参数,比只验证 1 次可信得多。
这就是**交叉验证(Cross Validation)**——用多种数据切分方式验证同一个策略,看结论是否一致。
但金融数据有一个特殊的限制,先用一个生活类比说清楚:**做菜可以“先看明天的菜单再决定今天买什么菜”——但炒股不行**,因为你今天根本不知道明天会发生什么。
把这个限制翻译成两个量化术语:
- **时间序列属性(Time-Series Nature** —— 金融数据有先后顺序:今天比昨天晚发生,比明天早发生。顺序不能打乱。
- **时间穿越 / 数据泄露(Look-Ahead Bias / Data Leakage** —— 在训练或验证时,不小心让模型“看到了未来”。比如本应该用 2020 年的数据训练,结果你混了一点 2024 年的数据进来——这等于让你的策略在考试前“偷看了答案”,回测会异常漂亮,但放到实盘上一笔笔真金白银地亏。
所以做交叉验证时**每折必须保持时间顺序**——这就是 **时间序列分割(Time-Series Split**:训练集永远在测试集之前,绝不让“未来”出现在“过去”的训练里。
试试看。
### 动手实验 3:交叉验证
我们一起把这份 spec 写出来。这次重点看两件新东西:**时序切分而非随机切分**(金融数据的硬约束)、**接续型 spec 的减法**(不复述前面已经讲过的概念,专心做一件新事)。
#### 起草上下文 + 任务
> **上下文**:在 `q6-avoid-overfitting.ipynb` 中已有参数优化 + Walk-forward 分析。Walk-forward 只用了一种切法——如果换一种切法,结论还一样吗?一次验证和多次验证,可信度差多少?
>
> **任务**:在 notebook 新建单元格,用 `oxq.optimize.TimeSeriesCV` 实现时间序列交叉验证。
#### 起草要求:时序切分的硬约束
> **要求**
>
> 1. 复用 Step 2 的参数空间和约束(无止损组 70 组合)
> 2. 数据范围:`DATA_START="2017-01-01"``ANALYSIS_END="2026-05-06"`——与 Step 2 同步锁死,保证 5 折切分点在任何时间跑都一致
> 3. 用 `oxq.optimize.TimeSeriesCV`(不是 sklearn 的 `KFold`):`n_splits=5``expanding=True`
> 4. 传入 `paramset`,让 `cross_validate()` 自动在每折训练集上做 GridSearch
> 5. `n_splits=5` 的依据:ML 教科书默认值,7 年数据上每折约 1.4 年验证集,足够长
> 6. `expanding=True` 的依据:训练集逐折扩大——Step 2 的 Anchored 比 Rolling 更稳定,本步跟随该结论
> 📌 **要点**:金融数据要用时序切分,不能用随机切分。普通 ML 用 sklearn 的 `KFold` 是随机切的(可设 `random_state`);金融场景必须用 `TimeSeriesSplit`——**训练集永远在测试集之前,绝不让“未来”出现在“过去”的训练里**。否则就是把答案偷给了考试中的你——回测看起来漂亮,实盘上一笔笔真金白银地亏,是这条硬约束最常见的代价。这是 q6 比通用 ML 课程多出的一条硬约束。
> 📌 **要点**:接续型 spec 应该做减法。spec-03 比 spec-01 / spec-02 都短得多——它不重新定义参数空间(直接复用 Step 2 的 `wf_paramset`)、不重讲一次什么是过拟合,专心做“5 折结果对比”一件事。**承接型 spec 应当克制重复——前面 spec 讲过的就别再讲;只引入本 spec 真正新的东西。**
#### 起草结果呈现
> **结果呈现**
>
> 1. 用 `cv_result.to_dataframe()` 打印汇总表,再 for 循环逐折打印(训练时段、验证时段、最优参数、样本内/样本外夏普、样本外收益)
> 2. 汇总:`mean_oos_metric()` ± `std_oos_metric()` + 范围 [min, max] + 唯一参数组合数
> 3. 柱状图(figsize 12×6):每折样本内 / 样本外夏普双柱 + 样本外均值水平线
> 4. 标题「5 折交叉验证:每折的样本内 vs 样本外夏普比」
> 5. 打印分析按实际数据动态描述:5 折中几折选了相同参数、变异系数大小、与 Step 2 Anchored 结论是否一致
完整 spec 在 [`specs/spec-03-cross-validation.md`](https://github.com/xingwudao/xquant-learning/blob/main/q6-avoid-overfitting/specs/spec-03-cross-validation.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了 5 折交叉验证的完整结果。
`TimeSeriesCV` 把数据切成 5 折。每折用前面的数据训练(训练集逐渐扩大),紧跟着的一段做验证。每折都独立做网格搜索——选出该折的最优参数,再到验证集上检验。
### 运行结果
5 折交叉验证的逐折详情如表 6-5 所示。
**表 6-5 5 折交叉验证逐折详情**
| Fold | 训练时段 | 验证时段 | 最优参数 | 样本内夏普 | 样本外夏普 | 样本外收益 |
|------|----------|----------|----------|-----------|-----------|-----------|
| 1 | 2017-01-01 ~ 2018-07-22 | 2018-07-23 ~ 2020-02-11 | mom=15, vol=15, freq=5 | 0.87 | 0.69 | 14.37% |
| 2 | 2017-01-01 ~ 2020-02-11 | 2020-02-12 ~ 2021-09-02 | mom=15, vol=10, freq=5 | 0.72 | 0.44 | 10.82% |
| 3 | 2017-01-01 ~ 2021-09-02 | 2021-09-03 ~ 2023-03-25 | mom=20, vol=15, freq=5 | 0.66 | 0.28 | 4.33% |
| 4 | 2017-01-01 ~ 2023-03-25 | 2023-03-26 ~ 2024-10-14 | mom=20, vol=15, freq=5 | 0.53 | 1.56 | 37.83% |
| 5 | 2017-01-01 ~ 2024-10-14 | 2024-10-15 ~ 2026-05-06 | mom=20, vol=20, freq=5 | 0.72 | 1.79 | 67.22% |
**TopNRanking CV 汇总:** 样本外夏普 0.95 ± 0.68,样本外范围 [0.28, 1.79],唯一参数组合 4/5。
5 折中 Fold 3 与 Fold 4 选择了相同的参数(mom=20, vol=15, freq=5),其余三折各不相同——这组结果该怎么读?
每折样本内 vs 样本外夏普对比如图 6-3 所示。
![5 折交叉验证:每折的样本内 vs 样本外夏普比](images/q6-avoid-overfitting/03-cross-validation-5fold.png)
### 结果解读
第三个认知升级:**多次验证比单次验证更可靠。**
先看参数一致性。5 折里只有 Fold 3 和 Fold 4 选了同一组参数(mom=20, vol=15, freq=5),其余三折各不相同——单看 5 折,共识不算强。但有意思的是:这组(mom=20, vol=15, freq=5)也正是 Step 2 里 Anchored Walk-forward 在 15 折里出现 7 次的最频繁参数。两种独立的切法(一种 15 折滑动、一种 5 折时序切分)都把概率最高的位置指向了同一组参数——切法换了,结论没变。
再看表现波动。样本外夏普的范围从 0.28 到 1.79,标准差 0.68。变异系数(标准差/均值)约为 0.72,仍小于 1——说明样本外表现整体相对一致,但波动不容忽视:最好的折(1.79)比最差的折(0.28)高了 6 倍以上。参数对了不代表什么时候都有正收益——策略对市场环境仍然敏感。
交叉验证的价值不是给你一个“最优参数”,而是告诉你**你的参数选择有多可靠**。5 折下来如果共识强,可以更有信心;共识弱,就该接受“参数选择本身有不确定性”,而不是盯着某一折的结果。
**数据说了算:多次验证给出更立体的画像——共识强可以更进一步,共识弱就该承认不确定性。即使参数对了,也别期望每年都有正收益。**
**这就是第三把铲子:交叉验证。** 它让你在多种切法下独立验证同一个结论——一次可能是巧合,多次都指向同一个答案就更可信。前两把铲子告诉你“是不是过拟合”,这一把告诉你“你有多大把握”。
到这里,我们从三个角度检验了参数选择——一次验证(Step 1)、动态验证(Step 2)、多次验证(Step 3)。但还有一种过拟合更隐蔽:不是参数选错了,而是**规则太多了**。
---
## 6.4 规则越多回测越好——是好事吗?
前三步检验的都是参数选择——找参数、更新参数、换切法验证参数。但还有一种过拟合更隐蔽。
回顾第四章:我们给策略加了止损、止盈、移动止损等交易规则。当时的结论是“不是所有规则都有用”。但那时我们没有做样本内/样本外验证。如果在样本内不断加规则、调参数,回测收益一路上升——这是真的在改进策略,还是只是在“凑”出好看的数字?
直觉告诉我们:**规则越多保护越全面,效果应该越好。** 止损防亏损、止盈锁利润、移动止损跟踪趋势——每一条都有道理。
真的吗?试试看——从最简单的策略开始,一层一层加规则,看样本内和样本外分别怎么变。
### 动手实验 4:规则负担
我们一起把这份 spec 写出来。这次重点看两件新东西:**规则数的定义**(“规则数”和“自由度”是两个不同的轴)、**业务约束链的金融语义**(止损 < 止盈 / 移动止损 ≥ 固定止损 / 回撤 ≥ 止损都是领域知识)。
#### 起草上下文 + 任务
> **上下文**:在 `q6-avoid-overfitting.ipynb` 中已有参数优化 + Walk-forward + 交叉验证。前三步检验“参数选得对不对”。本步检验另一种过拟合——规则太多。每加一条规则,就多了一个可调旋钮;旋钮越多,越容易在样本内“调出”好看的结果。
>
> **任务**:在 notebook 新建单元格,做两个堆叠实验展示规则负担。
#### 起草要求:4A 规则堆叠 + 4B 自由度堆叠
把“加规则”和“加自由度”分两个实验跑,**学员才能看出“是规则本身的问题还是自由度的问题”**。
> **要求**
>
> 1. 编写 `run_layer(layer_name, rules, paramset, description)` 辅助函数,复用 Step 1 的样本内/外切分
> 2. **4A 规则堆叠**6 层):基础策略 mom=20/vol=20/freq=10 固定,逐层添加交易规则
> - Layer 0:基础策略(无额外规则)
> - Layer 1+ 优化调仓频率
> - Layer 2+ 优化止损
> - Layer 3+ 优化止盈(约束 sl < tp
> - Layer 4+ 优化移动止损(约束 trail >= sl
> - Layer 5+ 最大回撤风控(约束 max_dd >= sl
> 3. **4B 自由度堆叠**(5 配置):从 1 自由度逐步加到 6 自由度
> - A:仅动量窗口(1 自由度)
> - B+ 波动率(2
> - C+ 调仓频率(3
> - D+ 止损阈值(4
> - E:+ 止盈 + 移动止损(6 自由度,粗网格 [10,20,30] 防止组合数爆炸)
> 4. 每层在样本内选最优 → 在样本外验证
> 5. 约束链按金融语义编码:止损 < 止盈、移动止损 ≥ 固定止损、回撤阈值 ≥ 止损
> 📌 **要点**:q6 第一次显式区分“规则数”和“自由度”两个轴。**4A 数的是“加了几条交易规则”**(止损是一条规则,即便它只有一个 threshold 参数);**4B 数的是“有几个可优化的旋钮”**(一条规则可能贡献 1 个或多个自由度)。两个轴不一样——通用 ML 课程很少这么分,但金融领域必须分:止损是一条独立的业务规则,它在 4A 里占一层;但它的 threshold 在 4B 里只贡献 1 自由度。**学员看到两个实验的曲线分别走出 plateau 拐点,才能确认“既不是规则数的问题,也不是单纯自由度的问题,是两者都到了 plateau”。**
> 📌 **要点**:业务约束链要用金融语义,不是技术约束。`sl < tp`(止损不能高于止盈,否则止损永远先触发)、`trail >= sl`(移动止损不能比固定止损更严,否则后者无意义)、`max_dd >= sl`(组合层风控阈值不应小于个仓止损,否则单仓止损还没触发组合就停了)——三组约束本身就是领域知识。**spec 里的约束链 = 嵌入领域知识的标准位置;学员看 spec 学的不只是参数,还有“为什么这些参数之间该有先后关系”。**
#### 起草结果呈现
> **结果呈现**
>
> 1. 两张汇总表:4A 六层 + 4B 五配置(含自由度、组合数、样本内/样本外夏普、衰减)
> 2. 两张图(各 figsize 12×6,双折线):4A「规则堆叠:样本内 vs 样本外」+ 4B「自由度越多 ≠ 越好」
> 3. 打印分析按实际数据动态描述:4A 找出“超过这一层的规则都是规则负担”的最佳层、4B 找“自欺欺人开始”的拐点
> 4. 引用 Brian Peterson 两段原话:「Rule burden is a form of overfitting」+「Too many rules will make a backtest look excellent in-sample, and may even work in walk forward analysis, but are very dangerous in production.」
完整 spec 在 [`specs/spec-04-rule-burden.md`](https://github.com/xingwudao/xquant-learning/blob/main/q6-avoid-overfitting/specs/spec-04-rule-burden.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了两组实验的完整结果。
**4A:逐层添加规则。** 从最简单的基础策略出发,每层多加一条规则,每层都在样本内做优化。约束条件先把不合理的组合(比如止损大于止盈、移动止损小于固定止损)过滤掉——剩下的层差异才不会被荒谬组合污染。
**4B:逐步增加参数维度。** 从只调 1 个参数(动量窗口)开始,逐步增加可调参数的数量。**自由度(Degrees of Freedom)**——可以调节的参数和规则的数量。自由度越多,优化器的“调节空间”就越大。
### 运行结果
4A 规则堆叠的样本内/样本外表现如表 6-6 所示。
**表 6-6 4A 规则堆叠:每层规则的样本内/样本外夏普与衰减**
| Layer | 描述 | 组合数 | 样本内夏普 | 样本外夏普 | 衰减 |
|-------|------|--------|-----------|-----------|------|
| 0 | 基础(固定参数) | 1 | 0.94 | 0.52 | -0.43 |
| 1 | + 优化调仓频率 | 3 | 1.06 | -0.07 | -1.13 |
| 2 | + 优化止损 | 18 | 1.11 | 0.36 | -0.75 |
| 3 | + 优化止盈 | 57 | 1.33 | 0.15 | -1.18 |
| 4 | + 优化移动止损 | 183 | 1.33 | 0.15 | -1.18 |
| 5 | + 最大回撤风控 | 825 | 1.33 | 0.15 | -1.18 |
4B 指标堆叠(按可优化的自由度分档)的对比如表 6-7 所示。
**表 6-7 4B 指标堆叠:自由度递增下的样本内/样本外夏普**
| 配置 | 自由度 | 组合数 | 描述 | 样本内夏普 | 样本外夏普 | 衰减 |
|------|--------|--------|------|-----------|-----------|------|
| A | 1 | 5 | 动量窗口 | 1.07 | -0.17 | -1.24 |
| B | 2 | 25 | + 波动率窗口 | 1.18 | 0.01 | -1.17 |
| C | 3 | 70 | + 调仓频率 | 1.18 | 0.01 | -1.17 |
| D | 4 | 420 | + 止损阈值 | 1.39 | 0.50 | -0.88 |
| E | 6 | 323 | + 止盈 + 移动止损 | 1.56 | -0.07 | -1.62 |
随层数 / 自由度增加,样本内一路向上、样本外却平甚至下降,对比如图 6-4 所示。
![4A 规则堆叠 与 4B 自由度越多不等于越好](images/q6-avoid-overfitting/04-rule-burden-4a-4b.png)
### 结果解读
第四个认知升级:**规则越多 != 策略越好,简单就是美。**
这是本章最重要的认知冲击。
先看 4A。样本内夏普从 Layer 0 的 0.94 爬升到 Layer 3 的 1.33——每加一条规则,优化器都能在样本内找到更好看的结果。但样本外呢?Layer 0 (基础策略,完全没有额外规则)的样本外夏普是 0.52——这是所有层中最高的!加了调仓频率优化后(Layer 1),样本外直接变成了 -0.07。止损(Layer 2)挽回了一些(0.36),但之后的止盈、移动止损、回撤风控全部无效——样本外停在 0.15 不再提升。
注意 Layer 3-5 三行的样本内、样本外、衰减数字一字不差(IS 1.33 / OS 0.15 / 衰减 -1.18)——这不是抄错。Layer 3 引入了 `sl < tp`、Layer 4 又叠 `trail >= sl`、Layer 5 再叠 `max_dd >= sl`,三条约束把搜索空间一层层锁死。再增加的旋钮被前面的约束吃掉,最后什么都调不动。**这是规则负担的另一面:旋钮多了,相互之间的约束也多,优化器其实根本没机会真正“利用”那几个新自由度。**
也就是说:最简单的基础策略,反而是样本外表现最好的。
再看 4B。从配置 D 到配置 E,自由度从 4 增加到 6,样本内夏普从 1.39 升到 1.56(好了 12%),但样本外从 0.50 跌到 -0.07——**自欺欺人的拐点**出现了。多出来的两个参数让优化器在样本内找到了更好看的结果,但在样本外完全不兑现。
为什么会这样?每条新规则都增加一个**自由度**——可以调节的旋钮。旋钮越多,优化器就有越多的空间去“凑”出好看的回测结果。这不是在发现真规律,而是在记忆噪音。这就是**规则负担(Rule Burden)**——规则本身就是一种过拟合的来源。规则太多会让回测看起来很漂亮,甚至在前推验证中也可能过关,但放到实盘就很危险。
这个判断不是我们一拍脑袋——R 语言量化框架 quantstrat 的作者 Brian Peterson 早就说过:“规则负担本身就是一种过拟合(Rule burden is a form of overfitting)。规则太多会让回测在样本内异常漂亮,甚至在前推验证里也能过关,但放到实盘就非常危险。”每条规则都是给优化器多一个旋钮——旋钮越多,凑出好看回测的余地就越大,对真实未来的把握反而越小。
**铁律:能用 2 个参数解决的问题,不要用 6 个。每增加一条规则,都要问——样本外也改善了吗?如果没有,删掉它。少即是多。**
**这就是第四把铲子:规则负担。** 它告诉你少即是多——不是“加规则越多策略越好”,而是“每加一条规则都要在样本外验证它真的帮上忙”。这把铲子和前三把不同:前三把帮你检查已有的策略,这一把帮你**在设计阶段就少挖坑**。
---
## 6.5 回头看:你刚才做了什么?
**6.5 是“做”——四把铲子的操作流程总图,把四步实验铺成你以后每次接到一份回测都能照着走的检查路径。** 四步下来,我们从四个角度回答了同一个核心问题——“这个参数组合是真的好,还是运气好?”
- **Step 1: 参数优化** → 490 种组合,样本内很好 → 样本外全面衰减 → 过拟合
- **Step 2: Walk-forward** → 模拟“边走边调” → 参数稳定才可信
- **Step 3: 交叉验证** → 5 折独立验证 → 共识强可以更进一步,共识弱就该承认不确定性
- **Step 4: 规则负担** → 规则越多样本内越好 → 样本外不买账 → 简单就是美
从今以后,拿到任何策略的回测结果,可以用一套防过拟合检查清单核对一遍真实性,如表 6-8 所示。
**表 6-8 防过拟合检查清单**
| 检查项 | 怎么做 | 好的信号 | 坏的信号 |
|--------|--------|----------|----------|
| 样本外验证 | 在没见过的数据上跑 | 衰减小 | 衰减大 |
| Walk-forward | 分窗口优化+验证 | 参数跨窗口稳定 | 参数每次都变 |
| 交叉验证 | 多折验证 | 各折结果一致 | 各折差异巨大 |
| 规则负担 | 从简到繁,逐层加 | 样本外同步改善 | 样本内改善但样本外不变 |
| 参数敏感性(第五章) | 扫描参数 | 高原型 | 山峰型 |
加上第五章的参数敏感性,你手上已经有五把检验策略的武器。第一章那次“被自己的回测骗”的肉身记忆,到这里被沉淀成了一份可以挂在墙上的检查清单——这就是“疑”从感觉变成方法的过程,也是飞轮归因层在你手里第一次完整闭环的一小步。下次再接到一份“漂亮”的回测,你不会再赤手空拳,而是知道该问哪几个问题、用哪几把铲子去挖。
更重要的是:这四把铲子不只是让你“能识破自己的过拟合”,它们也让你**敢于做更激进的策略实验**——因为你知道有工具可以兜住底。怀疑回测不是为了畏手畏脚,是为了能放心动手。这就是“疑”和“做”在飞轮里的真正关系:**疑得越深,做得越敢。**
---
## 6.6 本章总结
**6.6 是“概念速查 + 带走的问题”——和 6.5 严格分工:6.5 写“怎么做”,6.6 写“用到了什么 / 还有什么没回答”。**
### 策略进化路径
第五章产出:完整的策略评估方法论 + 体检清单 → Step 1: 参数优化(样本内很好但样本外衰减,过拟合) → Step 2: Walk-forward(模拟定期重新优化,参数稳定性) → Step 3: 交叉验证(多次验证更可靠,但表现差异大) → Step 4: 规则负担(规则/参数越多越危险,简单就是美) → 第六章产出:四把防身工具 + 防过拟合检查清单 + “怀疑回测”的态度
### 概念速查表
本章涉及的核心概念汇总如表 6-9 所示,方便随时回查。
**表 6-9 第六章核心概念速查**
| 概念 | 含义 | 类比 |
|------|------|------|
| 参数优化 / 网格搜索 | 把所有可能的参数组合都试一遍,选出表现最好的 | 从几千所大学里挑分数最高的 |
| 样本内(训练集) | 用来优化参数的那段数据 | 模拟考试的试题 |
| 样本外(测试集) | 用来验证参数的另一段数据,优化时没见过 | 真正的考试 |
| 过拟合 | 样本内表现很好但样本外大幅衰减 | 背了真题成绩好,但没学会真知识 |
| Walk-forward 分析 | 在历史上模拟“定期重新优化”的过程 | 每学期重新选课本复习 |
| 滚动式 Walk-forward | 训练窗口固定大小,向前滑动 | 只翻最近两年的课本 |
| 锚定式 Walk-forward | 训练窗口从起点开始,越来越大 | 从高一课本开始全部复习 |
| 参数稳定性 | 好参数在不同窗口中保持一致 | 不管怎么考,你的强项科目都一样 |
| 交叉验证 | 用多种切法验证同一个策略 | 做实验要重复多次才可信 |
| 时间序列分割 | 保持时间顺序的交叉验证 | 不能用未来考题“复习”过去 |
| 规则负担 | 每条新规则增加过拟合风险 | 考试作弊手段越多,被抓的风险越大 |
| 自由度 | 可以调节的参数和规则数量 | 作弊的旋钮越多,越容易凑出高分 |
### 本章核心认知
走完四步实验后,本章最值得带走的八条认知如表 6-10 所示。
**表 6-10 第六章核心认知**
| 认知 | 来源 |
|------|------|
| 参数优化是双刃剑——优化后必须在新数据上验证 | Step 1 |
| 样本内的好成绩不算数 | Step 1 |
| 参数稳定性比参数最优更重要 | Step 2 |
| 好参数跨窗口稳定,坏参数到处变 | Step 2 |
| 多次验证比单次验证更可靠 | Step 3 |
| 不同验证方法指向同一个参数 → 可信度高 | Step 3 |
| 规则越多 != 策略越好 | Step 4 |
| 能用 2 个参数解决的问题,不要用 6 个 | Step 4 |
### 带走的问题
我们已经知道怎么评估策略(第五章)、怎么防过拟合(第六章)。回测世界的问题基本解决了。
但回测终究是模拟。在回测里,你可以按任何价格、在任何时间成交。真实世界里呢?
- 你想买的时候,市场上有足够的卖盘吗?
- 你下单到成交之间,价格会不会已经变了?
- 回测假设的手续费,和真实交易一样吗?
**从回测到实盘,还有多少坑在等着你?** → 第七章
**你在第一章撞过那堵墙,第五章手里有了体检表,这一章把怀疑变成了肌肉记忆。下面一章,回测的世界要让位给真实成交的世界——你将带着这四把铲子,走进实盘。**
> 本章所有代码的可运行版本见 `notebooks/q6-avoid-overfitting.ipynb`
+581
View File
@@ -0,0 +1,581 @@
# 第七章:真的下单:跨到实盘
第六章你建立了一套完整的防线——参数优化、前推验证、交叉验证、规则负担检查。策略通过了所有考验,你信心满满。
**明天就去买!**
但打开券商 APP,你愣住了。策略说“沪深300ETF 权重 35%,纳指100ETF 权重 25%,黄金ETF 权重 40%”——可 APP 上只能输入股数和价格。35% 是多少股?按什么价格下单?
> **📌 小词典:券商 / 下单 / 成交价**
>
> - **券商** —— 你委托买卖股票的中介机构(类似机票代理人)。
> - **下单** —— 告诉券商“买/卖什么、多少股、什么价格”(把订单递给售票窗口)。
> - **成交价** —— 券商真正帮你买到 / 卖出时的价格(想 ¥4.27 买、实际成交 ¥4.30、差额就是滑点)。
回测引擎在内部用精确的浮点数分配权重——你以为回测结果就是实盘结果。但中间隔了一整条执行链:权重要翻译成股数,价格在不停变化,下单要花佣金和印花税,成交价和预期价格总有差距。这些差距不是抽象的——本章后面你会亲眼看到,仅仅因为成交价模式不同,同一笔策略最好和最差之间累计差出 6.64%;再叠加佣金 + 印花税 + 滑点,四年累计又被侵蚀 5.72%。这一章就是要看清这些“看不见的钱”都去哪了。
### 路线图
**选什么标的(第二章)→ 每个买多少(第三章)→ 什么时候买卖(第四章)→ 怎么验证有效(第五章)→ 如何避免自欺欺人(第六章)→ 如何真正执行(第七章本章)**
**这一章你跨进了飞轮的“执行层”——主练动作回到“做+看”:把“权重 → 订单 → 成交”这条执行链上的每一步都做成可重复的规则,再用执行报告“看”出回测和实盘之间的差距。** 本章从“迫不及待想操作”的状态出发,在操作中暴露四个问题,四步对应的方法如表 7-1 所示。
**表 7-1 第七章四步问题与方法对照**
| 步骤 | 问题 | 方法 |
|------|------|------|
| Step 1 | 35% 是多少股? | 目标权重 → 具体订单 |
| Step 2 | 价格变了怎么办? | 成交价压力测试 + 成本层叠 |
| Step 3 | 回测 vs 实盘,差距有多大? | Alpaca 模拟交易 |
| Step 4 | 执行得对不对? | 执行报告 + 逐笔对账 |
> **📌 小词典:实盘 vs 模拟盘**
>
> - **实盘** —— 行业惯例统称把策略接入交易接口、订单走真实撮合的运行模式。
> - **本章特指** —— Alpaca 模拟盘(paper trading),订单走 Alpaca 的撮合环境,不涉及真金白银。本章后文提到的“实盘”都是这个意思。
> - **真正用钱下单** —— 需要按所在市场的开户、入金、合规流程独立办理(A 股的对照见拓展阅读)。
(操作流程见前言“怎么使用这本书”。)
---
## 7.1 35% 是多少股?
策略给出的是权重——沪深300ETF 35%、纳指100ETF 25%、黄金ETF 40%。但券商 APP 只接受“买多少股、什么价格”。
你需要一个翻译器:把目标权重翻译成具体的订单。
这就像你规划了一次旅行预算——“交通占 35%,住宿占 25%,吃饭占 40%”——但到了订票网站,你只能输入具体金额。而且不能精确到分——火车票是 139 元,不是 138.52 元。凑成整数后,实际比例和计划比例总有些差距。
买股票也一样:你只能买 100 股的整数倍。8100 股沪深300ETF 不会刚好等于 35%。
### 动手实验 1:目标权重 → 具体订单
我们一起把这份 spec 写出来。这次重点看三件新东西:**实盘金额必须用 Decimal**、**lot_size 是市场制度的硬约束**,以及**单点 → 横切 → 网格的三段递进**。
#### 起草上下文 + 任务
q7 的 spec 必须从第一行就交代「为什么是实盘场景」——上下文用调仓日开场,把学员从「浮点数权重」的舒适世界拉到「券商 APP 只接受整数股」的现实。
> **上下文**:学员跑完第六章,策略通过了所有检验。今天是调仓日,风险平价信号给出目标权重——沪深300ETF 35% / 纳指100ETF 25% / 黄金ETF 40%。打开券商 APP,只能输入股数和价格。
>
> **任务**:在 `q7-execution.ipynb` 中用 `generate_orders()` 把目标权重翻译成具体订单,并对比 A 股(lot_size=100)和美股(lot_size=1)的取整偏差。
> 📌 **要点**:实盘金额计算必须用 `Decimal` 而非 float。spec 在 import 段写 `from decimal import Decimal`,常量定义里所有权重、价格、本金都用 `Decimal("0.35")` 字符串构造——浮点累积误差在大额账户上会到分到角的差异,这是 q7 起的第一条领域规矩。
#### 起草要求:lot_size 与三段递进
要求段把市场规则当不变量写死——A 股 100 股一手、美股 1 股一手——这不是 oxq 的设计选择,是真实世界的制度差异。然后用三段递进暴露问题。
> **要求**
>
> 1. 常量:A 股标的 `("510300.SS", "513100.SS", "518880.SS")`、目标权重 35%/25%/40%、模拟价格 ¥4.27/¥1.72/¥8.04(全用 Decimal 字符串构造)
> 2. 第一步——空仓 + 10 万人民币 + `lot_size=100`,生成 A 股订单清单
> 3. 第二步——A 股(lot_size=100vs 美股(SPY/QQQ/GLDlot_size=1)的对比表
> 4. 第三步——五档资金量(1 万 / 5 万 / 10 万 / 50 万 / 100 万)× 两市场的网格扫描
> 5. 对比柱状图(figsize 14×5):左图最大权重偏差、右图现金残余占比;A 股红、美股蓝
> 📌 **要点**`lot_size=100` 是 A 股的硬约束(100 股一手是市场规则),`lot_size=1` 是美股的硬约束。把这种「制度差异」参数化进 spec,学员才能看清真相——不是 oxq 在挑选实现,是市场在挑选规则。
> 📌 **要点**:三段递进(单点 → 横切 → 网格)是 q7 整章的节奏——第一步跑通接口、第二步两市场对比、第三步五档资金 × 两市场。先看到现象、再看到对比、再看到规律,避免一上来用网格扫描压垮 0 基础学员。
#### 起草结果呈现
> **结果呈现**:订单表格 + A 股 vs 美股对比表 + 五档资金量两张表 + 对比柱状图;打印投入金额和剩余现金,过渡句「订单有了,但按什么价格下单?」自然带到下一节。
完整 spec 在 [`specs/spec-01-order-generator.md`](https://github.com/xingwudao/xquant-learning/blob/main/q7-execution/specs/spec-01-order-generator.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了订单生成的完整结果。`generate_orders()` 就是那个翻译器——输入目标权重、当前持仓、最新价格和总资金,输出具体的订单清单。`lot_size=100` 表示 A 股必须买 100 股的整数倍。A 股 ETF 单价虽然低(几元钱),但 100 股一手的约束让偏差远大于美股。
### 运行结果
先看 ¥100,000 的订单清单,如表 7-2 所示。
**表 7-2 ¥100,000 资金下 A 股 ETF 的订单清单与取整偏差**
| 标的 | 方向 | 目标股数 | 预估金额 | 目标权重 | 实际权重 | 偏差 |
|------|------|----------|----------|----------|----------|------|
| 沪深300ETF | BUY | 8100 | ¥34,587.00 | 35.0% | 34.6% | -0.41% |
| 纳指100ETF | BUY | 14500 | ¥24,940.00 | 25.0% | 24.9% | -0.06% |
| 黄金ETF | BUY | 4900 | ¥39,396.00 | 40.0% | 39.4% | -0.60% |
投入金额:¥98,923.00 / 剩余现金:¥1,077.001.08% 的资金没有被使用)
A 股 100 股整手约束让偏差非常明显——黄金 ETF 想要 40%,实际只做到 39.4%,差了 0.6 个百分点。有 1,077 元趴在账户里不产生任何收益。
再看 A 股 vs 美股的对比(同样是 10 万本金),如表 7-3 所示。
**表 7-3 A 股 vs 美股 10 万本金的取整偏差对比**
| 目标权重 | A 股标的 | 股数 | 实际权重 | 偏差 | 美股标的 | 股数 | 实际权重 | 偏差 |
|----------|----------|------|----------|------|----------|------|----------|------|
| 35.0% | 沪深300ETF | 8100 | 34.6% | -0.41% | SPY | 59 | 35.0% | -0.01% |
| 25.0% | 纳指100ETF | 14500 | 24.9% | -0.06% | QQQ | 48 | 24.6% | -0.41% |
| 40.0% | 黄金ETF | 4900 | 39.4% | -0.60% | GLD | 134 | 40.0% | -0.00% |
A 股现金残余:¥1,077.001.08% / 美股现金残余:$416.520.42%
A 股的偏差明显更大,现金残余也翻了一倍多。这就是 `lot_size=100` 整手约束的代价。
资金量的影响更直观,如图 7-1 所示。
![资金量越大,取整偏差越小;A 股偏差远大于美股](images/q7-execution/01-rounding-deviation.png)
左图:A 股(红色)在 1 万人民币时偏差高达 7.8%,即使 10 万人民币也有 0.6%。而美股(蓝色)在各资金量下的最大权重偏差都很小,1 万美元时偏了 5.3%,10 万美元就只有 0.4%。
右图:现金残余的规律一样——资金量越小,越多钱闲置在账户里。A 股 1 万人民币的账户,9.6% 的资金什么都没买到。
### 结果解读
第一次看到“理想”和“现实”的差距。
回测引擎在内部用浮点数精确分配权重——沪深300ETF 就是精确的 35.000%。但你去下单时,只能买 100 股的整数倍。8100 股沪深300ETF 的实际权重是 34.6%,不是 35.0%。
**取整偏差是系统性的**——每次调仓都会产生,日积月累就是真实的收益偏差。A 股的 100 股整手约束让偏差尤其明显。小资金账户(比如 10 万人民币做三只 ETF 的风险平价)可能连基本的权重分布都无法实现。
还有一笔“隐形损失”:**现金残余**。那些因为凑不成整股而趴在账户里的现金,一分钱收益都没产生。
但这只是第一个问题。订单生成了,按什么价格下单?
---
## 7.2 价格变了怎么办
回测里你只看到“权重”,但实盘里每一笔下单都要付钱给三个人——这是先建立预算感再做实验的前提。
**三笔执行成本你必须先认清:**
- **佣金(Commission** —— 付给券商的服务费,A 股通常是万分之一到万分之三,最低 5 元;美股按笔或按股;港股约万分之五。本章 SimBroker 默认填的是万分之一。
- **印花税(Stamp Duty** —— 付给政府的,A 股**卖出**时收千分之一(买入不收);港股买卖双向各 0.13%;美股没有印花税。第二章拓展阅读已经预告过,本章你将在执行报告里第一次亲眼看到它把钱怎么慢慢吃掉。
- **滑点(Slippage** —— 你下单时看到的价格和最终成交的价格之间的差。看似小,每笔 0.1%-0.3%,但乘以一年几十笔交易,累积下来不可忽视。这一节就是要让你看到它有多大。
带着这三个成本的画面感,我们再回到具体场景:Step 1 生成了订单清单——“买沪深300ETF 8100 股,按 ¥4.27”。但 ¥4.27 是昨天的收盘价。你现在下单,价格可能已经涨到 ¥4.30 了,也可能跌到 ¥4.24。
按哪个价格买?这个差异有多大影响?
回测引擎默认你在收盘价精确成交——但你不可能在收盘的那一瞬间下单。实际上,你看到收盘数据后才能做决策,最早也是第二天才能交易。那第二天的价格和昨天收盘价能差多少?
还是那张订票网站——昨晚你查到火车票 139 元,今早决定下单,价格可能已经涨到 145 元,也可能降到 132 元。同一张票,看价格的时间和真正付款的时间隔开一夜,花的钱就不一样。
我们用五种成交价假设做一次压力测试。
### 动手实验 2:成交价压力测试
我们一起把这份 spec 写出来。这次重点看两件新东西:**带宽即认知**(用 fill_between 把执行不确定性变成可见的橙色带)和**积分式归因**(成本拆四层逐层加,把“扣了 5%”分解成佣金/印花税/滑点各扣多少)。
#### 起草上下文 + 任务
> **上下文**:在 `q7-execution.ipynb` 中已有 Step 1 的订单生成实验。Step 1 生成了订单「买沪深300ETF 8100 股,按 ¥4.27」——但这是昨天收盘价,现在下单价格已经变了。按哪个价格买?差异有多大影响?
>
> **任务**:用 `FillPriceMode` 的五种模式做压力测试,再用四层成本层叠展示佣金、印花税和滑点的持续侵蚀。
> 📌 **要点**:金融术语在 spec 内最低限度解释。q7 的金融黑话密度高(成交价、滑点、印花税折算、归一化净值、夏普比率),spec 写作要假设学员单读 spec 也能看懂——在要求段开头加一段术语简释,不让 spec 完全依赖 book 才能读懂。
#### 起草要求:五种模式与四层成本
要求段同时压缩了「五种 FillPriceMode 全跑」和「四层成本层叠」两条主线。第一条让学员看到执行不确定性的带宽;第二条让学员看到成本归因。
> **要求**
>
> 1. 数据:标的 `510300.SS / 513100.SS / 518880.SS`,起始 `2021-01-01`,用 `YFinanceDownloader` + 本地缓存
> 2. 策略沿用 q4 产出参数:`RebalanceFrequencyRule(interval_days=21)` + `StopLossRule(threshold=0.10)`,封装成 `make_strategy()` / `make_rules()` 辅助函数
> 3. 五种 `FillPriceMode`CLOSE / MID / NEXT_OPEN / NEXT_HIGH / NEXT_LOW)各跑一次完整回测,佣金 `PercentageFee(rate=Decimal("0.0006"), min_fee=Decimal("5"))`(A 股万一佣金 + 印花税折算双边约万五,合计约万六)
> 4. 净值曲线图(figsize 12×8,上下两图高度比 3:1,`layout='constrained'`):上图五条归一化净值(起点=100),下图各模式与 CLOSE 的差距折线 + NEXT_HIGH / NEXT_LOW 之间用橙色 `fill_between` 填充
> 5. 四层成本层叠(NEXT_OPEN 模式):无成本 → 加万一佣金 → 加佣金+印花税(万六) → 加佣金+印花税+滑点(千一)
> 6. 成本层叠图(同样 12×8 / 3:1):上图四条归一化净值,下图成本侵蚀折线(三条差距线 ≤ 0,正值说明计算有误)
> 📌 **要点**:**带宽即认知**——HIGH 和 LOW 之间的橙色填充把“执行不确定性”从抽象概念变成可视化的带宽。学员第一眼看到的不是数字,是带宽。把“画带宽”作为视觉契约写进 spec,胜过千言文字。
> 📌 **要点**:**积分式归因**——成本拆四层逐层加,每层一次完整回测、四条净值曲线对比。学员看到的不是「成本扣 X%」一个数字,而是「佣金扣多少、印花税扣多少、滑点扣多少」的归因分解。这是 q5/q6 已建立的“分布优于均值”思想在执行场景的延续。
> 📌 **要点**:印花税近似要主动声明“用 PercentageFee 折算为双边费率,实际略有差异”。这是教学诚实——不把模型当真理,承认精度边界。q7 涉及真实金额的 spec 都要遵循这条规矩。
#### 起草结果呈现
> **结果呈现**:五种模式对比表(5 行含收益/夏普)+ 净值曲线图(含执行不确定性带)+ 成本层叠表(4 行成本逐层递增、收益逐层递减)+ 成本层叠图(含成本侵蚀曲线);过渡句「这些都是估算,真实执行到底差多少?」带到下一节。
完整 spec 在 [`specs/spec-02-fill-price.md`](https://github.com/xingwudao/xquant-learning/blob/main/q7-execution/specs/spec-02-fill-price.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了五种成交价模式的对比结果和成本层叠分析。
`FillPriceMode` 有五种模式:`CLOSE`(收盘价,回测默认假设)、`NEXT_OPEN`(次日开盘价,最接近实盘)、`NEXT_HIGH`(次日最高价,最差买入场景)、`NEXT_LOW`(次日最低价,最好买入场景)、`MID`(最高与最低均价)。同一个策略跑五遍,唯一的区别是“假设在什么价格成交”。
成本层叠则用四层配置逐层加码:无成本 → 加佣金(万一) → 加佣金 + 印花税 → 加佣金 + 印花税 + 滑点。用“次日开盘价”作为基础,因为这最接近实盘。**滑点(Slippage)** 是你下单时价格因市场波动而偏离预期的部分——就像你在拥挤的菜市场买菜,问价时 5 块,等你掏钱时变 5.1 了。印花税是 A 股特有的成本——每次卖出时收取 0.05%,这里用 PercentageFee 近似折算为双边费率。
### 运行结果
先看五种成交价模式的对比,如表 7-4 所示。
**表 7-4 五种成交价模式下的回测指标对比**
| 成交价模式 | 累计收益率 | 年化收益率 | 最大回撤 | 夏普比率 | 交易次数 |
|------------|------------|------------|----------|----------|----------|
| 收盘价(回测理想) | 83.22% | 12.90% | -15.45% | 1.15 | 184 |
| 中间价 | 85.62% | 13.19% | -14.34% | 1.18 | 184 |
| 次日开盘价(最接近实盘) | 80.00% | 12.50% | -15.98% | 1.12 | 184 |
| 次日最高价(最差买入) | 81.02% | 12.62% | -16.13% | 1.14 | 185 |
| 次日最低价(最好买入) | 76.58% | 12.06% | -17.19% | 1.08 | 184 |
五条净值曲线和执行不确定性区间如图 7-2 所示。
![五种成交价模式的净值曲线和执行不确定性区间](images/q7-execution/02-fill-price-modes.png)
先看上图的五条净值曲线——起点都是 100,终点却不一样。同一个策略,同样的买卖决策,仅仅因为成交价格不同,累计收益差了好几个百分点。
再看下图——橙色填充区域就是 NEXT_HIGH 和 NEXT_LOW 之间的差距,叫**执行不确定性区间**。这个区间越宽,说明你对执行结果的控制力越弱。
收盘价(回测理想)83.22%;次日开盘价(最接近实盘)80.00%,比理想少了 3.22%;次日最高价(最差情况)81.02%,比理想少了 2.20%;次日最低价(最好情况)76.58%,比理想少了 6.64%。最好和最差之间的差距:4.45%。
再看成本层叠,如表 7-5 所示。
**表 7-5 成本层叠对回测指标的影响**
| 成本层级 | 累计收益率 | 年化收益率 | 夏普比率 |
|----------|------------|------------|----------|
| 无成本 | 82.44% | 12.80% | 1.15 |
| + 佣金(万一) | 80.92% | 12.61% | 1.13 |
| + 佣金 + 印花税 | 80.00% | 12.50% | 1.12 |
| + 佣金 + 印花税 + 滑点 | 76.72% | 12.08% | 1.09 |
成本层叠的净值曲线和侵蚀分解如图 7-3 所示。
![成本层叠:每一层都在侵蚀收益](images/q7-execution/03-cost-layers.png)
上图四条线逐层下移——绿色(无成本)在最上面,红色(全成本)在最下面。下图的成本侵蚀曲线持续下行,说明成本的影响不是一次性的,而是每次交易都在积累。
成本侵蚀总计:5.72%(其中佣金 1.52%,印花税 0.92%,滑点 3.28%)。
184 笔交易,佣金吃掉了 1.52%,印花税吃掉了 0.92%,滑点吃掉了 3.28%。合计 5.72% 的收益被交易成本侵蚀。
### 结果解读
五条净值曲线讲了两件事。
**第一,回测的收盘价假设是最乐观的。** 回测默认你能在收盘价精确成交——但实际上你不可能在收盘的那一瞬间下单。次日开盘价是更现实的假设,而你的实际成交价会落在次日最低价和最高价之间的某个位置。
NEXT_HIGH 和 NEXT_LOW 之间的带状区域就是“执行不确定性”——**同一个策略决策,仅仅因为执行时机不同,结果就有这么大的差距。** 这个差距不是策略的问题,而是执行的问题。
**第二,成本是持续的侵蚀。** 佣金、印花税和滑点不是一次性的损失,而是每次交易都在发生。一年几十次调仓下来,成本层层叠加,侵蚀掉的收益比你想象的多得多。
你刚才看到的就是 A 股的真实成本结构。佣金万一是基础,印花税在每次卖出时额外扣 0.05%,滑点更是无法避免。三者叠加,四年累计侵蚀了 5.72% 的收益——这就是为什么低频策略往往比高频策略更适合个人投资者。
但以上都是“估算”——用不同的成交价模式模拟出来的。真实执行到底差多少?
---
## 7.3 回测 vs 实盘,差距有多大
把前两步的具体数字摊在桌面上:佣金 + 印花税 + 滑点,**四年累计侵蚀 5.72% 的收益**;同一笔订单按“最好”和“最差”成交价跑,**累计收益差距高达 6.64%**。这些不是抽象的“差距”——是真金白银从你的账户里走掉。
**这一节标的物切换为美股 ETF(SPY、QQQ、GLD)。** 前两个实验沿用 A 股 ETF(沪深300、纳指100、黄金),和 第二、六章 保持一致;从这一节开始换成美股 ETF——因为 open-xquant 内置了 Alpaca 美股模拟交易接口,可以让你亲手体验真实下单。放心——美股和 A 股面对的执行问题完全一样(取整偏差、滑点、成本),只是 A 股有 100 股整手约束和印花税,某些问题更严重。解读中会详细桥接。
### A 股桥接:为什么用美股练手,结论却双向适用
你接下来要上的 Alpaca 是美股模拟盘——选它是因为接入简单(注册即用,无需开户、无需入金)。但**美股看到的所有问题,A 股一模一样、且大多更严重**:
1. **取整偏差** —— A 股 100 股整手,偏差比美股大得多(美股可买 1 股)
2. **价格滑点** —— A 股同样存在,开盘竞价和收盘竞价时段尤其明显
3. **交易成本** —— A 股还要额外付印花税(卖出 0.05%)
4. **执行落差** —— 同样的策略,A 股的执行落差通常比美股更大
**“用美股练手 + 搬回 A 股操作”是这一节的实操路径**——美股 Alpaca 让你在不用开户的情况下亲手感受成交,看到的所有结论搬回 A 股完全成立。当你回到 A 股券商 APP 手动操作时,建议你:
- 先用 `generate_orders()` 生成订单清单(Step 1 的 SOP),不要凭感觉手敲数量
- 定期对比回测净值和实际账户净值,监控执行落差是否在预算内
- 如果执行落差持续放大,回到本节诊断三问——是成本问题,还是执行时机问题?
现在,我们让策略接入 Alpaca 模拟交易平台,真正跑一遍。然后把回测净值和模拟实盘净值画在同一张图上——差距一目了然。
**关键概念:** Engine 和 Strategy 完全不变,只是把数据源和交易接口换了。回测用 SimBroker + LocalMarketDataProvider,实盘用 LiveBroker + AlpacaMarketDataProvider。策略逻辑一行都不用改。
这就像你做菜——同一份菜谱,在家里用燃气灶做和在户外用柴火做,食材和步骤一样,但火候控制不同,最终味道总有差异。策略是“菜谱”,交易接口是“灶”。
### 动手实验 3Alpaca 模拟交易
为了体验真实交易摩擦,本节使用美股 ETF(SPY、QQQ、GLD)。这是因为 open-xquant 内置了 Alpaca 美股模拟交易接口,可以让你亲手感受真实下单——提交订单、等待成交、查看回报,而不是在历史数据上模拟成交。
要使用 Alpaca 模拟盘,你需要:
1. 在 alpaca.markets 免费注册一个账号
2. 在账号设置中生成 API Key 和 Secret Key
3. 在项目根目录创建 `.env` 文件,填入:
```
ALPACA_API_KEY=你的Key
ALPACA_SECRET_KEY=你的Secret
```
4. 把上面的步骤告诉 AI 助手,它会帮你完成配置
我们一起把这份 spec 写出来。这是 q7 最有挑战的一份——同时要让有 Alpaca 账号的学员体验真实下单、让没账号的学员能完成完整流程、还不能把 API key 写进任何文件。重点看三件新东西:**API key 三重防线**、**Part A / B-1 / B-2 三段切分**,以及**外部依赖的兜底设计**。
#### 起草上下文 + 任务
> **上下文**:在 `q7-execution.ipynb` 中已有 Step 1 的订单生成(A 股 ETF)+ Step 2 的成交价压力测试(A 股 ETF)。前两步暴露了取整偏差、滑点和成本,但都是「估算」。现在切换为美股 ETF(SPY/QQQ/GLD),接入 Alpaca 模拟交易平台让策略真正跑一遍。
>
> **任务**:完成三段不可省略的演示——Part A:SimBroker 回测基准;Part B-1Alpaca 数据源做历史回测对比数据源差异;Part B-2:LiveBroker 提交真实订单到 Alpaca 模拟盘。无 Alpaca 账号时提供替代方案。
> 📌 **要点**:A 股 → 美股的标的切换要诚实交代——open-xquant 当前只内置 Alpaca 美股 paper trading,国内券商接口尚未集成。spec 上下文段必须说清这个限制,避免学员误解「只有美股才有 paper trading」。
#### 起草要求:API key 三重防线 + 三段切分
要求段把 API key 处理设计成三重防线:环境变量、try/except 兜底、验证段安全断言。一道防线漏了还有两道兜着。
> **要求**
>
> 1. **Part A**YFinance 下载 SPY/QQQ/GLD 起始 `2021-01-01`,用 `make_us_strategy()` / `make_us_rules()` 跑收盘价 + 佣金回测,结果赋给 `result_sim`
> 2. 定义公共画图函数 `plot_sim_vs_live(norm_sim, norm_live, title, ...)`figsize 12×83:1 子图,`layout='constrained'`,下图差距填充)
> 3. 检测环境变量 `os.environ.get("ALPACA_API_KEY")`,全部 Alpaca 操作包 try/except,失败自动转兜底
> 4. **Part B-1**`AlpacaMarketDataProvider(feed="iex")` + `Engine.setup() / step()` 逐 bar 执行 + SimBroker,对比 YFinance vs Alpaca IEX 的数据源差异
> 5. **Part B-2**:核心动作 a) 用 `AlpacaClient` 读持仓 + 账户权益;b) `generate_orders(..., lot_size=1)` 算调仓;c) `LiveBroker(paper=True)` 逐笔提交;d) 等待 3 秒后查成交;e) 打印滑点对比表;f) `live_broker.close()` + 打印最终持仓。边界条件:无需调仓时跳过下单、市场关闭时查订单状态
> 6. **无 Alpaca 替代方案**`SimBroker(fill_price_mode=NEXT_OPEN)` + 滑点 + 佣金模拟实盘,同样赋给 `result_live` 供 Step 4 使用
> 📌 **要点****API key 三重防线**——① 环境变量 `os.environ.get("ALPACA_API_KEY")` 不传参;② 整段包 try/except,连接失败自动切兜底;③ 验证段显式断言「不包含任何硬编码的 API Key」。q7 是学员第一次面对真实可花钱的接口,即使是 paper trading,养成「绝不硬编码 key」的习惯是关键。这三层缺一不可,可作为「涉外部 API 的 spec」的安全模板。
> 📌 **要点****Part A / B-1 / B-2 三段切分**承接 spec-02 的「积分式归因」——Part A 建立基准、Part B-1 揭示数据源差异、Part B-2 展示真实下单。让学员看到两层执行落差(数据层 + 真实成交层),而不是一笔糊涂账。
> 📌 **要点**:**外部依赖的兜底设计**是 q7 唯一的「双轨 spec」——q1-q6 都假设学员能跑通核心流程,但 q7 必须接受「少数学员没 Alpaca 账号」的现实。无账号路径用 NEXT_OPEN + 滑点的 SimBroker 模拟,让没账号的学员也能完整跑完这一节。涉及外部账号 / 网络 / 付费服务的 spec 应推广这种设计。
#### 起草结果呈现
> **结果呈现**:有 Alpaca 时输出必须含三部分(SimBroker 基准累计收益 / 数据源对比图 + Alpaca IEX 累计收益 / LiveBroker 实盘演示标题 + 当前持仓 + 调仓计划 + 最终持仓);无 Alpaca 时显示模拟实盘对比图;`result_live` 必须赋值供 Step 4 使用。
完整 spec 在 [`specs/spec-03-alpaca-paper.md`](https://github.com/xingwudao/xquant-learning/blob/main/q7-execution/specs/spec-03-alpaca-paper.md)——复制给 AI,弹窗选「允许」。如果你跳过 Alpaca 配置,下一段代码会自动切到 SimBroker 模拟实盘——不影响后续阅读。
AI 助手执行完毕后,你的 notebook 里应该出现了回测 vs 实盘的对比结果。
实验分三部分:Part A 用收盘价 + 佣金跑标准回测,作为“理想基准”;Part B-1 把行情数据从 YFinance 换成 Alpaca IEX,看“数据源不同”本身能带来多大差异;Part B-2 用 **LiveBroker** 把订单真正发送到 Alpaca 交易所——SimBroker 在本地模拟成交,而 LiveBroker 等待交易所返回真实成交回报。即使是 Paper Trading(模拟盘),订单也经过了交易所的撮合引擎,成交价是市场真实报价。
### 运行结果
先看数据源差异,如表 7-6 所示。
**表 7-6 YFinance vs Alpaca IEX 同期数据源回测差异**
| 指标 | 数值 |
|------|------|
| SimBroker 回测(YFinance 数据) | 累计收益 118.59%184 笔交易 |
| Alpaca IEX 数据 + SimBroker 回测 | 累计收益 112.66%187 笔交易 |
| 数据源差异 | 5.93% |
两个数据源的净值曲线对比如图 7-4 所示。
![同一策略,不同数据源](images/q7-execution/04-data-source-comparison.png)
先看上图——蓝色线(YFinance 数据)和红色线(Alpaca IEX 数据)走势相似,但终点差了将近 6 个百分点。下图的执行落差从 2022 年开始持续扩大。
同一个策略、同一个 SimBroker,仅仅因为行情数据来自不同供应商,结果就差了 5.93%。你以为回测结果是“确定的”,其实连数据本身都有不确定性——不同数据源的收盘价可能有微小差异(四舍五入方式、调整方法、采样时间点),四年累积下来就是这个数字。
再看 LiveBroker 的真实订单提交。这次运行时账户里已经有了之前建好的仓位(GLD 83 股、QQQ 41 股、SPY 51 股),策略只需要做小幅调整——卖出 1 股 QQQ。
订单提交后,由于市场已关闭,订单状态为“accepted”(已接受,等待下次开盘执行)。这就是真实交易的常态——你不能随时成交,要等市场开门。如果你在开盘时间运行,订单通常会在几秒内成交,成交价和预期价格之间的差距就是真实的滑点。
### 结果解读
这一步揭示了两层执行落差。
**第一层:数据源差异。** 同一个策略、同一个 SimBroker,仅仅因为行情数据来自不同的供应商(YFinance vs Alpaca IEX),回测结果就不一样。不同数据源的收盘价可能有微小差异(四舍五入、调整方式、采样时间点不同)。你以为回测结果是“确定的”,其实连数据本身都有不确定性。
**第二层:真实执行差异。** 用 LiveBroker 向 Alpaca 模拟盘提交真实订单后,成交价和你预期的收盘价总是不一样。这就是**执行落差(Implementation Shortfall**——从“策略决定买”到“真正买到”之间的距离。差距来自三个方面:
1. **成交价偏差**——回测用收盘价,实盘在某个时刻成交,价格已经变了
2. **交易成本**——佣金、印花税(A 股)
3. **市场冲击**——你的买单本身推高了价格(大资金才明显,小资金可忽略)
关键认知:**执行落差像每次下单都要交一笔过路费——不是 bug,是从回测到实盘的必经收费站。** 关键是知道这费多大、有没有突然涨价;你需要做的不是消除它(不可能),而是监控它的浮动是否在合理范围、在评估策略时预留这个空间。
还有一个重要的设计细节值得注意:**Engine 和 Strategy 一行都没改。** 从回测切换到模拟实盘,只是换了数据源和交易接口。策略逻辑和执行环境完全解耦——这意味着你在回测中验证过的策略,可以直接接入实盘,不用重写任何代码。
差距看到了,但具体哪笔交易差得多、哪笔差得少?
---
## 7.4 怎么知道执行得对不对
Step 3 让你看到了整体的执行落差。但“整体差 6%”这个数字不够——你需要知道:
- 具体哪笔交易差得最多?
- 是特定标的的问题,还是特定日期的问题?
- 大部分交易的执行质量如何?
这就是**执行报告**的作用:逐笔对账,找到问题。
你可能觉得“对账”这个词很陌生,但你每天都在做类似的事——收到外卖后核对订单:点了三道菜,到了没有?价格和下单时一样吗?少了一道菜你会找客服。执行报告就是交易世界的“对账单”。
### 动手实验 4:执行报告
我们一起把这份 spec 写出来。这是 q7 的收尾 spec——前三份让学员看到执行落差「整体上」有多大,这份让学员**逐笔对账**找到落差的具体来源。重点看两件新东西:**反平均设计**(最贵的一笔比平均更值钱)和**实盘 vs 回测的对照表**(双图各回答一个问题)。
#### 起草上下文 + 任务
> **上下文**:在 `q7-execution.ipynb` 中已有 Step 1 / 2 / 3 的产出,特别是 `result_sim``result_live` 两个回测结果。Step 3 让学员看到了整体的执行落差,但「整体差 6%」这个数字不够——需要知道具体哪笔差得最多、是标的问题还是日期问题。
>
> **任务**:用 `ExecutionReport` 逐笔对比回测和实盘的成交,分析执行质量分布。
> 📌 **要点**q7 的 spec 链同 q1 一样讲究「接续型 spec」——`result_live` 不论走 spec-03 的 Part B-2Alpaca)还是兜底路径都会赋值。spec 的上下文段必须把上游变量名、形状、来源全列出,AI 才知道直接用哪份数据,不会重新跑回测。
#### 起草要求:实盘 vs 回测的对照表
要求段是这份 spec 的灵魂——五步走完一条诊断闭环:逐笔对比 → 汇总 → 最贵的一笔 → 分布图 → 桥接 A 股。
> **要求**
>
> 1. 用 `result_sim.trades``result_live.trades` 构造 `ExecutionReport`,打印前 20 笔逐笔对比表(日期/标的/方向/回测价/实盘价/滑点/回测股数/实盘股数/股数差)
> 2. 汇总统计:总笔数、匹配交易数、仅回测有、仅实盘有、平均价格滑点、总手续费差异
> 3. **最贵的一笔**:从匹配交易(`sim_shares > 0 且 live_shares > 0`)中按 `|price_slippage|` 排序,打印 Top 1 详情;滑点 > 1% 时分析原因(波动大/流动性差/跳空)
> 4. 执行质量分布图(figsize 14×5,两子图并排):左图滑点直方图(30 bins,标零滑点红色虚线 + 平均值橙色虚线)+ 右图按标的分组的滑点箱线图(SPY/QQQ/GLD 三色填充)
> 5. 桥接 A 股:用文字说明美股的执行问题在 A 股一样存在(取整偏差更大 / 滑点 / 印花税 / 执行落差),并给出操作建议(`generate_orders()` 生成订单清单 / 定期对比净值 / 异常时检查原因)
> 📌 **要点**:**反平均设计**——「找出最贵的一笔」是关键的反平均。执行落差的“平均”是 0.05% 看起来无害,但少数几笔可能差 1-2%。学员只看汇总统计会得到错误结论,必须看尾部。这是 q5/q6 已建立的「分布优于均值」原则在执行场景的延续。
> 📌 **要点**:**实盘 vs 回测的对照表**用双图各回答一个问题——直方图回答「整体怎样」(看分布形状),箱线图回答「问题在哪只标的」(看哪个箱子异常多)。两个视角不可互相替代,缺一不可。
> 📌 **要点**:桥接 A 股要克制——q7 的主线是认知(了解执行落差),不是工程(A 股自动化交易)。spec 用文字桥接而非要求重跑,避免给学员塞超出本章范围的负担(国内券商 API 是另一套基础设施)。
#### 起草结果呈现
> **结果呈现**:逐笔对比表 ≥ 20 行 + 汇总统计(含匹配交易数和平均滑点)+ 最贵一笔具体数值 + 直方图 + 箱线图 + 桥接 A 股的操作建议。
完整 spec 在 [`specs/spec-04-execution-report.md`](https://github.com/xingwudao/xquant-learning/blob/main/q7-execution/specs/spec-04-execution-report.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了逐笔对比的执行报告。`ExecutionReport` 接收两个成交记录列表——回测的和实盘的——按日期和标的匹配,逐笔对比价格差异。
### 运行结果
先看逐笔对比表(前 20 笔,节选),如表 7-7 所示。
**表 7-7 回测 vs 实盘逐笔成交对比(前 20 笔节选)**
| 日期 | 标的 | 方向 | 回测价 | 实盘价 | 滑点 | 回测股数 | 实盘股数 | 股数差 |
|------|------|------|--------|--------|------|----------|----------|--------|
| 2021-02-02 | GLD | BUY | $172.11 | $172.14 | +0.02% | 213 | 211 | -2 |
| 2021-03-04 | GLD | BUY | $0.00 | $159.00 | +0.00% | 0 | 3 | +3 |
| 2021-04-05 | GLD | BUY | $161.92 | $161.91 | -0.01% | 19 | 21 | +2 |
| 2021-05-04 | GLD | SELL | $166.58 | $166.56 | -0.01% | 26 | 30 | +4 |
| ... | | | | | | | | |
大部分交易的滑点很小(0.01%-0.02%),但股数差异时有出现——因为两个数据源的价格不同,`generate_orders()` 计算出的股数也不一样。
汇总统计如表 7-8 所示。
**表 7-8 回测 vs 实盘成交汇总统计**
| 指标 | 数值 |
|------|------|
| 总交易笔数 | 194 |
| 匹配交易 | 177(回测和实盘都有) |
| 仅回测有 | 7 |
| 仅实盘有 | 10 |
| 平均价格滑点 | +1.6199% |
| 总手续费差异 | $-17.33 |
177 笔匹配交易,平均滑点 1.62%。还有 7 笔只在回测中出现、10 笔只在实盘中出现——这是因为两个数据源的价格差异导致策略在不同时间触发调仓。
最贵的一笔:日期 2021-02-02,标的 SPY(标普500ETF),方向 BUY,回测成交价 $356.17,实盘成交价 $381.39,价格滑点 +7.08%。
一笔交易的滑点高达 7.08%——这是第一笔交易,回测和实盘的起始价格差距最大。后续交易的滑点会小得多。
执行质量分布如图 7-5 所示。
![执行质量分布:滑点直方图和按标的分组的箱线图](images/q7-execution/05-execution-quality.png)
先看左图的直方图。橙色虚线标注了平均滑点位置。大部分交易的滑点集中在 0-2% 之间,但右侧有一条长尾——少数几笔交易的滑点特别大。
再看右图的箱线图。三只 ETF 的滑点分布差异不大,但每只都有几个“异常值”(箱子外面的点)——这些就是执行层面的尾部风险。
执行质量统计如表 7-9 所示。
**表 7-9 美股模拟盘执行质量统计**
| 指标 | 数值 |
|------|------|
| 总交易笔数 | 177 |
| 平均滑点 | +1.620% |
| 中位数滑点 | +0.973% |
| 最小滑点 | -0.112% |
| 最大滑点 | +7.082% |
| 滑点超过 0.5% 的交易 | 99 笔(56% |
### 结果解读
执行报告是实盘运行后最重要的“对账”工具。
滑点直方图讲了一个重要的事实:**大部分交易的执行质量是正常的,滑点很小。但总有少数几笔偏差特别大。** 这就是执行层面的“尾部风险”——和策略收益的尾部风险一样,你无法消除它,但你需要知道它的存在。
箱线图按标的分组,能帮你发现:是某只 ETF 的执行质量特别差(可能流动性不够),还是所有标的都差不多?如果某只标的的滑点持续偏大,你可能需要考虑换一只流动性更好的标的,或者调整下单时机。
**实盘运行的习惯:** 每次调仓后,生成执行报告对一遍账。不是看一眼净值就完了——要逐笔检查,发现异常及时处理。这和开车一样:你不会只看速度表,还要看油量、水温、胎压。
---
## 7.5 本章总结
四步下来,我们从四个角度回答了同一个核心问题——“回测结果能直接用吗?”
Step 1:订单生成 → 权重只能买整数股 → 取整偏差 + 现金残余 → Step 2:成交价压力测试 → 五种价格假设差距巨大 → 执行不确定性 → Step 3:Alpaca 模拟盘 → 数据源不同结果就不同 → 执行落差是常态 → Step 4:执行报告 → 逐笔对账 → 大部分正常,少数偏差大。
从今以后,从回测走向实盘,你可以用这套流程管理执行风险,整理成检查清单如表 7-10 所示。
**表 7-10 从回测走向实盘的执行风险检查清单**
| 检查项 | 怎么做 | 好的信号 | 坏的信号 |
|--------|--------|----------|----------|
| 取整偏差 | 用 `generate_orders()` 提前计算 | 偏差 < 0.5% | 偏差 > 2%,资金太小或 lot_size 太大 |
| 成交价敏感性 | 用五种 FillPriceMode 跑压力测试 | 五种模式差距小 | 差距大,策略对执行时机敏感 |
| 交易成本 | 逐层叠加佣金、印花税、滑点 | 成本占年化收益 < 20% | 成本吃掉大部分收益 |
| 执行落差 | 对比回测净值和实盘净值 | 落差稳定且可预期 | 落差持续放大 |
| 执行质量 | 生成执行报告逐笔对账 | 滑点集中在 0 附近 | 频繁出现大滑点 |
**一个更深的认知:回测收益不等于实盘收益。** 中间隔着取整偏差、滑点、佣金、印花税。在评估策略时,回测收益减去预估的执行落差,才是你的合理预期。
### 策略进化路径
第六章产出:通过检验的策略,信心满满 → Step 1:权重转订单(取整偏差 + 现金残余)→ Step 2:五种成交价压力测试(滑点 + 成本全景)→ Step 3:回测 vs 实盘(亲眼看到执行落差)→ Step 4:执行报告(逐笔对账,找到差距来源)→ 第七章产出:从回测到实盘的完整闭环 + “执行有成本”的认知
### 概念速查表
本章涉及的核心概念汇总如表 7-11 所示,方便随时回查。
**表 7-11 第七章核心概念速查**
| 概念 | 含义 | 类比 |
|------|------|------|
| 取整偏差 | 因为只能买整数股,实际权重和目标权重之间的差距 | 预算 35% 交通费,但车票只有整数价,实际花的不是精确的 35% |
| 现金残余 | 凑不成整股而闲置在账户里的零头资金 | 买完所有东西后口袋里剩的零钱 |
| 成交价模式 | 回测中假设订单在什么价格成交 | 网上标价和到店实际付款价的区别 |
| 执行不确定性 | 最好和最差成交价之间的差距区间 | 同一航班,不同时间买的票价差异 |
| 滑点 | 下单时价格因市场波动偏离预期的部分 | 菜市场问价 5 块,掏钱时变 5.1 了 |
| 执行落差 | 回测收益和实盘收益之间的差距 | 菜谱上说 30 分钟做好,实际花了 45 分钟 |
| LiveBroker | 把订单真正发送到交易所的交易接口 | 从“纸上模拟下单”到“真的点了购买按钮” |
| 执行报告 | 逐笔对比回测成交和实盘成交的对账单 | 收到外卖后核对订单:点了三道菜,价格对不对,少没少 |
### 本章核心认知
走完四步实验后,本章最值得带走的核心认知如表 7-12 所示。
**表 7-12 第七章核心认知**
| 认知 | 来源 |
|------|------|
| 回测权重是理想值,实际只能买整数股 | Step 1 实验 |
| 资金量越小、整手约束越大,偏差越大 | Step 1 实验 |
| 同一个决策,执行时机不同,结果差距很大 | Step 2 实验 |
| 回测的收盘价假设是最乐观的 | Step 2 实验 |
| 交易成本(佣金+滑点+印花税)持续侵蚀收益 | Step 2 实验 |
| 回测和实盘永远有差距——执行落差 | Step 3 实验 |
| 策略逻辑和执行环境可以完全解耦 | Step 3 实验 |
| 实盘要养成逐笔对账的习惯 | Step 4 实验 |
| 大部分交易正常,少数几笔偏差大——执行的尾部风险 | Step 4 实验 |
### 带走的问题
我们已经走完了从选标的(第二章)到真正执行(第七章)的完整闭环。回测世界和实盘世界之间的桥梁搭好了。
但策略跑起来之后,新的问题出现了:
- 上个月赚了 3%,这个月亏了 5%——这是正常波动,还是策略出了问题?
- 什么时候应该停下来,什么时候应该继续坚持?
- 发现问题后,怎么系统性地改进,而不是推翻重来?
**策略活着的时候,怎么照顾它?** → 第八章
**到这里,你完成了飞轮的“执行层”——这一章把“做 + 看”落到了真金白银的边界:“做”是把 35% 的权重翻成 8100 股的订单、把 NEXT_OPEN 当成更诚实的成交假设、再用 LiveBroker 把订单递给 Alpaca;“看”是用执行报告逐笔对账,看清那 1.62% 的平均滑点和 7.08% 的最贵一笔藏在哪里。本书口号“做要规则、看要指标、还要怀疑你的指标”——前两步在这一章贯穿到底,“疑”留到下一章在实盘环境里继续。**
> 本章所有代码的可运行版本见 `notebooks/q7-execution.ipynb`
+523
View File
@@ -0,0 +1,523 @@
# 第八章:让策略活着:监控、诊断、迭代
第七章你搭好了从回测到实盘的桥梁——订单生成、成交价压力测试、模拟交易、执行报告。策略终于跑起来了。
**然后你开始每天看净值。**
上个月赚了 3%,开心。这个月亏了 5%,慌了。盯着那条往下走的曲线,你脑子里全是问号——策略是不是坏了?要不要停?要不要改?还是再等等?
别慌。这一章教你三件事:怎么看(监控)、怎么查(诊断)、怎么改(迭代)。
**飞轮转回起点了。** 这一章你站在飞轮的“归因层”——但和第五章不同,第五章你“看”的是离线的回测;这一章你“看”的是活着的实盘。
本书口号“做要规则、看要指标、还要怀疑你的指标”——做(监控规则)、看(仪表盘指标)、疑(诊断三问),三个动作在这一章被你**第二次**走了一遍。
### 路线图
**选什么标的(第二章)→ 每个买多少(第三章)→ 什么时候买卖(第四章)→ 怎么验证有效(第五章)→ 如何避免自欺欺人(第六章)→ 如何真正执行(第七章)→ 如何持续改进(第八章本章)**
本章从“盯着净值焦虑”的状态出发,用三个步骤建立系统性的迭代能力,三个步骤的问题与方法对照如表 8-1 所示。
**表 8-1 第八章三步迭代框架——问题与方法对照**
| 步骤 | 问题 | 方法 |
|------|------|------|
| 第 1 步 | 策略是不是坏了? | 监控仪表盘——用指标替代焦虑 |
| 第 2 步 | 问题出在哪? | 诊断三问——由外到内定位原因 |
| 第 3 步 | 怎么改? | 假设驱动的对照实验 |
(操作流程见前言“怎么使用这本书”。)
---
## 8.1 策略是不是坏了?
上个月赚了 3%,这个月亏了 5%。你盯着净值曲线,心里发慌。
但“净值在跌”这个信息太粗糙了——就像你身体不舒服,光说“不舒服”没用,得量体温、测血压、做检查。策略也一样,需要一个“仪表盘”,用具体的指标告诉你:现在到底是正常波动,还是真的出了问题。
我们用三个指标构成监控仪表盘,三者覆盖的角度如表 8-2 所示。
**表 8-2 监控仪表盘的三个核心指标**
| 指标 | 含义 | 类比 |
|------|------|------|
| **滚动夏普**(最近 3 个月) | 最近 3 个月的风险调整收益 | 体温计——正常在 0 以上,跌破 0 说明在“发烧” |
| **滚动回撤** | 从近期高点到现在跌了多少 | 水位线——持续下降说明“水在退” |
| **对比沪深 300 多赚了多少**(你 - 沪深 300) | 同期你的策略和沪深 300 指数的差额 | 跑步比赛——持续落后说明跑法有问题 |
### 动手实验 1:监控仪表盘
这是本章第一份“运营 spec”,和前七章那种“建一个新功能”不同——这里我们要写的是**怎么周期性地看一个已经在跑的策略**。我们一起把这份 spec 写出来。这次重点看两件新东西:**心智切换**——上下文段第一句话就要告诉 AI“这不是建造,是体检”;以及**周期性运营 spec 的四要素**——触发条件 / 频率 / 阈值 / 输出形态全部显式。
#### 起草上下文 + 任务
上下文段开头那句“用具体指标替代焦虑”是整个第八章的总动机。运营章 spec 的上下文不必交代环境怎么搭,要交代**学员现在的情绪状态和场景**:
> **上下文**:学员跑完第七章,策略终于在实盘运行。但上个月赚了 3%,这个月亏了 5%——是正常波动还是策略出了问题?需要一个“仪表盘”,用具体指标替代焦虑。
>
> **任务**:在 notebook 中跑基准回测后,用 `StrategyMonitor` 构建监控仪表盘,画三个滚动指标的时间序列图,并自动检测恶化时段。
> 📌 **要点**:运营 spec 的上下文要交代“心智切换”。前七章的 spec 是“建一个新功能”,这一章开始是“看一个已有功能的健康状态”。学员如果带着建造者的姿态去做运维,看了 30 个图也不知道在干嘛——上下文一句话点明“用指标替代焦虑”,就是给学员(也给 AI)定调子:这次的产出不是新代码,是健康判断。
#### 起草要求:周期性运营 spec 的四要素
要求段是这份 spec 的主体——重点是把“什么时候触发监控、多久跑一次、什么数值算异常、异常了怎么输出”四件事**全部显式写明**:
> **要求**
>
> 1. 跑基准回测(`SimBroker` + 风险平价 + 21 天调仓 + 10% 止损),拿到 `result_base`
> 2. 用 `StrategyMonitor(result_base, benchmark="510300.SS", roll_window=63)` 获取滚动指标
> - `roll_window=63` ≈ 3 个月交易日(21 天/月 × 3),太短(<20)每天报警、太长(>120)警报来时已经亏了半年
> 3. 画四行子图(figsize 14×16,共享 x 轴):净值曲线 / 滚动夏普(标零线和 1 线)/ 滚动回撤 / 相对沪深300超额收益,红色填充异常区域
> 4. 自动检测恶化时段——告警规则显式声明:
> - **触发条件**:滚动夏普 < 0 且持续 ≥ 20 个交易日
> - **频率**:每月例行检查一次;第 1 步发现恶化时立即触发第 2 步诊断
> - **阈值**`sharpe_threshold=0.0, min_days=20`(在 `StrategyMonitor` 构造时锁定)
> - **输出形态**:遍历 `monitor.bad_periods`,每段一行打印起止日期、持续天数、平均夏普
> 5. 打印结尾过渡句:“2022 年集中出现恶化,原因是什么?”——把读者推向第 2 步
> 📌 **要点**:周期性运营 spec 的告警四要素——触发条件 / 频率 / 阈值 / 输出形态——必须全部显式。建造类 spec 的“成功标志”是“代码跑通、图渲染出来”;运营类 spec 的“成功标志”是“在该报警的时候报了警、不该报的时候没报”。**任何一个要素藏在 oxq 库默认参数里、没写进 spec,半年后阈值变了你也不知道**——这是运营 spec 区别于 q1-q7 建造 spec 的核心方法点。
> 📌 **要点**:关键常量旁边要写“为什么是这个数”。`roll_window=63` 不是随便选的——它是行业惯例(约 3 个月交易日),理由也要进 spec。运营场景里的每一个数字都是阈值,阈值的合理性比数值本身更重要。
#### 起草结果呈现
> **结果呈现**:四行仪表盘图正常渲染 + 恶化时段列表非空(2022 年应有多个时段)+ `equity``daily_ret``result_base``monitor` 变量已定义(后续 spec 接续使用)。
完整 spec 在 [`specs/spec-01-monitoring.md`](https://github.com/xingwudao/xquant-learning/blob/main/q8-iteration/specs/spec-01-monitoring.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了基准回测结果和一张四行的监控仪表盘图。
这个实验做了什么?先用 第四、七章 同款的风险平价策略(沪深300ETF + 纳指100ETF + 黄金ETF21 天调仓 + 10% 止损)跑一次从 2021 年到现在的完整回测,拿到净值曲线。然后把净值喂给 `StrategyMonitor`——它就是那个“体检中心”,自动计算三个滚动健康指标(滚动夏普、滚动回撤、相对基准超额收益),并标注出指标持续恶化的时段。
### 运行结果
先看基准回测:累计收益 86.45%,年化 12.50%,最大回撤 -16.62%,夏普 1.17,共 184 笔交易。
监控仪表盘的整体效果如图 8-1 所示。
![监控仪表盘——四个指标同时恶化的时段就是“危险区”](images/q8-iteration/01-monitoring-dashboard.png)
图 8-1 第一行是净值曲线,整体向上但中间有几段明显的下跌。第二行是**滚动夏普**——红色填充区域就是夏普跌破 0 的时段,说明最近 3 个月策略在亏钱。第三行是回撤,持续加深说明策略在“失血”。第四行是相对沪深300的超额收益,为负说明还不如直接买指数。
仪表盘自动检测到的恶化时段如表 8-3 所示。
**表 8-3 监控仪表盘自动检测到的恶化时段**
| 起始日期 | 结束日期 | 持续天数 | 平均夏普 |
|----------|----------|----------|----------|
| 2022-02-07 | 2022-04-29 | 58 天 | -1.78 |
| 2022-05-05 | 2022-08-10 | 68 天 | -2.25 |
| 2022-08-29 | 2022-09-30 | 23 天 | -1.13 |
| 2022-10-10 | 2022-12-01 | 39 天 | -1.02 |
| 2023-10-18 | 2023-11-14 | 20 天 | -0.84 |
| 2023-12-05 | 2024-01-03 | 21 天 | -0.91 |
2022 年集中出现了 4 段恶化,最长的一段持续了 68 天,平均夏普 -2.25。
### 结果解读
仪表盘让你从“盯着净值焦虑”变成“看指标判断”。
滚动夏普跌破 0,说明最近 3 个月策略在亏钱——这是最直接的“发烧”信号。回撤持续加深说明策略在“失血”。相对沪深300的超额收益为负,说明你还不如直接买指数。
三个指标同时恶化的时段就是需要关注的“危险区”。2022 年出现了多段集中恶化——但**发现问题不等于知道原因**。
是执行出了问题?市场变了?还是策略本身有缺陷?
---
## 8.2 问题出在哪?
第 1 步告诉你“最近不对劲”——但不对劲有很多种原因。
在开始诊断之前,先认识一个本节的关键概念:
**市场状态(regime)** ——市场不是一成不变的。有时候波动大、几乎所有资产一起剧烈起伏(“高波动”),有时候很平静(“低波动 / 正常”)。就像天气有晴天和台风天,同一件衣服在不同天气穿感受完全不同。策略也一样——一个在正常市场表现不错的策略,到了高波动状态可能完全失灵。**诊断三步里有两步都和它相关**——所以我们前置介绍它。
就像身体不舒服,你不会直接吃药。先量体温排除感冒,再查血排除感染,最后做 CT 看看有没有更深的问题。策略诊断也一样,我们由外到内排查三层:
1. **是执行的问题吗?** —— 回测和实盘之间的差距有没有异常放大
2. **是市场状态(regime)变了吗?** —— 市场的波动水平有没有发生大的变化
3. **是策略本身的设计问题吗?** —— 参数在不同时段的表现是否稳定
### 动手实验 2:诊断三问
我们一起把这份 spec 写出来。这次重点看两件新东西:**一份 spec 包多步诊断的取舍**——前七章每份 spec 只做一件事,这份一次塞进三步诊断;以及**阈值事前锁定**——什么样的数字算“通过”、什么样算“失败”,必须在跑诊断之前定好,不能跑完了再看图说话。
#### 起草上下文 + 任务
接续型 spec 链的上下文要写明前置 spec 留下了哪些变量、本份 spec 在叙事里的位置:
> **上下文**:在 `q8-iteration.ipynb` 中已有第 1 步监控仪表盘和恶化时段检测。第 1 步发现了多段恶化时段(2022 年集中出现),现在需要搞清楚原因。由外到内排查三层:执行 → 市场 → 策略。
>
> **任务**:完成三步诊断——排查执行落差、检测市场状态变化、检查参数稳定性。这三步是不可拆分的方法论,必须按“由外到内”的顺序执行。
> 📌 **要点**q1-q7 的写法是“一份 spec 一件事”,这份是“一份 spec 三件事”——叙事完整性优先于任务原子性。诊断方法论本身就是一个不可拆分的整体,拆成三份 spec 学员会丢掉“先排外因再查内因”的节奏。但代价是 spec 体积变大,所以**必须在任务段第一句就明示“这三步必须按顺序”**——给 AI 也给学员定下“这是一个流程,不是三个独立任务”的预期。
#### 起草要求:阈值事前锁定
要求段是这份 spec 的主体——三步诊断每一步都要把“通过 / 失败”的判定阈值事前写明,绝不能“看图说话”:
> **要求**
>
> 1. **诊断 1——执行落差排查**
> - 跑“理想回测”(收盘价 + 佣金,无滑点)和“模拟实盘”(次日开盘 + 佣金 + 千分之一滑点)
> - 计算两条净值曲线的滚动执行落差(63 日滚动均值)
> - **判定阈值**:落差走势平稳(每 63 日变化标准差 < 0.5%)→ 排除执行问题;若突然放大则结论改为“需进一步检查滑点 / 佣金模型”
> - 把落差放到学员能理解的语境中:N 年赚了 X%,其中 Y% 被执行成本吃掉
>
> 2. **诊断 2——市场状态检测**
> - 用 `MarketStateDetector(result_base)` 检测市场状态
> - 分状态打印策略表现(年化收益、夏普、天数)
> - **判定阈值**:高波动状态下夏普 < 0 且与第 1 步恶化时段重叠 ≥ 70% → 结论“市场状态变了,不是策略 bug”
> - 画两行子图:净值曲线 + 高波动红色阴影 / 市场波动率 + 中位数线 + 高波动阈值线
> - `detector``high_vol_mask``market_vol``HIGH_VOL_LINE` 变量必须已定义(第 3 步接续使用)
>
> 3. **诊断 3——参数稳定性检查**
> - 把数据分成前后两半(按交易日中点切分)
> - 对 5 种调仓频率(10/15/21/42/63 天)分别在两半上跑回测
> - **判定阈值**:用 `scipy.stats.kendalltau` 计算前后两半夏普排序的 tau(Kendall tau 是衡量两组排序一致性的统计量,取值 -1 到 1,越接近 1 说明前后两段的最优参数排序越像);tau ≥ 0.7 → 排序稳定,参数没过拟合;tau < 0.7 → 参数对时段敏感
> - 打印对比表:频率、前半段年化、前半段夏普、后半段年化、后半段夏普
> 📌 **要点**:诊断 spec 的核心是“事前锁定阈值”——绝不能“看图说话”。建造类 spec 的失败长什么样很明显(代码报错、图渲染不出来);诊断类 spec 的失败是隐式的——你跑完看到一个图,凭直觉说“看起来没问题”。**只要阈值不事前锁定,诊断就退化成 confirmation bias**——你想看到什么结论就能从图里读出什么结论。每一步诊断都必须配一个“通过 X、失败 Y”的数字阈值。
> 📌 **要点**:spec 不能给 AI“二选一”。如果你写“用 Kendall tau 或简单比较都行”,AI 选哪种你就拿到哪种结果,下次重跑可能换一种实现,结果不可比。**spec 是契约不是协商**——能确定一种方案就锁死一种,留 AI 自由发挥的余地越小,诊断越可复现。
#### 起草结果呈现
> **结果呈现**:三步诊断各一组图 + 数字结论;`detector``high_vol_mask``market_vol` 变量已定义供第 3 步使用。
完整 spec 在 [`specs/spec-02-diagnostics.md`](https://github.com/xingwudao/xquant-learning/blob/main/q8-iteration/specs/spec-02-diagnostics.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了三步诊断的完整结果。
这个实验做了什么?由外到内排查三层原因:
- **诊断 1(执行)**:跑两次回测——一次用“理想条件”(收盘价成交、只有佣金),一次用“模拟实盘”(次日开盘价 + 佣金 + 滑点),对比两条净值曲线的差距是否在正常范围内。
- **诊断 2(市场)**:用 `MarketStateDetector` 自动检测市场的波动率水平,把每一天标记为“高波动”或“正常”,然后分别统计策略在两种状态下的表现。
- **诊断 3(参数)**:把整段回测数据按时间中点切成前后两半(前半段大致覆盖 2021-2023 上半年的高波动市场,后半段覆盖 2023 下半年至今的恢复期),在两半上分别测试 5 种调仓频率,看最优参数在不同时段是否稳定。
### 运行结果
按“由外到内”的顺序看三步诊断的输出。
#### 诊断 1 — 执行落差排查
理想回测收益 92.13%,模拟实盘收益 83.54%,执行落差累计 8.59%,每年约 1.72%。
换个方式理解:策略 5 年累计赚了 92%,其中 8.6% 被佣金、滑点等执行成本吃掉了。相当于每年的执行成本约 1.7%——对于月度调仓的策略来说,这是正常水平。
更重要的是执行落差的走势是平稳的,没有突然放大——说明执行层面没有异常。如果这条线突然变陡(落差加速扩大),才说明执行出了问题。
排除执行问题。
#### 诊断 2 — 市场状态检测
回顾 8.2 节首讲过的**市场状态(regime)**——这一步我们用 `MarketStateDetector` 把每一天自动标记为高波动 / 正常 / 低波动,再看策略在不同状态下的表现差异。结果三档分得很开——表 8-4 一目了然。
策略在不同市场状态下的表现差异如表 8-4 所示。
**表 8-4 策略在不同市场状态下的表现**
| 市场状态 | 年化收益 | 夏普 | 天数 |
|----------|----------|------|------|
| 高波动 | -1.44% | -0.08 | 260 天 |
| 正常 | 16.62% | 1.80 | 940 天 |
| 低波动 | 34.72% | 6.32 | 36 天 |
策略净值与高波动时段的叠加如图 8-2 所示。
![策略净值 + 高波动时段——恶化和高波动高度吻合](images/q8-iteration/02-market-state.png)
图 8-2 的红色阴影区域就是高波动时段。和第 1 步检测到的恶化时段一对比,高度吻合——策略在高波动环境下年化收益 -1.44%,夏普 -0.08,在亏钱。而正常时段年化 16.62%,夏普 1.80,表现优秀。
恶化的原因是市场状态变了,不是策略的 bug。
#### 诊断 3 — 参数稳定性检查
把数据按时间中点切成前后两半,对 5 种调仓频率分别跑回测,结果如表 8-5 所示。
**表 8-5 5 种调仓频率在前后两半段的表现**
| 频率 | 前半段年化 | 前半段夏普 | 后半段年化 | 后半段夏普 |
|------|-----------|-----------|-----------|-----------|
| 10 天 | +3.30% | 0.40 | +25.78% | 2.17 |
| 15 天 | +1.91% | 0.25 | +23.76% | 2.02 |
| 21 天 | +2.18% | 0.28 | +23.13% | 1.89 |
| 42 天 | +2.88% | 0.37 | +24.82% | 1.95 |
| 63 天 | +5.34% | 0.65 | +24.56% | 1.87 |
不同时段的年化收益差异很大(前半段 2-5%,后半段 23-26%),说明策略在不同市场环境下表现差异大。但参数排序有变化——前半段 63 天最优,后半段 10 天最优。这意味着:策略的框架是对的,但对某些市场状态没有应对方案。
### 结果解读
三步诊断的结论汇总如表 8-6 所示。
**表 8-6 三步诊断的结论汇总**
| 诊断 | 问题 | 结论 |
|------|------|------|
| 执行 | 回测和实盘的差距有没有异常放大? | 没有。执行落差稳定,排除 |
| 市场 | 市场波动水平有没有大的变化? | 有。恶化时段和高波动高度吻合 |
| 策略 | 参数在不同时段是否稳定? | 排序有变化,但策略框架没问题,对某些市场状态没有应对方案 |
问题定位清楚了——不是执行的问题,不是参数过拟合,而是**策略对高波动的市场状态没有应对方案**。
把这套诊断流程压成一句以后能脱口而出的口诀——
> **🔧 归因 SOP 口诀:先排执行落差(外),再查市场状态(中),最后看策略本身(内)。**
这个由外到内的顺序不能反过来。先动策略本身,你大概率会“为了一个市场状态变化”而推翻整个框架,下一轮市场恢复正常你又要再推翻一次——这正是缺乏纪律的迭代最常见的死循环。三步走完之后再决定改什么、改哪一层,每一步都有数据支撑。
那怎么改?推翻重来?还是在现有策略上做小步改进?
---
## 8.3 怎么改?
诊断结果出来了——执行没问题,是市场状态变了,策略对高波动没有应对。
你可能想直接调参数试试。但改策略不是拍脑袋——你需要像做实验一样:**先提出假设和判断标准,再做对照实验,最后看结果确认还是推翻假设。**
就像医生看病——不是看到头疼就开止疼药,而是先做检查确认病因,再开药。
每次迭代都走这个闭环:
```
观察(看到了什么)—— 假设(猜原因)—— 验证标准(怎么判断对错)—— 实验(改一个变量)—— 结论(确认/推翻)
```
### 动手实验 3:假设驱动的对照实验
我们一起把这份 spec 写出来。这次重点看两件新东西:**让“假设”跟着 Strategy 跑**——open-xquant 的 `Strategy` 类在设计时就内置了 `hypothesis``objectives` 两个空,创建 Strategy 时填两个空(假设、判定标准),跑出来的 Strategy 自己背着这张假设卡;以及**验证标准事前锁定具体数字**——绝不写“看情况调整”这种橡皮条款。
#### 起草上下文 + 任务
> **上下文**:在 `q8-iteration.ipynb` 中已有第 1 步监控 + 第 2 步诊断的完整变量。诊断结论:策略对高波动的市场状态没有应对方案。
>
> **任务**:完成两轮迭代实验(调仓频率 + 波动率过滤),用 Strategy 的 `hypothesis`/`objectives` 字段展示完整的实验工作流,最后用 `ExperimentLog` 记录迭代历史。
#### 起草要求:假设跟着 strategy 跑 + 验证标准事前锁定
要求段是这份 spec 的灵魂——把“假设 → 验证标准 → 实验 → 结论”的方法论挂到 Strategy 自己身上的两个空(`Strategy.hypothesis``Strategy.objectives`),让 strategy 跑到哪、假设卡跟到哪:
> **要求**
>
> 1. **迭代 1——调仓频率对照实验**(注定被推翻的一轮):
> - 创建 Strategy 时填写 `hypothesis="缩短调仓频率能改善高波动期表现"`
> - 同时填写 `objectives={"high_vol_sharpe": "above_baseline", "max_dd_limit": dd_limit}`——验证标准事前锁定
> - 对 6 种频率(5/10/15/21/42/63 天)分别跑回测
> - 计算每种频率在高波动时段的夏普(用 `high_vol_mask` 筛选)
> - 假设验证:对比基准(21 天)和最优频率的高波动夏普,判断假设确认还是推翻
>
> 2. **迭代 2——波动率过滤对照实验**(注定被确认的一轮):
> - 创建 Strategy 时填写 `hypothesis="高波动时降仓位能显著降低回撤"`
> - 验证标准事前锁定:`objectives={"dd_improvement": ">=30%", "sharpe": ">=1.0"}`
> - 用 `VolFilteredOptimizer` 包装 `RiskParityOptimizer`:高波动期权重缩放 0.5
> - 测试 4 种配置:无过滤、阈值 10% / 15% / 20%
> - **“最佳配置”选择函数显式锁定**`best_label = argmax(dd_improvement)`——不能让 AI 揣摩“最佳”的含义
> - 假设验证:检查最佳配置是否同时满足两个事前标准
>
> 3. **迭代记录表**
> - 用 `ExperimentLog(name="第八章迭代实验")` 创建记录
> - 用 `log.add()` 添加两轮迭代,每行字段值字符级显式:`name` / `observation` / `hypothesis` / `criteria` / `result` / `conclusion` / `notes`
> - 用 `log.to_dataframe().to_string(index=False)` 打印记录表
> 📌 **要点**:让 hypothesis 跟着 strategy 跑——`hypothesis``objectives` 这两个空是 open-xquant 在 `Strategy` 类设计时就内置好的。spec 要求“创建 Strategy 时填写”这两个空,意思是把“假设驱动”从写在注释里、口头讲讲的方法论,挂到 Strategy 自己身上——跑出来的 Strategy 对象自带 `.hypothesis` 标签,`ExperimentLog` 自动取走填进表格。**以前你的假设写在心里、写在备忘录里——现在 Strategy 自己背着这张假设卡。**
> 📌 **要点**:验证标准必须事前锁定到具体数字。“高波动夏普优于基准”是数字、“回撤改善 ≥ 30% 且夏普 ≥ 1.0”是数字——绝不写“看情况调整”“效果好就采纳”这种橡皮条款。**事后改标准就是过拟合**——你跑完看到 25% 改善,临时把标准从 30% 改到 20%,那不是验证假设,是在为已有结果找说辞。事前锁定阈值是迭代 spec 区别于“调参 spec”的本质差异。
> 📌 **要点**:注意章节的节奏——迭代 1 故意安排成“假设被推翻”,迭代 2 是“假设被确认”。这是教学设计:先让学员体会“推翻不是失败、推翻是省下了实盘的学费”,再示范一个被确认的假设长什么样。spec 也要配合这个节奏——迭代 1 不要写得“温柔一点让它更容易通过”,要让结果如实推翻。
#### 起草结果呈现
> **结果呈现**:两轮指标表 + 净值对比图 + 假设验证结论 + 2 行迭代记录表(iter1 rejected + iter2 confirmed+ Strategy 的 hypothesis / objectives 字段值在输出中可见。
完整 spec 在 [`specs/spec-03-iteration.md`](https://github.com/xingwudao/xquant-learning/blob/main/q8-iteration/specs/spec-03-iteration.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了两轮对照实验的完整结果和迭代记录表。
这个实验做了什么?两轮对照实验,每轮都遵循“假设 —— 验证标准 —— 实验 —— 结论”的流程:
- **迭代 1(调仓频率)**:假设“缩短调仓频率能改善高波动期表现”。保持策略逻辑不变,只改调仓周期——分别测试 5/10/15/21/42/63 天六种频率,重点比较各频率在高波动时段(第 2 步检测出的那些红色阴影区域)的夏普。
- **迭代 2(波动率过滤)**:假设“高波动时降低仓位能显著减少回撤”。保持调仓频率不变,用 `VolFilteredOptimizer` 包装 `RiskParityOptimizer`——当市场波动率超过阈值时,自动将权重缩放 0.5(仓位减半)。分别测试无过滤、阈值 10%/15%/20% 四种配置,看回撤改善了多少、付出了多少收益代价。
open-xquant 的 `Strategy` 支持在创建时填写 `hypothesis`(假设)和 `objectives`(验证目标),让“假设驱动”从口号变成 Strategy 自己背着的两个标签。`ExperimentLog` 则帮你自动记录每次迭代的假设、标准、结果和结论。
### 运行结果
两轮迭代的产出按“先被推翻、后被确认”的节奏展开。
#### 迭代 1 — 调仓频率
观察:高波动时段策略表现最差。假设:21 天调仓太慢,缩短频率能改善。验证标准:高波动时段夏普优于基准。
6 种调仓频率的全时段与高波动指标对比如表 8-7 所示。
**表 8-7 迭代 1——6 种调仓频率的指标对比**
| 频率 | 年化收益 | 最大回撤 | 夏普比率 | 高波动夏普 |
|------|----------|----------|----------|------------|
| 5 天 | +13.01% | -14.47% | 1.25 | 0.22 |
| 10 天 | +14.55% | -12.57% | 1.39 | 0.56 |
| 15 天 | +13.29% | -14.14% | 1.26 | 0.09 |
| 21 天(基准) | +12.50% | -16.62% | 1.17 | -0.08 |
| 42 天 | +14.09% | -15.98% | 1.29 | 0.44 |
| 63 天 | +14.71% | -11.49% | 1.31 | 0.84 |
不同调仓频率的净值曲线对比如图 8-3 所示。
![迭代 1:不同调仓频率的净值对比——图例中是高波动时段的夏普](images/q8-iteration/03-iter1-rebal-freq.png)
注意看表 8-7 最后一列——“高波动夏普”,这才是我们验证假设的指标。全时段夏普最高的是 10 天(1.39),但那不是我们的验证目标。高波动时段表现最好的是 63 天(0.84)——**更长而非更短**。
**假设被推翻。** 缩短调仓频率并没有改善高波动期表现。
这告诉我们什么?改“多久做一次决策”没有用——因为每次做的决策(风险平价满仓分配)没变。问题不在于你多久看一次天气预报,而在于看到暴雨预警后有没有带伞。
#### 迭代 2 — 波动率过滤
观察:频率不是关键,真正的问题是高波动时策略依然满仓暴露在风险中。假设:高波动时降仓位,能显著降低回撤。验证标准:回撤改善 ≥ 30%,夏普 ≥ 1.0。
4 种波动率过滤配置的指标对比如表 8-8 所示。
**表 8-8 迭代 2——4 种波动率过滤配置的指标对比**
| 配置 | 年化收益 | 最大回撤 | 夏普比率 | 回撤改善 |
|------|----------|----------|----------|----------|
| 无过滤(基准) | +12.50% | -16.62% | 1.17 | — |
| 过滤>10% | +6.57% | -8.70% | 1.18 | 47.6% |
| 过滤>15% | +10.44% | -9.62% | 1.43 | 42.1% |
| 过滤>20% | +12.89% | -12.43% | 1.28 | 25.2% |
不同配置的净值曲线对比如图 8-4 所示。
![迭代 2:波动率过滤的净值对比——高波动时仓位减半](images/q8-iteration/04-iter2-vol-filter.png)
最佳配置是“过滤>10%”:回撤从 -16.62% 改善到 -8.70%(改善 47.6%),夏普 1.18。两个验证标准都达标。
**假设被确认。** 波动率过滤有效。
但年化收益从 +12.50% 降到 +6.57%,代价 5.93%。你用 5.9% 的收益换来了 48% 的回撤改善——这就是**取舍(trade-off)**。没有“最好”的参数,只有“适合你风险承受能力”的选择。
### 结果解读
两轮迭代的关键认知汇总如表 8-9 所示。
**表 8-9 两轮迭代的核心认知**
| | 改了什么 | 学到了什么 |
|---|---|---|
| 迭代 1 | 执行参数(多久做一次) | 问题不在频率——策略对市场状态没有反应 |
| 迭代 2 | 策略逻辑(做什么) | 让策略感知市场状态并调整行为才有效 |
假设被推翻不是失败——这是最有价值的学习。如果你凭直觉觉得“调快点应该更好”,然后不做验证就上线,可能越改越差。**对照实验的意义就在于此:用数据而不是直觉做决策。**
**“执行参数”和“策略逻辑”是两件事。** 调仓频率是“多久行动一次”,但每次行动的内容(满仓风险平价)没变。如果问题出在行动的内容上,改频率没用——你需要改策略的行为本身。
---
## 8.4 回头看:你刚才做了什么?
三步实验,回答了同一个核心问题——**策略跑起来之后怎么办?**
第 1 步:建监控,发现“最近不对劲”(滚动夏普跌破 0)。第 2 步:做诊断,定位“是市场状态变了”(由外到内三层排查)。第 3 步:做迭代,改频率没用(推翻),改逻辑有效(确认)。
从今以后,策略出了问题你都可以套这套 SOP 应对,三步对照清单如表 8-10 所示。
**表 8-10 监控/诊断/迭代 SOP 速查**
| 步骤 | 怎么做 | 好的信号 | 坏的信号 |
|------|--------|----------|----------|
| 监控 | 看滚动夏普、回撤、超额 | 指标波动但整体健康 | 多个指标同时持续恶化 |
| 诊断 | 由外到内:执行、市场、策略 | 能定位到具体原因 | 三层都有问题(可能需要重新设计) |
| 迭代 | 假设、验证标准、实验、结论 | 假设被确认或被有价值地推翻 | 没有假设就开始改参数 |
---
## 8.5 本章总结
### 策略进化路径
第七章把策略推上了实盘,第八章把“上线之后怎么办”补完——三步进化路径如表 8-11 所示。
**表 8-11 第八章三步进化路径**
| 步骤 | 做了什么 | 关键发现 |
|------|---------|---------|
| 第 1 步 监控 | 滚动指标 + 自动标恶化 | 2022 年集中出现 4 段恶化 |
| 第 2 步 诊断 | 由外到内三层排查 | 不是 bug,是市场状态变了 |
| 第 3 步 迭代 | 两轮假设驱动对照 | 改频率没用,改逻辑才有效 |
第七章产出“从回测到实盘的完整闭环”,第八章在它之上多走一步——产出“策略迭代的完整方法论”加上“用数据做决策”的认知。
### 概念速查表
本章涉及的核心概念汇总如表 8-12 所示,方便随时回查。
**表 8-12 第八章核心概念速查**
| 概念 | 含义 | 类比 |
|------|------|------|
| 滚动夏普 | 最近一段时间(如 3 个月)的风险调整收益,衡量策略“现在”的健康度 | 体温计——数值正常说明没发烧 |
| 市场状态(regime) | 市场的波动水平和整体环境,不同时期差异很大 | 天气——晴天和台风天出门穿的衣服不一样 |
| 假设驱动迭代 | 先提出可验证的猜想和判断标准,再做实验,而不是凭感觉改 | 医生看病——先检查确认病因,再开药 |
| 对照实验 | 只改一个变量、其他不变,看效果 | 同一道菜只改盐的量,尝尝区别 |
| 执行参数 vs 策略逻辑 | “多久做一次”和“做什么”是两件事 | 多久看一次天气预报 vs 看到暴雨预警后是否带伞 |
| 迭代记录表 | 每次改动的假设、标准、结果、结论,形成可追溯的改进历史 | 病历本——记录每次看病的诊断和治疗 |
### 飞轮第一圈完整闭环
**到这里,你完成了第一圈完整的飞轮——本章的结论会反过来喂给下一圈飞轮的“候选 → 组合 → 执行”。** 候选层(第二章)→ 组合层(第三、四章)→ 执行层(第七章)→ 归因层(第五、六、八章)→ 反哺起点。三字要点(做 / 看 / 疑)也走了完整一遍:做 = 把每一层都写成可重复执行的规则;看 = 用指标判断好坏;疑 = 不止信回测,也质疑实盘的每一段。
下一圈飞轮要让它转得更快——这就是第九章的事。
### 本章核心认知
走完三步之后,本章最值得带走的九条认知如表 8-13 所示。
**表 8-13 第八章核心认知**
| 认知 | 来源 |
|------|------|
| 策略亏钱不一定是坏了,可能是正常波动 | 第 1 步 |
| 持续监控比偶尔看一眼净值重要得多 | 第 1 步 |
| 诊断要由外到内——先排执行,再看市场,最后看策略 | 第 2 步 |
| 市场状态会变,策略在不同状态下表现不同是正常的 | 第 2 步 |
| 改策略前先定好验证标准,不能事后改规则 | 第 3 步 |
| 改“多久行动”和改“怎么行动”是两件事 | 第 3 步 迭代 1 |
| 假设被推翻是好事——省下了实盘亏钱的学费 | 第 3 步 迭代 1 |
| 让策略感知市场状态并调整行为,才是有效的改进 | 第 3 步 迭代 2 |
| 每次改进都是取舍——用收益换安全,或反过来 | 第 3 步 迭代 2 |
### 带走的问题
我们已经走完了从选标的(第二章)到持续改进(第八章)的完整闭环。你现在掌握了监控、诊断和迭代的方法论。
但你可能已经感受到:每次迭代都要构造假设、改参数、跑回测、对比指标、记录结论……这个过程不算轻松。
策略不完美没关系,碰到问题也没关系——只要迭代得够快,从错误和失败中学习得够快,你就可以保持进化。**迭代速度本身就是竞争力。**
有没有办法让这个循环转得更快?—— 第九章
> 本章所有代码的可运行版本见 `notebooks/q8-iteration.ipynb`
---
## 8.6 拓展阅读
本章正文聚焦“监控 / 诊断 / 迭代”三个策略层面的动作。下面这段是动手之外的“软功”——读完正文后回头看,决定你能不能把这套方法长期跑下去。
### 迭代自己:耐心 / 纪律 / 记录
前面三步解决的是策略层面的问题,但还有一层问题藏在你自己身上。
**耐心(对应第 1 步 监控)。** 策略有波动是常态。不要每天盯着净值焦虑——你已经有了监控仪表盘,定期检查就够了。如果你发现自己每小时都在看净值,那不是在监控,是在焦虑。仪表盘的意义不只是发现问题,也是让你在没有问题的时候安心。
**纪律(对应第 3 步 验证标准)。** 定好了验证标准就不要改。如果实验结果不好就改标准,这和第六章讲的过拟合是一回事——你在“事后”调整规则来让结果看起来好,而不是真的在改进。假设被推翻了?接受它,记录下来,想下一个假设。
**记录(对应第 3 步 迭代记录表)。** 每次迭代都填迭代记录表。三个月后回头看,你会发现大部分假设都被推翻了——但你从每次推翻中学到的东西比确认更多。这些记录就是你的成长轨迹。
**一句话:** 策略层面的方法论你已经有了;接下来三年里能不能跑通,取决于你“耐心 / 纪律 / 记录”这三条软纪律守得有多严。这是带得走的“心态地图”。
+489
View File
@@ -0,0 +1,489 @@
# 第九章:持续寻找盈利机会:因子研究入门
前面八章,我们从零搭建了一个完整的量化策略——选标的、定仓位、择时买卖、验证有效性、避免过拟合、接入执行、持续迭代。策略跑起来了。
**然后呢?**
做完一个策略就结束了吗?量化交易员的日常到底在忙什么?
答案是:**因子研究**。
回头看前面八章的每一个关键决策——用 GDP 选国家、用动量排名买卖——背后都有一个共同的逻辑:找到一个数据指标,用它来做决策。这个数据指标,就是**因子(Factor)**。
量化交易员的日常工作,就是不断寻找新的因子、评估它们的预测能力、组合成更好的信号、改进策略。这一章,我们就来体验这个过程。
### 路线图
**完整跑通一个策略(第一章)→ 选什么标的(第二章)→ 每个买多少(第三章)→ 什么时候买卖(第四章)→ 怎么验证有效(第五章)→ 如何避免自欺欺人(第六章)→ 怎么真正执行(第七章)→ 如何持续改进(第八章)→ 持续找下一个 alpha(第九章本章)**
本章从“回顾已有决策”出发,正式引入因子概念,用三个步骤体验量化研究员的日常工作,三步的问题与方法对照如表 9-1 所示。
**表 9-1 第九章三步实验问题与方法对照**
| 步骤 | 问题 | 方法 |
|------|------|------|
| Step 1 | 前面的决策靠的是什么? | 回顾 → 引入因子概念 → IC 评估方法 → GDP 因子实验 |
| Step 2 | 动量因子到底靠不靠谱? | 25 只 ETF × 多个前向窗口 → IC 分析 → 分组诊断 |
| Step 3 | 因子评估能改进策略吗? | 补充因子 → 发现问题 → 波动率过滤 → 回测验证 |
(操作流程见前言“怎么使用这本书”。)
---
## 9.1 从直觉到因子
回顾一下前面课程中的两个关键决策,对照如表 9-2 所示。
**表 9-2 前面课程中的两个关键决策回顾**
| 章节 | 决策 | 用了什么数据 | 因子名称 | 因子类别 |
|------|------|-------------|---------|---------|
| 第二章选什么 | 选哪个国家的市场 | GDP 总量/增速 | GDP 因子 | 宏观因子 |
| 第三章买多少 | 涨的多买、跌的不买 | 风险调整动量 | 动量因子 | 动量因子 |
你可能没意识到,第二章和第三章里你已经在用因子了。**因子(Factor)**就是你做投资决策时参考的那个数据——GDP 高的国家更值得投、涨得好的多买一点。就像你挑餐厅会看评分、看距离、看价格,这些“参考指标”就是因子。
量化研究的日常工作,就是不断寻找和评估这些因子。
### 怎么评估一个因子好不好?
一个因子好不好,最直接的检验方式是:它的打分排名,和实际收益排名有多像?
这就是 **ICInformation Coefficient,信息系数)** 的核心思想。打个比方——IC 就像考试估分准不准:你预估的前 5 名跟实际前 5 名的重合度越高,说明你的判断越靠谱。
计算方法:每一期,把所有标的按因子值排名,再按下一期的实际收益排名,然后算两个排名的 Spearman 相关系数。Spearman 是相关系数的一种,专门看排名而不是数值——就像比较两次考试的“班级第几名”是否一致,而不是看具体分数。相关系数范围从 -1 到 +1:+1 表示因子排名和收益排名完全一致(完美预测),-1 表示完全相反,0 表示毫无关系。
**IC 均值**是多期 IC 的平均值,反映因子是否长期有效:
- 绝对值 > 0.03:有一定参考价值
- 绝对值 > 0.05:算不错的因子
- 绝对值 > 0.1:非常强的因子(实际中很罕见)
**ICIRIC 信息比率)** = IC 均值 / IC 标准差,衡量 IC 的稳定性。不仅要均值高,还要稳定(偶尔猜中一次的因子不算好,每年都稳定有效才算)——类似夏普比率的思路。
### 动手实验 1:GDP 因子 IC 分析
我们一起把这份 spec 写出来。这次重点看两件新东西:**评估工具先讲清楚再用**,以及**因子计算的关键参数要点名到函数级**。
#### 起草上下文 + 任务
这是收官章第一份 spec,承接前八章的所有实验。上下文段把“学员已经在用因子但没意识到”这个起点交代清楚:
> **上下文**:学员已完成第二至八章。本 spec 对应 notebook `q9-daily-work.ipynb` 的 Step 1,从零引入因子概念和 IC 评估方法,并用 10 国 GDP 数据做第一个因子评估实验。
>
> **任务**:在 notebook 中创建 Step 1 的所有单元格——导入库、因子回顾表、IC 方法说明、GDP 因子 IC 实验、排名对比图、GDP 分析、因子分类全景表。
> 📌 **要点**:收官章 spec 把“评估工具”前置于“评估实验”。要求段先单独要求一个 IC 方法说明 markdown(讲 Spearman、阈值、ICIR),再要求 GDP 实验——把全章共用的“测量尺”先建起来,后面所有 spec 都引用同一套语言。
#### 起草要求:函数签名精确 + 参数全显式
要求段是这份 spec 最长的一节(11 条)。两个最关键的细节:
> **要求**(摘要):前两条规定整体结构,3-5 条是具体的 oxq 模块调用细节。
>
> 1. IC 方法说明 markdown 必须放在 GDP 实验之前——先讲 Spearman 相关系数、IC 阈值(0.03 / 0.05 / 0.1)、ICIR 概念
> 2. GDP 数据下载用 `WorldBankFetcher()` + `FactorDownloader(fetcher, sub="macro")` + `dl.download("gdp", start="2010", end="2024", countries=countries)`
> 3. ETF 价格用 `YFinanceDownloader()` + `LocalMarketDataProvider().get_bars()`,时间范围 2011-01-01 至 2026-12-31
> 4. IC 计算:每个“因子年→收益年”配对,用 `scipy.stats.spearmanr` 算相关系数;同时跑 GDP 总量因子和 GDP 增速因子(`gdp_df.pct_change()`
> 5. 视觉契约:1×2 柱状图(figsize 14×5),正相关蓝色、负相关红色,加 IC 均值虚线;slope chart 选 IC 最大那一年,中美红色粗线高亮、其他灰色
> 📌 **要点**:量化研究 spec 必须把“算什么”具体到函数。“算 IC”不够——`spearmanr` 还是 `pearsonr`?前者看排名相关、后者看数值相关,因子评估默认用 spearman。**任何统计计算都要点名具体函数,避免 AI 在等价函数间随机选。**
> 📌 **要点**:复用现成模块时,spec 第一条要点名所有 import 路径——AI 不知道哪些轮子已经存在,名字写错就重复造轮子。本节涉及 `WorldBankFetcher``FactorDownloader``read_factor``Momentum``RollingVolatility` 等 7 个模块,全部按官方写法显式列出。
#### 起草结果呈现
> **结果呈现**:GDP 数据表(万亿美元) + 10 国 ETF 覆盖表 + 1×2 IC 柱状图 + IC 均值/ICIR 打印 + 排名对比 slope chart + GDP 分析 markdown + 6 类因子分类全景表 + 过渡文案。
完整 spec 在 [`specs/spec-01-factor-intro.md`](https://github.com/xingwudao/xquant-learning/blob/main/q9-daily-work/specs/spec-01-factor-intro.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了 GDP 数据表、IC 柱状图和排名对比图。
这个实验做了什么?先下载 10 个国家的 GDP 数据和对应的指数 ETF 价格数据,然后分别用两个 GDP 因子(总量和增速)来预测下一年的指数收益排名,计算每一年的 IC。
### 运行结果
先看两个因子的 IC 对比,如图 9-1 所示。
![GDP 总量因子和增速因子的年度 IC 对比](images/q9-daily-work/01-gdp-ic-comparison.png)
GDP 总量因子:IC 均值 = 0.1272ICIR = 0.3158。GDP 增速因子:IC 均值 = -0.0390ICIR = -0.1177。
GDP 总量因子的 IC 均值为正,说明 GDP 越大的国家,下一年股市表现确实有略好的倾向。虽然很弱、不稳定,但方向是对的。这就呼应了第二章的选择逻辑——我们根据 GDP 总量挑选了最大的两个经济体(中国和美国),用沪深 300 和纳斯达克来构建候选池,背后是有数据支撑的。
而 GDP 增速因子的 IC 更差,说明“增长快”并不能预测“股市涨得好”——经济增速和股市回报的关系比直觉想的要弱得多。
再看 IC 最高那一年的排名对比图,如图 9-2 所示。
![IC 最高年份的 GDP 排名 vs 收益排名对比](images/q9-daily-work/01-gdp-rank-comparison.png)
左列是 GDP 总量排名,右列是下一年指数收益排名。线越平,说明两个排名越接近(IC 越高);线交叉越多说明排名差异大(IC 低)。中美两国用红色高亮——你能直观感受到“IC 高”意味着什么。
### 结果解读
GDP 因子的局限很明显:年度数据、10 个国家、十几年样本,IC 估计噪音很大。但这正好说明了 IC 的价值——不是拍脑袋觉得“有道理”就行,要用数据验证。
### 因子的分类
因子的世界远不止 GDP 和动量。主要的因子类别如表 9-3 所示,也是你未来挖掘新因子的方向。
**表 9-3 因子分类全景**
| 因子类别 | 含义(一句话) | 常见例子 | 数据来源 |
|---------|--------------|---------|---------|
| 价值 | 便不便宜 | 市盈率、市净率 | 财报数据 |
| 动量 | 涨跌趋势 | N 日收益率、风险调整动量 | 价格数据 |
| 质量 | 公司好不好 | 利润率、负债率 | 财报数据 |
| 波动率 | 稳不稳 | 历史波动率 | 价格数据 |
| 宏观 | 大环境 | GDP、利率 | 经济数据 |
| 另类 | 非传统信息 | 新闻情绪、卫星图像 | 另类数据 |
前面课程里我们用过宏观因子(GDP)和动量因子(风险调整动量)。接下来我们重点研究动量因子——因为它用的是每天都有的价格数据,样本量大、更新快,最适合日常研究。
GDP 因子受限于年度频率和小样本,IC 很不稳定。接下来,我们用日频价格数据、更多标的来评估动量因子——这才是量化研究员的日常节奏。
---
## 9.2 动量因子——涨得好的会继续涨吗?
第三章里我们用动量排名来决定买多少——涨得好的多买,跌的不买。当时在 3 只标的上回测效果不错。
但那只是 3 只标的。动量因子在更大范围内真的有效吗?它在什么环境下会失灵?要回答这些问题,我们需要扩大标的池,用 IC 来严格评估。
### 动手实验 2:动量因子 IC 分析
我们一起把这份 spec 写出来。这次重点看两件新东西:**IC 计算函数的签名要点名到参数**,以及**最低样本数阈值要写明白**。
#### 起草上下文 + 任务
> **上下文**:在 `q9-daily-work.ipynb` 的 Step 1 中已完成库导入和 GDP 因子 IC 实验。已有变量 `downloader`YFinanceDownloader)、`provider`LocalMarketDataProvider)。本 spec 对应 Step 2,把标的池从 10 只扩展到 25 只,用日频数据做动量因子的 IC 分析。
>
> **任务**:在 notebook 中新建 Step 2 的所有单元格——构建 25 只标的池、计算动量因子与前向收益、IC 分析表和图、按波动率分组的 IC 分析、问题总结。
#### 起草要求:函数签名 + 最低样本阈值
要求段的灵魂是把 IC 计算的“工程化模式”用函数签名固化下来——任何新因子来了都按这个模板跑一遍。
> **要求**(摘要):
>
> 1. 25 只标的池:22 国 ETF + 3 个大宗商品(GLD / USO / SLV),时间范围 2015-01-01 至 2025-12-31
> 2. 因子矩阵:`Momentum().compute(df, column="close", period=20)``RollingVolatility().compute(df, column="close", period=20)`、RAM = 动量 / 波动率
> 3. 前向收益窗口 `[10, 20, 40]` 交易日:`df["close"].pct_change(h).shift(-h)`
> 4. 实现 `compute_ic_series(factor_df, fwd_df, min_assets=5)`:每个交易日取因子值和前向收益的交集标的(至少 min_assets 个),用 Spearman 算 IC
> 5. 实现 `compute_grouped_ic(factor_df, fwd_df, vol_df, min_assets=6)`:每天按波动率中位数把标的分高/低两组,组内分别算 Spearman IC;要求交集 ≥12、每组 ≥ min_assets
> 6. 视觉契约:2×3 子图(figsize 18×8),柱状图 `width=2, alpha=0.2, steelblue` + 60 日滚动均值 `darkblue` + IC 均值红色虚线
> 7. 末尾保留 `ic_ts = ic_results["风险调整动量 → 20日"]``fwd_df = fwd_dfs[20]` 给 spec-03
> 📌 **要点**:IC 计算这种“对每天做一次统计、跳过样本不足的天、收集成时间序列”的工程化模式,必须封装成函数——`compute_ic_series(factor_df, fwd_df, min_assets=5)`。函数签名级精度让 AI 没有自由发挥空间,也让学员未来换标的池时有明确的修改入口。
> 📌 **要点**:统计计算的阈值要写明白。`min_assets=5`(整体 IC)和 `min_assets=6`(分组 IC,配合交集 ≥12 的额外门槛)不是凭空选——Spearman 在 ≥5 时方差才可控,分组样本减半后 ≥6 才能稳定。**任何“最少多少样本”的阈值都要进 spec 的参数清单,否则 AI 可能选 3 也可能选 10。**
#### 起草结果呈现
> **结果呈现**:25 只标的覆盖表 + 动量因子矩阵 + 6 组 IC 对比表 + 2×3 IC 时间序列子图 + 分组 IC 表 + 高/低波动 IC 时间序列对比图 + 问题总结 markdown。
完整 spec 在 [`specs/spec-02-momentum-ic.md`](https://github.com/xingwudao/xquant-learning/blob/main/q9-daily-work/specs/spec-02-momentum-ic.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了 25 只标的的覆盖表、IC 对比表和多张 IC 时间序列图。
这个实验做了什么?把标的池从 第二、三章 的 3 只扩展到 25 只全球 ETF(22 个国家/地区 + 黄金/原油/白银),用 2015-2025 的日频数据,分别测试两个动量因子(20 日动量和风险调整动量)在三个前向收益窗口(10/20/40 交易日)上的 IC 表现。然后按每只标的自身的波动率分组,看动量因子在高波动和低波动标的上的表现差异。
### 运行结果
两个动量因子在三个前向窗口上的 IC 对比如表 9-4 所示。
**表 9-4 两个动量因子 × 三个前向窗口的 IC 对比**
| 因子 —— 前向窗口 | IC 均值 | IC 标准差 | ICIR | IC>0 |
|-----------------|--------|----------|------|------|
| 20日动量 —— 10日 | -0.0263 | 0.2794 | -0.0941 | 45.7% |
| 20日动量 —— 20日 | -0.0290 | 0.2731 | -0.1062 | 45.7% |
| 20日动量 —— 40日 | -0.0436 | 0.2794 | -0.1560 | 43.4% |
| 风险调整动量 —— 10日 | -0.0161 | 0.2730 | -0.0589 | 46.0% |
| 风险调整动量 —— 20日 | -0.0193 | 0.2705 | -0.0714 | 46.4% |
| 风险调整动量 —— 40日 | -0.0335 | 0.2770 | -0.1208 | 45.7% |
两个因子在三个窗口上的 IC 均值都接近零——动量因子的截面预测能力很弱。
2×3 的 IC 时间序列图如图 9-3 所示。
![两个动量因子 × 三个前向收益窗口 IC 对比](images/q9-daily-work/02-momentum-ic-grid.png)
图 9-3 每个格子是一个“因子 × 前向窗口”组合的 IC 随时间变化。蓝色柱子是每日 IC,深蓝线是 60 日滚动均值,红色虚线是 IC 均值。可以看到 IC 在零附近大幅波动,正负交替——说明动量因子的预测能力非常不稳定。
关键发现来自**按波动率分组的 IC 分析**——按标的自身波动率分组的 IC 表现如表 9-5 所示。
**表 9-5 动量因子在高波动 vs 低波动标的上的 IC 表现**
| 因子 | 整体 IC | 高波动组 IC | 低波动组 IC | Spread |
|------|--------|-----------|-----------|--------|
| 20日动量 | -0.0290 | -0.0334 | -0.0119 | 0.0215 |
| 风险调整动量 | -0.0193 | -0.0354 | -0.0011 | 0.0343 |
直观对比如图 9-4 所示。
![风险调整动量因子:高波动 vs 低波动标的 IC 对比](images/q9-daily-work/02-grouped-ic-comparison.png)
### 结果解读
分组分析揭示了一个重要规律:**同一天内,高波动标的的动量 IC 明显差于低波动标的**。
波动率大的资产,价格噪音更大,动量信号就更容易被噪音淹没,指错方向。风险调整动量(RAM = 动量 / 波动率)在低波动组表现更好,说明除以波动率确实有帮助——但对高波动标的依然救不了。
结论:**一个因子不够**。动量因子在高波动环境下失灵,我们需要补充能感知波动率环境的因子,在高波动时调整策略行为。
---
## 9.3 从因子到策略——让数据说话
我们已经知道了动量因子的弱点。现在的问题是:这个发现能不能帮我们改进策略?
### 动手实验 3:补充因子 + 策略改进
我们一起把这份 spec 写出来。这次重点看两件新东西:**用包装类把因子的弱点翻译成过滤规则**,以及**教学简化声明**——哪些参数是为了演示效果而非工程最优。
#### 起草上下文 + 任务
> **上下文**:在 `q9-daily-work.ipynb` 的 Step 2 中已完成 25 只标的的动量因子 IC 分析和分组分析。已有变量:`all_data``vol_df``fwd_dfs``ic_results``compute_ic_series` 函数、`downloader`。本 spec 对应 Step 3——补充因子、总结发现、改进策略并回测对比。
>
> **任务**:在 notebook 中新建 Step 3 的所有单元格——补充因子 IC、因子评估发现、策略改进框架、第三章三只标的基准回测、波动率过滤改进、对比分析。
#### 起草要求:包装类 + 教学简化声明
要求段把“研究 → 工程”这条路具象化:因子评估发现“动量在高波动时容易出错”,怎么把这个发现翻译成代码里能跑的过滤规则?答案是**包装一个 optimizer**——保留原 optimizer 的所有行为,只在末尾“打补丁”。
> **要求**(摘要):
>
> 1. 补充因子 IC:复用 `vol_df`(波动率因子)+ 新建 `rev_df = -df["close"].pct_change(5)`(短期反转因子),用 `compute_ic_series` 算 20 日前向 IC,三因子对比表
> 2. 第三章三只标的(510300.SS / 513100.SS / 518880.SS+ 基准策略:`Threshold` 信号、`TopNRankingOptimizer(score_col="ram", n=3, filter_negative=True)``RebalanceFrequencyRule(interval_days=21)`、data_start=“2020-06-01”
> 3. 实现 `VolFilteredOptimizer` 包装类:接受 `base_optimizer``vol_threshold``market_vol` 参数;`optimize()` 先调用 base 拿到权重、再从 indicators 取最新日期查 market_vol、若 > 阈值则把所有持仓权重减半、多余分配给 CASH;暴露 `name``required_indicators`
> 4. 阈值取市场平均波动率的中位数(教学场景刻意“过滤狠”——50% 时间减仓,让对比效果显著)
> 5. 视觉契约:净值曲线对比图(figsize 14×7),基准蓝 #3498DB、过滤橙 #E67E22,归一化到起点 = 100,图例标注夏普比率
> 📌 **要点**:把研究发现的因子弱点翻译成代码里的过滤规则时,首选**包装一个新类**——保留原 optimizer 的所有行为,只在末尾“打补丁”。这样原 optimizer 不动、新行为可插可拔,未来想换过滤逻辑只换包装。
>
> 📌 **要点**:包装类要复用原类的契约接口——`VolFilteredOptimizer` 接收一个 `base_optimizer` 引用,复用它的 `required_indicators`,只在 `optimize()` 末尾插入波动率检查。这样调用方完全感知不到差别,可以无痛切换。
> 📌 **要点**:教学场景的参数选择必须在 spec 里声明“是简化、不是最优”。波动率阈值取中位数(50% 时间减仓)、仓位减半(不是 0.7× 也不是 0.3×)——都是为了让对比效果显著的教学选择。**spec 写明这一点,学员仿写时才知道哪些数字可以、应该被改。**
#### 起草结果呈现
> **结果呈现**:三因子 IC 对比表 + 因子评估发现 markdown + 策略改进方向 markdown + 第三章三只标的覆盖信息 + 基准回测指标 + 阈值与高波动天数占比 + 过滤策略回测指标 + 指标对比表 + 净值曲线对比图 + 动态分析文本 + 小结 markdown。
完整 spec 在 [`specs/spec-03-factor-to-strategy.md`](https://github.com/xingwudao/xquant-learning/blob/main/q9-daily-work/specs/spec-03-factor-to-strategy.md)——复制给 AI,弹窗选「允许」。
AI 助手执行完毕后,你的 notebook 里应该出现了三因子 IC 对比表、基准策略和波动率过滤策略的回测结果,以及净值曲线对比图。
这个实验做了什么?分三步走:
1. **补充因子**:在 25 只 ETF 上计算波动率因子和短期反转因子的 IC,与动量因子对比
2. **基准回测**:回到第三章的三只标的(沪深300、纳指100、黄金),用 oxq 复刻第三章的动量排名策略作为基准
3. **改进回测**:基于因子评估的发现(高波动时动量预测力下降),给策略加上波动率过滤——高波动时自动降低仓位
### 运行结果
三因子在 25 只 ETF、20 日前向窗口上的 IC 对比如表 9-6 所示。
**表 9-6 三因子 IC 对比(25 只 ETF,20 日前向窗口)**
| 因子 | IC 均值 | IC 标准差 | ICIR | IC>0 |
|------|--------|----------|------|------|
| 风险调整动量 | -0.0193 | 0.2705 | -0.0714 | 46.4% |
| 波动率 | 0.0020 | 0.2643 | 0.0076 | 49.1% |
| 短期反转 | 0.0183 | 0.2786 | 0.0659 | 52.6% |
三个因子各有侧重——动量看趋势、波动率看风险、反转看短期超跌。它们在不同环境下各有优势。
### 因子评估的发现
关键发现是:**动量因子在高波动标的上 IC 为负,在低波动标的上接近零**——波动率是动量因子失灵的主要原因。这个发现直接指向一个改进方向:在使用动量因子做决策时,应该考虑波动率环境。
### 因子评估的发现能改进策略吗?
我们在 25 只 ETF 上做了因子评估,现在回到 第二、三章 的三只标的(沪深300、纳指100、黄金),看看这些发现能不能改进策略。
回顾一下贯穿全书的两层选择框架:
- **第一层:GDP 因子选候选池**——在 Step 1 中,GDP 因子帮我们从全球市场中筛出了有潜力的国家/资产类别
- **第二层:动量因子做择时**——在候选池内,用动量排名决定买谁、买多少
因子评估告诉我们,动量在高波动时容易出错。如果我们能让策略在高波动时自动降低仓位,应该能改善表现。
基准策略与改进策略的回测对比如表 9-7 所示。
**表 9-7 基准(动量排名)vs 改进(波动率过滤)策略对比**
| 策略 | 累计收益 | 年化波动率 | 最大回撤 | 夏普比率 |
|------|---------|----------|---------|---------|
| 基准(动量排名) | +113.36% | 15.01% | -13.32% | 1.09 |
| 改进(波动率过滤) | +92.96% | 11.00% | -10.17% | 1.25 |
净值曲线对比如图 9-5 所示。
![因子评估 —— 策略改进:基准 vs 波动率过滤](images/q9-daily-work/03-strategy-comparison.png)
基准策略夏普 1.09,最大回撤 -13.32%。波动率过滤后夏普 1.25,最大回撤 -10.17%,回撤改善 23.7%。因子评估告诉我们「高波动时动量 IC 翻负」,加上波动率过滤后回撤确实下降了——这就是因子评估带来的 Alpha,不是靠猜,而是靠数据发现问题、改进策略。
### 结果解读
波动率过滤带来了明显的改善:夏普比率从基准的 1.09 提升到 1.25,最大回撤从 -13.3% 降低到 -10.2%——回撤改善了约 24%。
代价是什么?累计收益从 113% 降到了 93%——高波动时降仓位,自然少赚了一些。但风险调整后的收益(夏普)更好了。这正是因子评估带来的价值:**不是让你赚更多,而是让你在承担同样风险的情况下赚得更稳**。
这个改进不是靠猜测,而是因子评估告诉我们的——IC 分析发现高波动时动量 IC 翻负,我们据此加了波动率过滤,回测验证了确实有效。这就是因子研究带来的 Alpha。
---
## 9.4 量化交易员的一周长什么样
到这里你已经掌握了“找因子 → 评估 → 改进策略”的完整流程。但每天打开电脑,到底先做什么、再做什么?光知道“应该会做”还不够,章名《量化交易员的日常工作》得给你一份能打印贴在桌面的作业日历。
把前八章的动作串成一条**可执行的研究流**——一个量化交易员典型的一周长什么样:
### 每个交易日(≤ 30 分钟)
**开盘前(5 分钟)。** 看一眼策略的实盘运行状态。监控仪表盘(第八章 Step 1)告诉你“是不是坏了”——滚动夏普还在 0 以上吗?回撤超过你预设的阈值了吗?相对沪深300超额收益还为正吗?三个指标全绿,今天没事;任意一个亮红灯,列入待诊断清单。
**盘后(10-15 分钟)。** 拉今日的成交回报,跑一次执行对账(第七章 Step 4)——回测口径下应该成交什么、实盘实际成交了什么、滑点和佣金加起来是不是在你的预算内。差距稳定 = 健康;差距突然放大 → 列入待诊断清单。
**晚间(10 分钟)。** 把今天的关键事件写进迭代记录表(第八章 Step 3)。“今天恶化时段触发”、“某只 ETF 触发止损”、“市场状态切到高波动”——三个月后这张表是你最值钱的资产。
### 每周(2-3 小时,建议周末)
**周一上午:监控扫描 + 诊断分流。** 把过去一周的滚动夏普、回撤、超额收益拉出来,对比上周——稳定还是恶化?如果有恶化时段,跑诊断三问(第八章 Step 2):先排执行落差(外)、再查市场状态(中)、最后看策略本身(内)。
**周末半天:因子探索 / 假设实验。** 两种典型节奏二选一:
- **因子探索(第九章流程)**:观察市场最近的特征 → 提一个新因子假设 → 跑 IC + 分组分析 → 决定是否纳入策略候选池。
- **假设驱动实验(第八章 Step 3)**:写下假设和验证标准 → 跑对照回测 → 看结果确认还是推翻假设。**纪律:标准定下来就不能事后改。**
### 每月(半天)
**月度复盘。** 拉本月的全部迭代记录,问三个问题:①假设确认率多少?②哪些假设被推翻、原因是什么?③下个月的研究方向?复盘的目的不是论功行赏,是让“疑”持续运转——上个月你以为对的东西,本月数据是不是还支持?
**工具升级。** 数据源更新了吗?open-xquant 有新模块要试吗?哪条 spec 你写第三遍了——可以模板化吗?这一步是给“做”持续松绑——让单位时间能做的实验更多。
### 每季度(1-2 天)
**季度策略检阅(第五 + 六章工具全用上)。** 跑一次完整的策略体检:基准对比、分年拆解、收益分布、参数敏感性、Walk-forward、交叉验证、规则负担——五把铲子(含第五章敏感性)每把都挥一次。任意一项亮红灯,启动重新设计或退役流程。
**视野扩展。** 读 1-2 篇论文 / 行业报告 / 优秀公开策略——不是为了立刻抄,是为了看看世界变到哪了。一个量化交易员退化的标志,是他几个月没读过任何外部输入。
### 一张可贴桌面的作业速查
把上面的节奏压成一张速查表,对应飞轮和三字要点的位置如表 9-8 所示。
**表 9-8 量化交易员的作业速查**
| 周期 | 主要动作 | 飞轮位置 | 三字要点 |
|------|---------|---------|---------|
| 每日 | 监控仪表盘 + 执行对账 + 写迭代记录 | 归因层 + 执行层 | 看 |
| 每周 | 监控扫描 + 诊断分流 + 因子/假设实验 | 归因层 + 候选/组合层 | 看 + 疑 + 做 |
| 每月 | 月度复盘 + 工具升级 | 归因层(反哺起点) | 疑 + 做 |
| 每季度 | 五把铲子全检阅 + 视野扩展 | 全飞轮 | 做 + 看 + 疑 |
**节奏比“努力”重要。** 量化不是靠加班赚钱——是靠**让飞轮持续转、让坏假设尽早被推翻、让好假设尽快上线**。一周一次因子实验、一个月一次复盘、一个季度一次大检阅——只要持续做,三年后的你会让现在的你认不出来。
---
## 9.5 本章总结
### 策略进化路径
三步走,每一步都有明确的产出:
- **Step 1**:因子概念 + IC 评估方法(从直觉到数据验证)
- **Step 2**:动量因子 IC 分析(发现高波动是弱点)
- **Step 3**:因子评估改进策略(波动率过滤提升夏普)
### 概念速查表
本章涉及的核心概念汇总如表 9-9 所示,方便随时回查。
**表 9-9 第九章核心概念速查**
| 概念 | 含义 | 类比 |
|------|------|------|
| 因子(Factor) | 用来预测资产收益的数据指标 | 挑餐厅时看的评分、距离、价格 |
| IC(信息系数) | 因子排名和收益排名的相似度,衡量因子预测能力 | 考试预测分和实际分的排名对比 |
| ICIR | IC 均值 / IC 标准差,衡量 IC 的稳定性 | 夏普比率的因子版——不仅要准,还要稳 |
| 截面分析 | 同一时间点比较不同标的 | 同一次考试比较不同学生的成绩 |
| 两层选择框架 | GDP 因子选候选池 + 动量因子做择时 | 先选城市(宏观),再选餐厅(微观) |
### 本章核心认知
走完三个 Step 后,本章最值得带走的三条 q9 独有认知如表 9-10 所示——其余共通认知由 9.6 飞轮 + 三字要点收束。
**表 9-10 第九章核心认知**
| 认知 | 来源 |
|------|------|
| 波动率是动量因子的关键弱点——高波动标的的 IC 明显更差 | Step 2 |
| 动量因子的截面预测力接近零,回测有效不等于因子有效 | Step 2 |
| 因子评估带来的 Alpha 来自发现因子的弱点,并把弱点转化为过滤规则 | Step 3 |
> 本章所有代码的可运行版本见 `notebooks/q9-daily-work.ipynb`
---
## 9.6 写给未来的你
这是全书的最后一章。回头看看你走过的路——把整本书带给你的东西,浓缩成一张图:**飞轮 + 三字要点 + 一句口号**。
**飞轮的四层你都站过。** 候选层(第二章选 ETF)→ 组合层(第三章定仓位 / 第四章加规则)→ 执行层(第七章下单)→ 归因层(第五、六、八章评估 / 防过拟合 / 监控诊断)→ 这一章的因子研究反过来回到候选与组合层,进入下一圈。这就是“飞轮转完一圈”——每转一圈都比上一圈聪明一点。
**三字要点你都练过。**
- **做** —— 把动作写成 spec,让 AI 把它变成可重复执行的规则。从第一章的均线、第三章的三种信号、第四章的止损止盈、到第八章的对照实验,每一次都是“做”。
- **看** —— 不允许“感觉还行”,所有判断都要落到指标上。年化、波动率、最大回撤、夏普 / 卡玛 / 索提诺、IC、ICIR——你已经收集了一整套尺子。
- **疑** —— 永远怀疑你的指标。第一章撞墙、第六章四把铲子、第八章诊断三问、第九章 IC 分组——你已经把“疑”从感觉训练成了流程。
**一句口号你能复述了:「做要规则、看要指标、还要怀疑你的指标。」** 会做、会看,但不会疑,最容易在某些时候把赚到的钱加倍还给市场。这正是为什么“疑”和“做”“看”在本书同等重要。
**你没有写过一行代码**——每一步都是通过撰写清晰的任务说明书(spec),让 AI 来执行。这不是偷懒,这是未来量化的工作方式。**Spec 写作能力本身就是本书最重要的一份带走品**:从第一章的“复制临摹”,到第三、四章的“改参数”,到第六章起的“自己拆四段式”,到第九章的“独立设计研究流程”——你的 spec 能力已经走完了三个台阶。
量化交易的门槛正在快速降低。以前需要金融学位 + 编程能力 + 大量资金,现在你只需要:清晰的思考 + 数据验证的习惯 + 一个好的 AI 助手。
但门槛降低不等于赚钱变容易了。市场不会因为工具变好就多给你钱。真正的竞争力在于:
- **更好的因子(做)**:别人没发现的数据关系,让“做什么”持续翻新
- **更快的迭代(看 + 疑)**:从假设到验证的周期越短越好,让“看 / 疑”快到几乎和市场同步
- **更强的纪律(疑)**:不被情绪左右,按数据做决策,不让“疑”被自己的乐观腐蚀
这三件事,恰好可以交给一个被研究纪律约束好的 AI Agent 去高速放大——具体怎么入门见本章末「拓展阅读」。
**飞轮在你手里——下一圈,让它转得更快、更稳、更聪明。** 祝你在量化的路上走得远。
---
## 拓展阅读
### 让 Agent 替你跑因子研究
整章你做的因子研究流程是高度结构化的——找因子、查 IC、看分组、改策略,每一步做什么、看什么指标都有明确标准。**结构化的流程最适合交给 AI Agent 执行。**
但通用 AgentOpenClaw、Cursor Agent 等)什么都能做但什么都做不精——拿来做量化研究容易跳过 IC 评估、用不可复现的方式处理数据、对 IC=0.01 的因子说“还不错”。这些都是研究纪律问题。
**Harness Engineering(缰绳工程)** —— 给 Agent 装上身份约束、操作流程、工具限制和技能模板,让“什么都能做”变成“按规矩做”。比喻来自驯马:马很强壮但需要缰绳,Agent 能力强但需要研究纪律。
### 用 open-xquant 让 Agent 接管你的因子研究
open-xquant 在 `agent/bootstrap/` 目录提供了一套量化研究专用的 harness:
- **SOUL.md** —— Agent 的身份和底线。最核心的约束是 **“不可复现 = 不可信 = 不可交易”**,通用 Agent 不会自带这条量化纪律
- **AGENT.md** —— 研究 SOP(5 阶段:业务约束 → 目标 → 5 要素假设 → 数据准备 → 逐层构建),每一步都要你确认才推进,不会偷跑
- **TOOLS.md** —— 工具清单(通过 MCP Server 暴露 62 个 SDK 工具)
- **19 个 skill 文件** —— 覆盖从策略构建、因子评估到实盘交易的全流程
**怎么用?** 用 `agent/bootstrap/` 覆盖 OpenClaw 默认文件 → 让 Agent 自行安装 open-xquant skills → 配置 MCP Server。然后你用自然语言提研究需求,Agent 按 SOP 引导你完成。具体安装步骤见 [open-xquant 仓库](https://github.com/xingwudao/open-xquant)。
### 更多因子、更复杂的组合——机器学习的入口
本课程只用了三个因子做演示。实际的量化研究中,因子数量可能是几十甚至上百个。当因子多了,手工写 if-else 规则来组合就不现实了——阈值怎么定?因子之间的交互关系怎么处理?
这时候就需要机器学习:
- **决策树 / 随机森林**:自动学习因子之间的分裂规则,可解释性好
- **梯度提升树(XGBoost / LightGBM**:量化行业最常用的多因子组合方法
- **神经网络**:能捕捉更复杂的非线性关系,但需要更多数据
核心思路都一样:把因子当作特征,未来收益当作标签,让模型学出最优的组合方式。这是你未来进入实战后可以深入探索的方向——但本书的主线已经够你跑很久了,**先把第八章的迭代闭环跑顺,再考虑加机器学习**。
+59
View File
@@ -0,0 +1,59 @@
# XQuant Beginner
《XQuant:人人都是量化交易员》开源书稿。
本仓库存放可发布的书稿源码、图片和发布配置。书中配套的
spec 与 notebook 不放在这里,统一维护在:
`https://github.com/xingwudao/xquant-learning`
## 开始阅读
- [前言](book/preface.md)
- [开始动手之前](book/getting-started.md)
- [第 1 章:第一次跑通一个完整策略](book/q1-how-to-profit.md)
完整目录见 [SUMMARY.md](SUMMARY.md)。
## 在线发布
本仓库支持两种发布方式:
- GitBook:通过 `.gitbook.yaml``README.md``SUMMARY.md` 同步发布
- 静态网页:通过 VitePress 构建为纯静态站点
## 本地预览静态站点
先安装 Node.js,然后运行:
```bash
npm install
npm run docs:dev
```
构建静态站点:
```bash
npm run docs:build
```
预览构建结果:
```bash
npm run docs:preview
```
构建产物位于 `.vitepress/dist/`
## 配套练习仓库
书中所有动手实验对应的 spec 和 notebook 位于:
`https://github.com/xingwudao/xquant-learning`
## 许可证
本仓库采用双许可证:
- 书稿正文与图片:`CC BY-NC-SA 4.0`
- 构建脚本、配置文件和自动化工作流:`MIT`
+2636
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "xquant-beginner",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"docs:dev": "vitepress --host 0.0.0.0",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview --host 0.0.0.0"
},
"devDependencies": {
"vitepress": "^1.6.4"
},
"overrides": {
"vite": "6.4.2"
}
}