qka 1.6.2.dev2__tar.gz → 1.6.3.dev8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/.gitignore +4 -1
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/PKG-INFO +1 -1
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/docs/examples/momentum.md +10 -15
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/docs/examples/multi_factor.md +6 -0
- qka-1.6.3.dev8/docs/guides/backtest.md +48 -0
- qka-1.6.3.dev8/docs/guides/data.md +82 -0
- qka-1.6.3.dev8/docs/guides/indicators.md +78 -0
- qka-1.6.3.dev8/docs/guides/report.md +74 -0
- qka-1.6.3.dev8/docs/guides/sizing.md +83 -0
- qka-1.6.3.dev8/docs/guides/strategy.md +114 -0
- qka-1.6.3.dev8/docs/guides/trading.md +86 -0
- qka-1.6.2.dev2/docs/getting-started/quickstart.md → qka-1.6.3.dev8/docs/index.md +31 -14
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/mkdocs.yml +8 -8
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/__init__.py +3 -1
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/core/backtest.py +7 -6
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/core/data.py +41 -6
- qka-1.6.2.dev2/docs/concepts/backtest.md +0 -109
- qka-1.6.2.dev2/docs/concepts/broker.md +0 -113
- qka-1.6.2.dev2/docs/concepts/data.md +0 -110
- qka-1.6.2.dev2/docs/concepts/indicators.md +0 -98
- qka-1.6.2.dev2/docs/concepts/sizing.md +0 -94
- qka-1.6.2.dev2/docs/concepts/strategy.md +0 -181
- qka-1.6.2.dev2/docs/index.md +0 -64
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/.github/workflows/docs.yml +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/.github/workflows/release.yml +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/.vscode/settings.json +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/CHANGELOG.md +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/LICENSE +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/README.md +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/docs/advanced/performance.md +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/docs/api/brokers.md +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/docs/api/core.md +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/docs/api/utils.md +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/docs/examples/buy_and_hold.md +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/docs/examples/ma_cross.md +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/docs/examples/rsi_atr.md +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/docs/user-guide/trading.md +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/pyproject.toml +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/brokers/__init__.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/brokers/client.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/brokers/server.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/brokers/trade.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/cli.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/core/__init__.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/core/accessor.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/core/broker.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/core/report.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/core/sizing.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/core/strategy.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/mcp/__init__.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/mcp/api.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/mcp/server.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/server/__init__.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/server/handlers/__init__.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/server/handlers/class_inspector_handler.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/server/handlers/code_executor_handler.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/server/ws_client.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/server/zmq_server.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/utils/__init__.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/utils/anis.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/utils/logger.py +0 -0
- {qka-1.6.2.dev2 → qka-1.6.3.dev8}/qka/utils/util.py +0 -0
|
@@ -7,32 +7,27 @@
|
|
|
7
7
|
```python
|
|
8
8
|
"""每月末按动量排序,选前 20% 的股票买入"""
|
|
9
9
|
from qka import Data, Strategy, Broker, Backtest
|
|
10
|
-
import numpy as np
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class Momentum(Strategy):
|
|
14
13
|
def __init__(self):
|
|
15
14
|
super().__init__()
|
|
16
15
|
self.broker = Broker(initial_cash=1_000_000)
|
|
17
|
-
self.
|
|
18
|
-
self.holdings = []
|
|
16
|
+
self.last_month = None
|
|
19
17
|
|
|
20
18
|
def on_bar(self, date):
|
|
21
|
-
#
|
|
19
|
+
# 月末附近调仓
|
|
22
20
|
if date.day < 28:
|
|
23
21
|
return
|
|
24
22
|
|
|
25
|
-
#
|
|
26
|
-
|
|
23
|
+
# 本月已调仓,跳过
|
|
24
|
+
month_key = (date.year, date.month)
|
|
25
|
+
if self.last_month == month_key:
|
|
27
26
|
return
|
|
28
|
-
self.
|
|
29
|
-
|
|
30
|
-
# 下个月的第一天重置标记
|
|
31
|
-
if date.day >= 28:
|
|
32
|
-
pass # 保持标记,等跨月再重置
|
|
27
|
+
self.last_month = month_key
|
|
33
28
|
|
|
34
29
|
close = self.get('close')
|
|
35
|
-
if close is None or close.empty
|
|
30
|
+
if close is None or close.empty:
|
|
36
31
|
return
|
|
37
32
|
|
|
38
33
|
# 过去 60 个交易日的动量
|
|
@@ -40,7 +35,7 @@ class Momentum(Strategy):
|
|
|
40
35
|
if hist is None or hist.empty:
|
|
41
36
|
return
|
|
42
37
|
|
|
43
|
-
#
|
|
38
|
+
# 计算每只股票的阶段收益率
|
|
44
39
|
ret = {}
|
|
45
40
|
for sym in hist.columns:
|
|
46
41
|
prices = hist[sym].dropna()
|
|
@@ -55,7 +50,7 @@ class Momentum(Strategy):
|
|
|
55
50
|
top_n = max(1, len(sorted_syms) // 5)
|
|
56
51
|
buy_list = sorted_syms[:top_n]
|
|
57
52
|
|
|
58
|
-
#
|
|
53
|
+
# 卖出不在列表里的持仓
|
|
59
54
|
for sym in list(self.broker.positions.keys()):
|
|
60
55
|
if sym not in buy_list:
|
|
61
56
|
pos = self.broker.positions[sym]
|
|
@@ -63,7 +58,7 @@ class Momentum(Strategy):
|
|
|
63
58
|
if price > 0:
|
|
64
59
|
self.broker.sell(sym, price, pos['size'])
|
|
65
60
|
|
|
66
|
-
#
|
|
61
|
+
# 买入列表中的新标的
|
|
67
62
|
cash_per_sym = self.broker.cash / len(buy_list)
|
|
68
63
|
for sym in buy_list:
|
|
69
64
|
if sym in self.broker.positions:
|
|
@@ -13,6 +13,7 @@ class MultiFactor(Strategy):
|
|
|
13
13
|
def __init__(self):
|
|
14
14
|
super().__init__()
|
|
15
15
|
self.broker = Broker(initial_cash=1_000_000)
|
|
16
|
+
self.last_month = None
|
|
16
17
|
|
|
17
18
|
def on_bar(self, date):
|
|
18
19
|
close = self.get('close')
|
|
@@ -23,6 +24,11 @@ class MultiFactor(Strategy):
|
|
|
23
24
|
if date.day < 28:
|
|
24
25
|
return
|
|
25
26
|
|
|
27
|
+
month_key = (date.year, date.month)
|
|
28
|
+
if self.last_month == month_key:
|
|
29
|
+
return
|
|
30
|
+
self.last_month = month_key
|
|
31
|
+
|
|
26
32
|
rsi = self.get('rsi_14')
|
|
27
33
|
hist = self.history('rsi_14', 20)
|
|
28
34
|
if hist is None or hist.empty:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# 回测
|
|
2
|
+
|
|
3
|
+
使用 `Backtest` 运行策略并获取回测结果。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 基本用法
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from qka import Backtest
|
|
11
|
+
|
|
12
|
+
bt = Backtest(data, MyStrategy())
|
|
13
|
+
bt.run(benchmark='000300.SH')
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`Backtest` 的参数:
|
|
17
|
+
|
|
18
|
+
| 参数 | 说明 |
|
|
19
|
+
|------|------|
|
|
20
|
+
| `data` | Data 实例 |
|
|
21
|
+
| `strategy` | 策略实例(可在构造函数中传入 cash 等参数) |
|
|
22
|
+
|
|
23
|
+
## run 方法
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
bt.run(benchmark='000300.SH') # 带沪深 300 基准对比
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
执行流程:按时间排序数据 → 逐日调用 `on_bar(date)` → 记录每日资产变化 → 下载基准数据。
|
|
30
|
+
|
|
31
|
+
大规模回测(数百只股票、数年数据)时,QKA 采用分批计算策略,避免一次性加载全部数据到内存。
|
|
32
|
+
|
|
33
|
+
## 查看结果
|
|
34
|
+
|
|
35
|
+
- `summary()` — 终端输出绩效指标
|
|
36
|
+
- `report()` — 生成 HTML 报告
|
|
37
|
+
|
|
38
|
+
详见[报告](report.md)。
|
|
39
|
+
|
|
40
|
+
## 基准对比
|
|
41
|
+
|
|
42
|
+
A 股最常使用沪深 300 指数:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
bt.run(benchmark='000300.SH')
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
启用基准后,summary 和 report 中会增加超额收益、超额夏普等指标。
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 数据
|
|
2
|
+
|
|
3
|
+
回测的第一步是获取数据。QKA 负责下载、缓存和合并,用户只需指定股票代码。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 基本用法
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
data = Data(symbols=['000001.SZ', '600000.SH'])
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
以上代码会:
|
|
14
|
+
|
|
15
|
+
1. 从 baostock 下载日线数据
|
|
16
|
+
2. 每只股票保存为独立的 parquet 文件到本地缓存
|
|
17
|
+
3. 下次运行时直接读取缓存,无需重复下载
|
|
18
|
+
|
|
19
|
+
## 参数
|
|
20
|
+
|
|
21
|
+
| 参数 | 作用 | 默认值 |
|
|
22
|
+
|------|------|--------|
|
|
23
|
+
| `symbols` | 股票代码列表 | 必填 |
|
|
24
|
+
| `period` | 数据周期 | `'1d'` |
|
|
25
|
+
| `adjust` | 复权方式:`'qfq'`(前复权)、`'hfq'`(后复权)、`'bfq'`(不复权) | `'qfq'` |
|
|
26
|
+
| `source` | 数据源:`'baostock'` 或 `'akshare'` | `'baostock'` |
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
data = Data(symbols=['000001.SZ'], adjust='hfq')
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 缓存位置
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
datadir/
|
|
36
|
+
└── baostock/
|
|
37
|
+
└── 1d/
|
|
38
|
+
└── qfq/
|
|
39
|
+
├── 000001.SZ.parquet
|
|
40
|
+
├── 600000.SH.parquet
|
|
41
|
+
└── ...
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
每只股票独立文件。清除缓存直接删除该目录即可。
|
|
45
|
+
|
|
46
|
+
## 合并后的数据
|
|
47
|
+
|
|
48
|
+
多股票回测时,QKA 将各股票合并为一张宽表,列名格式为 `{股票代码}_{字段名}`:
|
|
49
|
+
|
|
50
|
+
| 日期 | 000001.SZ_close | 600000.SH_close |
|
|
51
|
+
|------|----------------|----------------|
|
|
52
|
+
| 2024-01-02 | 10.2 | 8.1 |
|
|
53
|
+
| 2024-01-03 | 10.5 | 8.3 |
|
|
54
|
+
|
|
55
|
+
策略中无需关心此格式——`self.get('close')` 自动解析为横截面 Series。
|
|
56
|
+
|
|
57
|
+
## 两种加载模式
|
|
58
|
+
|
|
59
|
+
**普通模式** — 适用于少量股票(几十只以内):
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
data = Data(symbols=['000001.SZ'])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**懒加载(lazy)** — 适用于大规模数据(数百只股票),分批计算,降低内存占用:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
bt.run(lazy=True)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
两种模式下策略代码完全一致,`self.get()` 和 `self.history()` 用法不变。
|
|
72
|
+
|
|
73
|
+
## 数据源选择
|
|
74
|
+
|
|
75
|
+
| 数据源 | 优点 | 缺点 |
|
|
76
|
+
|--------|------|------|
|
|
77
|
+
| **baostock**(默认) | 稳定,开箱即用 | 单线程下载 |
|
|
78
|
+
| **akshare** | 支持并发 | 个股接口偶发屏蔽 |
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
data = Data(symbols=['000001.SZ'], source='akshare')
|
|
82
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# 指标
|
|
2
|
+
|
|
3
|
+
策略中经常需要均线、RSI、MACD 等技术指标。QKA 在数据加载时一次性预计算完毕,策略中直接读取。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 基本用法
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
data = Data(
|
|
11
|
+
symbols=['000001.SZ'],
|
|
12
|
+
indicators={
|
|
13
|
+
'sma_20': ('sma', 20),
|
|
14
|
+
'rsi_14': ('rsi', 14),
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
策略中直接调用:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
def on_bar(self, date):
|
|
23
|
+
sma = self.get('sma_20')
|
|
24
|
+
rsi = self.get('rsi_14')
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 支持的指标
|
|
28
|
+
|
|
29
|
+
| 指标 | 写法 | 生成列 |
|
|
30
|
+
|------|------|--------|
|
|
31
|
+
| 简单均线 | `('sma', 20)` | `sma_20` |
|
|
32
|
+
| 指数均线 | `('ema', 14)` | `ema_14` |
|
|
33
|
+
| 加权均线 | `('wma', 30)` | `wma_30` |
|
|
34
|
+
| RSI | `('rsi', 14)` | `rsi_14` |
|
|
35
|
+
| ATR | `('atr', 14)` | `atr_14` |
|
|
36
|
+
| MACD | `('macd', 12, 26, 9)` | `macd`、`macd_signal`、`macd_histogram` |
|
|
37
|
+
| 布林带 | `('bbands', 20, 2)` | `bbands_upper`、`bbands_middle`、`bbands_lower` |
|
|
38
|
+
|
|
39
|
+
## 指定计算字段
|
|
40
|
+
|
|
41
|
+
默认以收盘价(close)计算。如需对其他字段计算:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
indicators={
|
|
45
|
+
'sma_high_10': ('sma', 'high', 10), # 对最高价计算均线
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 自定义指标
|
|
50
|
+
|
|
51
|
+
通过函数实现任意自定义计算逻辑:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
def add_ma5(df):
|
|
55
|
+
"""每只股票单独计算 5 日均线"""
|
|
56
|
+
df['ma5'] = df['close'].rolling(5).mean()
|
|
57
|
+
return df
|
|
58
|
+
|
|
59
|
+
data = Data(symbols=['000001.SZ'], indicators=add_ma5)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
函数接收每只股票的 DataFrame,返回添加新列后的 DataFrame。可与内置指标混用:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
indicators={
|
|
66
|
+
'sma_20': ('sma', 20),
|
|
67
|
+
'ma5': add_ma5,
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 预计算与动态计算对比
|
|
72
|
+
|
|
73
|
+
| 方式 | 计算次数 | 策略中的用法 |
|
|
74
|
+
|------|---------|-------------|
|
|
75
|
+
| 预计算 | 加载时计算 1 次 | `self.get('sma_20')` |
|
|
76
|
+
| 动态计算 | 每根 bar 计算 1 次 | 每 bar 手动调用 rolling |
|
|
77
|
+
|
|
78
|
+
股票数量越多、回测周期越长,预计算的性能优势越明显。
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# 报告
|
|
2
|
+
|
|
3
|
+
回测完成后,QKA 提供终端概览和 HTML 报告两种方式查看结果。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## summary — 终端概览
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
bt.summary()
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
输出示例:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
QKA 回测报告 — MyStrategy
|
|
17
|
+
────────────────────────────────────────
|
|
18
|
+
初始资金: ¥100,000.00
|
|
19
|
+
最终资产: ¥178,233.45
|
|
20
|
+
总收益率: +78.23%
|
|
21
|
+
年化收益率: +11.34%
|
|
22
|
+
最大回撤: -32.15%
|
|
23
|
+
夏普比率: 0.68
|
|
24
|
+
交易次数: 47
|
|
25
|
+
胜率: 61.70%
|
|
26
|
+
────────────────────────────────────────
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 指标说明
|
|
30
|
+
|
|
31
|
+
| 指标 | 说明 |
|
|
32
|
+
|------|------|
|
|
33
|
+
| 总收益率 | 回测期间总资产涨幅 |
|
|
34
|
+
| 年化收益率 | 折算为年化后的收益率,便于不同时长策略间的比较 |
|
|
35
|
+
| 最大回撤 | 历史最高点到最低点的最大跌幅 |
|
|
36
|
+
| 夏普比率 | 单位风险对应的超额收益。> 1 为良好,> 2 为优秀 |
|
|
37
|
+
| 胜率 | 盈利交易占总交易笔数的比例 |
|
|
38
|
+
| 交易次数 | 总交易笔数。次数过少时,胜率和收益率的统计意义有限 |
|
|
39
|
+
|
|
40
|
+
启用 benchmark 后会多出:
|
|
41
|
+
|
|
42
|
+
| 指标 | 说明 |
|
|
43
|
+
|------|------|
|
|
44
|
+
| 超额收益 | 策略相对基准的超额回报 |
|
|
45
|
+
| 超额夏普 | 超额收益的夏普比率 |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## report — HTML 报告
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
bt.report(title='我的策略', output_path='./my_report.html')
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
浏览器自动打开。不传 `output_path` 时,默认保存到 `./reports/` 目录。
|
|
56
|
+
|
|
57
|
+
### 报告内容
|
|
58
|
+
|
|
59
|
+
- **绩效指标卡片** — summary 中的关键数字,卡片式展示
|
|
60
|
+
- **净值曲线** — 资产变化曲线,支持基准对比
|
|
61
|
+
- **月度收益热力图** — 各月收益百分比,绿色表示盈利,红色表示亏损
|
|
62
|
+
- **交易明细列表** — 每笔交易的买卖时间、价格、股数、盈亏
|
|
63
|
+
|
|
64
|
+
### 手机查看
|
|
65
|
+
|
|
66
|
+
报告是自包含的 HTML 文件,使用 Plotly 纯前端渲染,无需启动服务器。双击即可打开,通过微信或邮件分享后,在手机上同样可正常查看。
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 备注
|
|
71
|
+
|
|
72
|
+
- `bt.report()` 返回 `Path` 对象,可使用 `str(report_path)` 获取文件路径
|
|
73
|
+
- 多次调用 `bt.run()` 会覆盖前次回测的报告
|
|
74
|
+
- 交易次数过少时,胜率 100% 也可能不具备统计意义,需结合交易次数综合判断
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# 仓位
|
|
2
|
+
|
|
3
|
+
固定股数(如 100 股)无法适配不同资金规模。`self.sizing` 根据可用资金自动计算合理买入股数。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 方法一览
|
|
8
|
+
|
|
9
|
+
| 方法 | 适用场景 |
|
|
10
|
+
|------|---------|
|
|
11
|
+
| `percent(ratio, price)` | 按可用资金比例买入 |
|
|
12
|
+
| `fixed_amount(amount, price)` | 每月固定金额定投 |
|
|
13
|
+
| `fixed_shares(n)` | 固定股数,自动按手取整 |
|
|
14
|
+
| `atr_risk(risk_ratio, price, atr)` | 波动率自适应仓位 |
|
|
15
|
+
| `kelly(win_rate, wl_ratio, price)` | 已知胜率赔率时的最优下注 |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## percent — 比例仓位
|
|
20
|
+
|
|
21
|
+
最常用的方式。用可用资金的一定比例买入:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
size = self.sizing.percent(0.1, price)
|
|
25
|
+
# 可用资金 10 万,price=10 → 1000 股
|
|
26
|
+
# 可用资金 1 万,price=10 → 100 股
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## fixed_amount — 固定金额
|
|
30
|
+
|
|
31
|
+
适合定投场景,每次投入固定金额:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
size = self.sizing.fixed_amount(5000, price)
|
|
35
|
+
# price=10 → 500 股
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## fixed_shares — 固定股数
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
size = self.sizing.fixed_shares(100)
|
|
42
|
+
# 返回 100(现金充足时),不足时返回 0
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## atr_risk — ATR 风控
|
|
46
|
+
|
|
47
|
+
根据波动率动态调整仓位。ATR 较大时减少买入股数,ATR 较小时增加:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
atr = float(self.get('atr_14')[sym])
|
|
51
|
+
size = self.sizing.atr_risk(0.02, price, atr)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
ATR=0.5:`10万 × 2% / (0.5 × 2) = 2000 股`
|
|
55
|
+
ATR=1.0:`10万 × 2% / (1.0 × 2) = 1000 股`
|
|
56
|
+
|
|
57
|
+
波动率翻倍时仓位自动减半。
|
|
58
|
+
|
|
59
|
+
## kelly — 凯利公式
|
|
60
|
+
|
|
61
|
+
适合有历史统计数据的场景:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
size = self.sizing.kelly(0.6, 2.0, price)
|
|
65
|
+
# 胜率 60%,赔率 2,f* = (0.6×2 - 0.4)/2 = 0.4 → 40% 仓位
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
计算结果 ≤ 0 时返回 0(不下注)。
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 注意事项
|
|
73
|
+
|
|
74
|
+
**基于可用现金计算**,不含持仓市值。如需基于总资产:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
total = self.broker.cash
|
|
78
|
+
for sym, pos in self.broker.positions.items():
|
|
79
|
+
total += pos['size'] * pos['cost_basis']
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- ATR 为 0 时返回 0(新股或一字板)
|
|
83
|
+
- 价格必须大于 0,前复权可能出现负价格,此时不交易
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# 策略
|
|
2
|
+
|
|
3
|
+
策略是一个继承 `Strategy` 基类的 Python 类,核心是实现 `on_bar` 方法。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 基本结构
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from qka import Strategy, Broker
|
|
11
|
+
|
|
12
|
+
class MyStrategy(Strategy):
|
|
13
|
+
def __init__(self):
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.broker = Broker(initial_cash=100_000)
|
|
16
|
+
|
|
17
|
+
def on_bar(self, date):
|
|
18
|
+
close = self.get('close')
|
|
19
|
+
if close is None or close.empty:
|
|
20
|
+
return
|
|
21
|
+
# 交易逻辑写在这里
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 生命周期
|
|
25
|
+
|
|
26
|
+
回测按时间顺序逐日推进。每个交易日调用一次 `on_bar(date)`,策略根据当日数据决定是否交易。
|
|
27
|
+
|
|
28
|
+
## 数据获取
|
|
29
|
+
|
|
30
|
+
**`self.get(factor)`** — 当日所有股票的横截面数据:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
close = self.get('close')
|
|
34
|
+
# 返回 pd.Series:
|
|
35
|
+
# 000001.SZ 10.50
|
|
36
|
+
# 600000.SH 8.32
|
|
37
|
+
|
|
38
|
+
price = float(close['000001.SZ'])
|
|
39
|
+
|
|
40
|
+
for sym in close.index:
|
|
41
|
+
price = float(close[sym])
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**`self.history(factor, window)`** — 过去 N 个交易日的历史数据:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
hist = self.history('close', 20)
|
|
48
|
+
# 返回 pd.DataFrame:
|
|
49
|
+
# 000001.SZ 600000.SH
|
|
50
|
+
# 2024-01-02 10.2 8.12
|
|
51
|
+
# 2024-01-03 10.5 8.32
|
|
52
|
+
|
|
53
|
+
avg = hist['000001.SZ'].mean()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
可访问的字段由 `Data` 配置决定。基础字段(`open`/`high`/`low`/`close`/`volume`)自动可用,指标字段需在 `indicators` 中声明。
|
|
57
|
+
|
|
58
|
+
## 交易
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
self.broker.buy('000001.SZ', price, size) # 买入
|
|
62
|
+
self.broker.sell('000001.SZ', price, size) # 卖出
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`size` 为股数。使用 `self.sizing` 计算合理仓位。
|
|
66
|
+
|
|
67
|
+
## 仓位计算
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
size = self.sizing.percent(0.1, price) # 可用资金的 10%
|
|
71
|
+
size = self.sizing.fixed_amount(10000, price) # 固定金额 1 万元
|
|
72
|
+
size = self.sizing.atr_risk(0.02, price, atr) # ATR 风控仓位
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
详见[仓位](sizing.md)。
|
|
76
|
+
|
|
77
|
+
## 状态管理
|
|
78
|
+
|
|
79
|
+
在 `__init__` 中定义的自定义属性在整个回测过程中持续有效:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
class MyStrategy(Strategy):
|
|
83
|
+
def __init__(self):
|
|
84
|
+
super().__init__()
|
|
85
|
+
self.broker = Broker(initial_cash=100_000)
|
|
86
|
+
self.bought = False # 自定义状态
|
|
87
|
+
self.entry_prices = {} # 入场价格记录
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 多股票处理
|
|
91
|
+
|
|
92
|
+
`self.get('close')` 返回所有股票的横截面,遍历处理即可:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
def on_bar(self, date):
|
|
96
|
+
close = self.get('close')
|
|
97
|
+
if close is None or close.empty:
|
|
98
|
+
return
|
|
99
|
+
for sym in close.index:
|
|
100
|
+
price = float(close[sym])
|
|
101
|
+
if price <= 0 or sym in self.broker.positions:
|
|
102
|
+
continue
|
|
103
|
+
size = self.sizing.percent(0.1, price)
|
|
104
|
+
if size > 0:
|
|
105
|
+
self.broker.buy(sym, price, size)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 注意事项
|
|
109
|
+
|
|
110
|
+
| 行为 | 原因 |
|
|
111
|
+
|------|------|
|
|
112
|
+
| 不要在 `on_bar` 中下载数据 | 每根 bar 都下载会导致性能严重下降 |
|
|
113
|
+
| 不要在 `on_bar` 中使用 print | 数千根 bar 的输出量过大,应使用 `logger.debug` |
|
|
114
|
+
| 不要访问回测引擎内部 | 策略只需与 broker 和 sizing 交互 |
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# 交易
|
|
2
|
+
|
|
3
|
+
`Broker` 是虚拟券商,负责资金管理、持仓记录和交易费用计算。策略中通过 `self.broker` 访问。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 初始化
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
self.broker = Broker(initial_cash=100_000)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
默认费率(贴近 A 股实盘):
|
|
14
|
+
|
|
15
|
+
| 费用 | 收取方向 | 默认费率 | 最低收费 |
|
|
16
|
+
|------|---------|----------|---------|
|
|
17
|
+
| 佣金 | 买卖双向 | 万 2.5 | 5 元 |
|
|
18
|
+
| 印花税 | 卖出 | 万 5 | 无 |
|
|
19
|
+
| 滑点 | 买卖双向 | 0.1% | 无 |
|
|
20
|
+
|
|
21
|
+
自定义费率:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
self.broker = Broker(
|
|
25
|
+
initial_cash=1_000_000,
|
|
26
|
+
commission_rate=0.0001, # 万 1
|
|
27
|
+
slippage=0.0005, # 0.05%
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 买卖操作
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
self.broker.buy('000001.SZ', price, size)
|
|
35
|
+
self.broker.sell('000001.SZ', price, size)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
买入时扣除资金并增加持仓,卖出时释放资金并减少持仓。交易费用自动计算。
|
|
39
|
+
|
|
40
|
+
## 持仓与资金
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
self.broker.cash # 当前可用资金(¥85,000)
|
|
44
|
+
self.broker.positions # {'000001.SZ': {'size': 1000, 'cost_basis': 10.2}}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`positions` 中每只股票包含:
|
|
48
|
+
- `size` — 持仓股数
|
|
49
|
+
- `cost_basis` — 平均买入成本
|
|
50
|
+
|
|
51
|
+
## 完整的买卖流程
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
def on_bar(self, date):
|
|
55
|
+
close = self.get('close')
|
|
56
|
+
if close is None or close.empty:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
for sym in close.index:
|
|
60
|
+
price = float(close[sym])
|
|
61
|
+
if price <= 0:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
# 无持仓则买入
|
|
65
|
+
if sym not in self.broker.positions:
|
|
66
|
+
size = self.sizing.percent(0.1, price)
|
|
67
|
+
if size > 0:
|
|
68
|
+
self.broker.buy(sym, price, size)
|
|
69
|
+
|
|
70
|
+
# 涨幅超过 20% 则卖出
|
|
71
|
+
else:
|
|
72
|
+
pos = self.broker.positions[sym]
|
|
73
|
+
cost = pos['cost_basis']
|
|
74
|
+
if price / cost - 1 > 0.2:
|
|
75
|
+
self.broker.sell(sym, price, pos['size'])
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 特殊情况处理
|
|
79
|
+
|
|
80
|
+
| 情况 | Broker 行为 |
|
|
81
|
+
|------|------------|
|
|
82
|
+
| 现金不足 | 不执行买入,记录日志 |
|
|
83
|
+
| 持仓不足 | 不执行卖出,记录日志 |
|
|
84
|
+
| 价格 ≤ 0 | 不交易,记录日志 |
|
|
85
|
+
| 部分卖出 | 减少对应持仓数量 |
|
|
86
|
+
| 全部卖出 | 从 positions 中移除该股票 |
|