qka 2.1.1.dev2__tar.gz → 2.1.2.dev6__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-2.1.1.dev2 → qka-2.1.2.dev6}/PKG-INFO +52 -45
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/README.md +152 -145
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/backtest.py +3 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/data.py +83 -7
- qka-2.1.2.dev6/skills/qka/SKILL.md +37 -0
- qka-2.1.2.dev6/skills/qka/references/analysis.md +117 -0
- qka-2.1.2.dev6/skills/qka/references/backtest.md +155 -0
- qka-2.1.2.dev6/skills/qka/references/broker.md +38 -0
- qka-2.1.1.dev2/skills/qka/SKILL.md → qka-2.1.2.dev6/skills/qka/references/data.md +313 -731
- qka-2.1.2.dev6/skills/qka/references/sizing.md +17 -0
- qka-2.1.2.dev6/skills/qka/references/strategy.md +110 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/.github/workflows/release.yml +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/.gitignore +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/.vscode/settings.json +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/CHANGELOG.md +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/LICENSE +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/pyproject.toml +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/__init__.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/cli.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/__init__.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/accessor.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/analysis.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/broker.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/indicator.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/report.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/sizing.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/strategy.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/utils/__init__.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/utils/logger.py +0 -0
- {qka-2.1.1.dev2 → qka-2.1.2.dev6}/tools/generate_api_ref.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: qka
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2.dev6
|
|
4
4
|
Summary: QKA(快量化 / Quant Kit for A-shares)- 简洁易用的 A 股量化回测框架
|
|
5
5
|
Project-URL: Home, https://github.com/zsrl/qka
|
|
6
6
|
Project-URL: Repository, https://github.com/zsrl/qka
|
|
@@ -63,56 +63,29 @@ Description-Content-Type: text/markdown
|
|
|
63
63
|
</a>
|
|
64
64
|
</p>
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
简洁易用的 A 股量化回测框架。
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
from qka import Data, Strategy, Backtest
|
|
68
|
+
---
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
symbols=['sz.000001'],
|
|
73
|
-
indicators={
|
|
74
|
-
'sma_5': ('ta.trend.sma_indicator', 'close', 5),
|
|
75
|
-
'sma_20': ('ta.trend.sma_indicator', 'close', 20),
|
|
76
|
-
},
|
|
77
|
-
)
|
|
70
|
+
## 安装
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
def __init__(self):
|
|
81
|
-
super().__init__()
|
|
82
|
-
self.pct = 0.2
|
|
72
|
+
### 包安装
|
|
83
73
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
fast = self.get('sma_5')
|
|
87
|
-
slow = self.get('sma_20')
|
|
88
|
-
for sym in close.index:
|
|
89
|
-
price = float(close[sym])
|
|
90
|
-
if price <= 0:
|
|
91
|
-
continue
|
|
92
|
-
if fast[sym] > slow[sym]:
|
|
93
|
-
size = self.sizing.percent(self.pct, price)
|
|
94
|
-
if size > 0:
|
|
95
|
-
self.broker.buy(sym, price, size)
|
|
96
|
-
else:
|
|
97
|
-
pos = self.broker.positions.get(sym, {}).get('size', 0)
|
|
98
|
-
if pos > 0:
|
|
99
|
-
self.broker.sell(sym, price, pos)
|
|
100
|
-
|
|
101
|
-
strategy = MaCross()
|
|
102
|
-
bt = Backtest(data, strategy)
|
|
103
|
-
bt.run(cash=200000, start_date='2024-01-01')
|
|
104
|
-
print(bt.metrics['total_return_pct'])
|
|
74
|
+
```bash
|
|
75
|
+
pip install qka
|
|
105
76
|
```
|
|
106
77
|
|
|
107
|
-
|
|
78
|
+
需要 Python 3.10+。
|
|
108
79
|
|
|
109
|
-
|
|
80
|
+
### AI 技能安装
|
|
81
|
+
|
|
82
|
+
为 Claude Code、Cursor 等 AI 编程工具安装 QKA 技能:
|
|
110
83
|
|
|
111
84
|
```bash
|
|
112
|
-
|
|
85
|
+
npx skills add zsrl/qka
|
|
113
86
|
```
|
|
114
87
|
|
|
115
|
-
|
|
88
|
+
安装后,AI 助手即可自动加载 QKA 框架的 API 文档,生成符合规范的量化策略代码。
|
|
116
89
|
|
|
117
90
|
## 快速上手
|
|
118
91
|
|
|
@@ -174,11 +147,46 @@ print(bt.metrics['sharpe_ratio']) # 夏普比率
|
|
|
174
147
|
|
|
175
148
|
框架 API 完整文档见 [skills/qka/SKILL.md](skills/qka/SKILL.md)——所有类的方法签名、参数、返回值和约束都在里面。
|
|
176
149
|
|
|
177
|
-
##
|
|
150
|
+
## 完整示例
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from qka import Data, Strategy, Backtest
|
|
178
154
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
155
|
+
data = Data(
|
|
156
|
+
symbols=['sz.000001'],
|
|
157
|
+
indicators={
|
|
158
|
+
'sma_5': ('ta.trend.sma_indicator', 'close', 5),
|
|
159
|
+
'sma_20': ('ta.trend.sma_indicator', 'close', 20),
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
class MaCross(Strategy):
|
|
164
|
+
def __init__(self):
|
|
165
|
+
super().__init__()
|
|
166
|
+
self.pct = 0.2
|
|
167
|
+
|
|
168
|
+
def on_bar(self, date):
|
|
169
|
+
close = self.get('close')
|
|
170
|
+
fast = self.get('sma_5')
|
|
171
|
+
slow = self.get('sma_20')
|
|
172
|
+
for sym in close.index:
|
|
173
|
+
price = float(close[sym])
|
|
174
|
+
if price <= 0:
|
|
175
|
+
continue
|
|
176
|
+
if fast[sym] > slow[sym]:
|
|
177
|
+
size = self.sizing.percent(self.pct, price)
|
|
178
|
+
if size > 0:
|
|
179
|
+
self.broker.buy(sym, price, size)
|
|
180
|
+
else:
|
|
181
|
+
pos = self.broker.positions.get(sym, {}).get('size', 0)
|
|
182
|
+
if pos > 0:
|
|
183
|
+
self.broker.sell(sym, price, pos)
|
|
184
|
+
|
|
185
|
+
strategy = MaCross()
|
|
186
|
+
bt = Backtest(data, strategy)
|
|
187
|
+
bt.run(cash=200000, start_date='2024-01-01')
|
|
188
|
+
print(bt.metrics['total_return_pct'])
|
|
189
|
+
```
|
|
182
190
|
|
|
183
191
|
## 许可证
|
|
184
192
|
|
|
@@ -192,4 +200,3 @@ print(bt.metrics['sharpe_ratio']) # 夏普比率
|
|
|
192
200
|
---
|
|
193
201
|
|
|
194
202
|
> ⚠️ 量化交易存在风险,请充分了解后再使用本框架。
|
|
195
|
-
充分了解后再使用本框架。
|
|
@@ -1,145 +1,152 @@
|
|
|
1
|
-
# QKA — 快量化
|
|
2
|
-
## Quant Kit for A-shares
|
|
3
|
-
|
|
4
|
-
<p align="center">
|
|
5
|
-
<a href="https://pypi.org/project/qka/">
|
|
6
|
-
<img src="https://img.shields.io/pypi/v/qka?color=blue" alt="PyPI">
|
|
7
|
-
</a>
|
|
8
|
-
<a href="https://github.com/zsrl/qka">
|
|
9
|
-
<img src="https://img.shields.io/badge/python-3.10+-blue" alt="Python">
|
|
10
|
-
</a>
|
|
11
|
-
<a href="LICENSE">
|
|
12
|
-
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
|
13
|
-
</a>
|
|
14
|
-
</p>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
1
|
+
# QKA — 快量化
|
|
2
|
+
## Quant Kit for A-shares
|
|
3
|
+
|
|
4
|
+
<p align="center">
|
|
5
|
+
<a href="https://pypi.org/project/qka/">
|
|
6
|
+
<img src="https://img.shields.io/pypi/v/qka?color=blue" alt="PyPI">
|
|
7
|
+
</a>
|
|
8
|
+
<a href="https://github.com/zsrl/qka">
|
|
9
|
+
<img src="https://img.shields.io/badge/python-3.10+-blue" alt="Python">
|
|
10
|
+
</a>
|
|
11
|
+
<a href="LICENSE">
|
|
12
|
+
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
|
13
|
+
</a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
简洁易用的 A 股量化回测框架。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
### 包安装
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install qka
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
需要 Python 3.10+。
|
|
29
|
+
|
|
30
|
+
### AI 技能安装
|
|
31
|
+
|
|
32
|
+
为 Claude Code、Cursor 等 AI 编程工具安装 QKA 技能:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx skills add zsrl/qka
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
安装后,AI 助手即可自动加载 QKA 框架的 API 文档,生成符合规范的量化策略代码。
|
|
39
|
+
|
|
40
|
+
## 快速上手
|
|
41
|
+
|
|
42
|
+
### 数据
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from qka import Data
|
|
46
|
+
|
|
47
|
+
data = Data(
|
|
48
|
+
symbols=['sz.000001', 'sh.600000'],
|
|
49
|
+
indicators={
|
|
50
|
+
'sma_5': ('ta.trend.sma_indicator', 'close', 5),
|
|
51
|
+
'rsi_14': ('ta.momentum.rsi', 'close', 14),
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
df = data.get() # 返回宽表 DataFrame,列名 {symbol}|{factor}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 策略
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from qka import Strategy
|
|
61
|
+
|
|
62
|
+
class MyStrategy(Strategy):
|
|
63
|
+
def __init__(self):
|
|
64
|
+
super().__init__()
|
|
65
|
+
self.lookback = 20 # 自定义参数
|
|
66
|
+
|
|
67
|
+
def on_bar(self, date):
|
|
68
|
+
close = self.get('close') # 当前横截面
|
|
69
|
+
hist = self.history('close', 20) # 历史窗口
|
|
70
|
+
# 交易逻辑:self.broker.buy / self.broker.sell
|
|
71
|
+
# 仓位计算:self.sizing.percent / self.sizing.fixed_shares
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 回测
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from qka import Backtest
|
|
78
|
+
|
|
79
|
+
strategy = MyStrategy()
|
|
80
|
+
bt = Backtest(data, strategy)
|
|
81
|
+
bt.run(cash=200000, start_date='2024-01-01', benchmark='sh.000300')
|
|
82
|
+
print(bt.metrics['total_return_pct']) # 总收益率
|
|
83
|
+
print(bt.metrics['sharpe_ratio']) # 夏普比率
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 核心能力
|
|
87
|
+
|
|
88
|
+
- **多数据源** — baostock(默认)、akshare、QMT
|
|
89
|
+
- **预计算指标** — ta 库全部 60+ 指标,`('ta.trend.sma_indicator', 'close', 5)` 格式直接透传
|
|
90
|
+
- **事件驱动回测** — 按日推进,`self.get()` 横截面 + `self.history()` 窗口序列
|
|
91
|
+
- **仓位管理** — `sizing.percent()` / `sizing.fixed_amount()` / `sizing.fixed_shares()` / `sizing.atr_risk()`
|
|
92
|
+
- **交易模拟** — 佣金万 2.5、印花税万 5(仅卖出)、滑点 0.1%,最低佣金 5 元
|
|
93
|
+
- **绩效指标** — 总收益率、年化、夏普比率、最大回撤、Calmar、胜率、盈亏比等 13 项
|
|
94
|
+
- **基准对比** — 支持沪深 300(或指定指数)对比
|
|
95
|
+
|
|
96
|
+
## 文档
|
|
97
|
+
|
|
98
|
+
框架 API 完整文档见 [skills/qka/SKILL.md](skills/qka/SKILL.md)——所有类的方法签名、参数、返回值和约束都在里面。
|
|
99
|
+
|
|
100
|
+
## 完整示例
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from qka import Data, Strategy, Backtest
|
|
104
|
+
|
|
105
|
+
data = Data(
|
|
106
|
+
symbols=['sz.000001'],
|
|
107
|
+
indicators={
|
|
108
|
+
'sma_5': ('ta.trend.sma_indicator', 'close', 5),
|
|
109
|
+
'sma_20': ('ta.trend.sma_indicator', 'close', 20),
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
class MaCross(Strategy):
|
|
114
|
+
def __init__(self):
|
|
115
|
+
super().__init__()
|
|
116
|
+
self.pct = 0.2
|
|
117
|
+
|
|
118
|
+
def on_bar(self, date):
|
|
119
|
+
close = self.get('close')
|
|
120
|
+
fast = self.get('sma_5')
|
|
121
|
+
slow = self.get('sma_20')
|
|
122
|
+
for sym in close.index:
|
|
123
|
+
price = float(close[sym])
|
|
124
|
+
if price <= 0:
|
|
125
|
+
continue
|
|
126
|
+
if fast[sym] > slow[sym]:
|
|
127
|
+
size = self.sizing.percent(self.pct, price)
|
|
128
|
+
if size > 0:
|
|
129
|
+
self.broker.buy(sym, price, size)
|
|
130
|
+
else:
|
|
131
|
+
pos = self.broker.positions.get(sym, {}).get('size', 0)
|
|
132
|
+
if pos > 0:
|
|
133
|
+
self.broker.sell(sym, price, pos)
|
|
134
|
+
|
|
135
|
+
strategy = MaCross()
|
|
136
|
+
bt = Backtest(data, strategy)
|
|
137
|
+
bt.run(cash=200000, start_date='2024-01-01')
|
|
138
|
+
print(bt.metrics['total_return_pct'])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 许可证
|
|
142
|
+
|
|
143
|
+
[MIT](LICENSE)
|
|
144
|
+
|
|
145
|
+
## 致谢
|
|
146
|
+
|
|
147
|
+
- [baostock](http://baostock.com) — 免费 A 股数据
|
|
148
|
+
- [ta](https://github.com/bukosabino/ta) — 技术指标库
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
> ⚠️ 量化交易存在风险,请充分了解后再使用本框架。
|
|
@@ -132,6 +132,7 @@ class Backtest:
|
|
|
132
132
|
for factor, data in by_factor.items():
|
|
133
133
|
self.strategy._data.push(dt, factor, data)
|
|
134
134
|
# dask 路径:策略使用 self.get() / self.history()
|
|
135
|
+
self.strategy.broker.timestamp = dt
|
|
135
136
|
self.strategy.on_bar(dt)
|
|
136
137
|
self.strategy.broker.on_bar(
|
|
137
138
|
dt, self.strategy._data.get
|
|
@@ -143,6 +144,7 @@ class Backtest:
|
|
|
143
144
|
by_factor = self._parse_row(row)
|
|
144
145
|
for factor, data in by_factor.items():
|
|
145
146
|
self.strategy._data.push(date, factor, data)
|
|
147
|
+
self.strategy.broker.timestamp = date
|
|
146
148
|
self.strategy.on_bar(date)
|
|
147
149
|
self.strategy.broker.on_bar(
|
|
148
150
|
date, self.strategy._data.get
|
|
@@ -154,6 +156,7 @@ class Backtest:
|
|
|
154
156
|
by_factor = self._parse_row(row)
|
|
155
157
|
for factor, data in by_factor.items():
|
|
156
158
|
self.strategy._data.push(date, factor, data)
|
|
159
|
+
self.strategy.broker.timestamp = date
|
|
157
160
|
self.strategy.on_bar(date)
|
|
158
161
|
self.strategy.broker.on_bar(
|
|
159
162
|
date, self.strategy._data.get
|
|
@@ -45,8 +45,24 @@ class Data():
|
|
|
45
45
|
pool_size (int): 并发下载线程数
|
|
46
46
|
datadir (Path): 数据缓存目录
|
|
47
47
|
target_dir (Path): 目标存储目录
|
|
48
|
+
extra_fields (List[str]): baostock 扩展字段(选股/估值用),如 ['peTTM', 'pbMRQ', 'turn']
|
|
48
49
|
"""
|
|
49
50
|
|
|
51
|
+
# baostock query_history_k_data_plus 完整支持的基础字段(除 date 索引外)
|
|
52
|
+
BAOSTOCK_BASE_FIELDS = ["open", "high", "low", "close", "volume", "amount"]
|
|
53
|
+
# 可通过 extra_fields 追加的扩展字段白名单(行情/估值/选股类)
|
|
54
|
+
BAOSTOCK_EXTRA_FIELDS = [
|
|
55
|
+
"preclose", # 前收盘价
|
|
56
|
+
"turn", # 换手率(%)
|
|
57
|
+
"tradestatus", # 交易状态(1=正常, 0=停牌)
|
|
58
|
+
"pctChg", # 涨跌幅(%)
|
|
59
|
+
"isST", # 是否 ST(1=是, 0=否)
|
|
60
|
+
"peTTM", # 市盈率(TTM)
|
|
61
|
+
"pbMRQ", # 市净率(MRQ)
|
|
62
|
+
"psTTM", # 市销率(TTM)
|
|
63
|
+
"pcfNcfTTM", # 市现率(TTM)
|
|
64
|
+
]
|
|
65
|
+
|
|
50
66
|
def __init__(
|
|
51
67
|
self,
|
|
52
68
|
symbols: Optional[List[str]] = None,
|
|
@@ -57,6 +73,7 @@ class Data():
|
|
|
57
73
|
pool_size: int = 10,
|
|
58
74
|
datadir: Optional[Path] = None,
|
|
59
75
|
indicators: Optional[dict] = None,
|
|
76
|
+
extra_fields: Optional[List[str]] = None,
|
|
60
77
|
):
|
|
61
78
|
"""
|
|
62
79
|
初始化数据对象
|
|
@@ -71,6 +88,10 @@ class Data():
|
|
|
71
88
|
pool_size: 并发下载线程数
|
|
72
89
|
datadir: 缓存目录路径
|
|
73
90
|
indicators: 预计算指标/因子,支持三种格式:
|
|
91
|
+
extra_fields: baostock 扩展字段列表(选股/估值用,如 ['peTTM', 'pbMRQ', 'turn']),
|
|
92
|
+
可选值见 BAOSTOCK_EXTRA_FIELDS。追加的列同样遵循 {symbol}|{field} 命名,
|
|
93
|
+
如 'sh.600000|peTTM'。注意:首次下载后缓存字段固定,变更 extra_fields
|
|
94
|
+
会自动检测列缺失并重新下载对应股票。
|
|
74
95
|
|
|
75
96
|
**1. 字典(混搭 ta 函数和自定义因子):**
|
|
76
97
|
```python
|
|
@@ -99,6 +120,17 @@ class Data():
|
|
|
99
120
|
self.source = source
|
|
100
121
|
self.pool_size = pool_size
|
|
101
122
|
|
|
123
|
+
# extra_fields 白名单校验 + 去重
|
|
124
|
+
self.extra_fields = []
|
|
125
|
+
for f in (extra_fields or []):
|
|
126
|
+
if f not in self.BAOSTOCK_EXTRA_FIELDS:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"extra_fields 含不支持的字段: {f}。"
|
|
129
|
+
f"可选: {self.BAOSTOCK_EXTRA_FIELDS}"
|
|
130
|
+
)
|
|
131
|
+
if f not in self.extra_fields:
|
|
132
|
+
self.extra_fields.append(f)
|
|
133
|
+
|
|
102
134
|
# 统一处理 indicators 参数
|
|
103
135
|
if callable(indicators):
|
|
104
136
|
# 函数形式 → 保存为 callable
|
|
@@ -125,6 +157,35 @@ class Data():
|
|
|
125
157
|
self.target_dir = self.datadir / self.source / self.period / (self.adjust or "bfq")
|
|
126
158
|
self.target_dir.mkdir(parents=True, exist_ok=True)
|
|
127
159
|
|
|
160
|
+
def _cache_missing_extra_fields(self, path: Path) -> bool:
|
|
161
|
+
"""检查已有 parquet 缓存是否缺少 extra_fields 指定的列。"""
|
|
162
|
+
if not self.extra_fields or not path.exists():
|
|
163
|
+
return False
|
|
164
|
+
try:
|
|
165
|
+
cols = set(pq.read_schema(path).names)
|
|
166
|
+
except Exception:
|
|
167
|
+
return True
|
|
168
|
+
return any(f not in cols for f in self.extra_fields)
|
|
169
|
+
|
|
170
|
+
def _merged_extra_fields(self, path: Path) -> List[str]:
|
|
171
|
+
"""
|
|
172
|
+
计算本次下载实际请求的扩展字段:当前 extra_fields 与缓存已有扩展列的并集。
|
|
173
|
+
|
|
174
|
+
保证同一 datadir 下不同 extra_fields 配置共享缓存时,列只增不减、
|
|
175
|
+
不互相覆盖(第一次只有 peTTM,第二次再加 pbMRQ 时 peTTM 仍保留)。
|
|
176
|
+
"""
|
|
177
|
+
merged = list(self.extra_fields)
|
|
178
|
+
if not path.exists():
|
|
179
|
+
return merged
|
|
180
|
+
try:
|
|
181
|
+
existing = set(pq.read_schema(path).names)
|
|
182
|
+
except Exception:
|
|
183
|
+
return merged
|
|
184
|
+
for f in self.BAOSTOCK_EXTRA_FIELDS:
|
|
185
|
+
if f in existing and f not in merged:
|
|
186
|
+
merged.append(f)
|
|
187
|
+
return merged
|
|
188
|
+
|
|
128
189
|
def _download(
|
|
129
190
|
self, symbol: str,
|
|
130
191
|
download_start: str = None,
|
|
@@ -135,6 +196,7 @@ class Data():
|
|
|
135
196
|
|
|
136
197
|
首次下载只拉请求范围(非全量)。已存在时检查缓存覆盖范围,
|
|
137
198
|
只补下载缺失的部分(前面缺失补前面,后面缺失补后面),合并去重写回。
|
|
199
|
+
若缓存缺少 extra_fields 指定的列(如从无扩展字段升级到有),则全量重新下载。
|
|
138
200
|
|
|
139
201
|
Args:
|
|
140
202
|
symbol: 股票代码
|
|
@@ -151,12 +213,16 @@ class Data():
|
|
|
151
213
|
default_start = '1990-01-01'
|
|
152
214
|
default_end = pd.Timestamp.now().strftime("%Y-%m-%d")
|
|
153
215
|
|
|
154
|
-
#
|
|
155
|
-
|
|
216
|
+
# 实际请求的扩展字段 = 当前配置 ∪ 缓存已有扩展列(列只增不减,不互相覆盖)
|
|
217
|
+
merged_extra = self._merged_extra_fields(path)
|
|
218
|
+
|
|
219
|
+
# ── 首次下载:只拉请求范围(缓存缺失 extra_fields 列时也全量重下)──
|
|
220
|
+
if not path.exists() or self._cache_missing_extra_fields(path):
|
|
156
221
|
df = self._get_from_baostock(
|
|
157
222
|
symbol,
|
|
158
223
|
start_date=download_start or default_start,
|
|
159
224
|
end_date=download_end or default_end,
|
|
225
|
+
extra_fields=merged_extra,
|
|
160
226
|
)
|
|
161
227
|
if len(df) == 0:
|
|
162
228
|
raise RuntimeError(f"{symbol}: baostock 返回空数据")
|
|
@@ -183,6 +249,7 @@ class Data():
|
|
|
183
249
|
end_before = (cache_min - pd.Timedelta(days=1)).strftime("%Y-%m-%d")
|
|
184
250
|
df_before = self._get_from_baostock(
|
|
185
251
|
symbol, start_date=download_start, end_date=end_before,
|
|
252
|
+
extra_fields=merged_extra,
|
|
186
253
|
)
|
|
187
254
|
if len(df_before) > 0:
|
|
188
255
|
pieces.insert(0, df_before)
|
|
@@ -196,6 +263,7 @@ class Data():
|
|
|
196
263
|
symbol,
|
|
197
264
|
start_date=start_after,
|
|
198
265
|
end_date=download_end or default_end,
|
|
266
|
+
extra_fields=merged_extra,
|
|
199
267
|
)
|
|
200
268
|
if len(df_after) > 0:
|
|
201
269
|
pieces.append(df_after)
|
|
@@ -220,7 +288,7 @@ class Data():
|
|
|
220
288
|
缓存不存在、不覆盖请求范围、或需要拉最新数据时返回 True。
|
|
221
289
|
"""
|
|
222
290
|
path = self.target_dir / f"{symbol}.parquet"
|
|
223
|
-
if not path.exists():
|
|
291
|
+
if not path.exists() or self._cache_missing_extra_fields(path):
|
|
224
292
|
return True
|
|
225
293
|
if self.source != 'baostock':
|
|
226
294
|
return False
|
|
@@ -624,6 +692,7 @@ class Data():
|
|
|
624
692
|
self, symbol: str,
|
|
625
693
|
start_date: str = '1990-01-01',
|
|
626
694
|
end_date: str = '2050-12-31',
|
|
695
|
+
extra_fields: Optional[List[str]] = None,
|
|
627
696
|
) -> pd.DataFrame:
|
|
628
697
|
"""
|
|
629
698
|
从 baostock 获取单个股票的数据。
|
|
@@ -632,17 +701,24 @@ class Data():
|
|
|
632
701
|
symbol: baostock 格式股票代码,如 sz.000001、sh.600000
|
|
633
702
|
start_date: 起始日期,格式 YYYY-MM-DD,默认 1990-01-01
|
|
634
703
|
end_date: 截止日期,格式 YYYY-MM-DD,默认 2050-12-31
|
|
704
|
+
extra_fields: 本次请求的扩展字段列表。None 时使用 self.extra_fields;
|
|
705
|
+
调用方(_download)可传入"当前配置 ∪ 缓存已有列"的并集,保证列只增不减
|
|
635
706
|
|
|
636
707
|
Returns:
|
|
637
|
-
pd.DataFrame: 股票数据,以 date 为索引,包含 open, high, low, close, volume, amount
|
|
708
|
+
pd.DataFrame: 股票数据,以 date 为索引,包含 open, high, low, close, volume, amount
|
|
709
|
+
及 extra_fields 指定的扩展列(如有)
|
|
638
710
|
"""
|
|
639
711
|
# adjustflag: 1=不复权, 2=前复权, 3=后复权
|
|
640
712
|
adjust_map = {'bfq': '1', 'qfq': '2', 'hfq': '3'}
|
|
641
713
|
adjustflag = adjust_map.get(self.adjust, '2')
|
|
642
714
|
|
|
715
|
+
# 基础字段 + extra_fields 扩展字段(缺省用 self.extra_fields)
|
|
716
|
+
extra = list(extra_fields) if extra_fields is not None else self.extra_fields
|
|
717
|
+
fields = ",".join(["date"] + self.BAOSTOCK_BASE_FIELDS + extra)
|
|
718
|
+
|
|
643
719
|
rs = bs.query_history_k_data_plus(
|
|
644
720
|
symbol,
|
|
645
|
-
|
|
721
|
+
fields,
|
|
646
722
|
start_date=start_date,
|
|
647
723
|
end_date=end_date,
|
|
648
724
|
frequency='d',
|
|
@@ -659,8 +735,8 @@ class Data():
|
|
|
659
735
|
if len(df) == 0:
|
|
660
736
|
return df
|
|
661
737
|
|
|
662
|
-
# baostock
|
|
663
|
-
numeric_cols =
|
|
738
|
+
# baostock 返回的数值列是字符串,转数值类型(基础列 + 本次请求的扩展字段)
|
|
739
|
+
numeric_cols = self.BAOSTOCK_BASE_FIELDS + extra
|
|
664
740
|
for col in numeric_cols:
|
|
665
741
|
if col in df.columns:
|
|
666
742
|
df[col] = pd.to_numeric(df[col], errors="coerce")
|