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.
Files changed (30) hide show
  1. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/PKG-INFO +52 -45
  2. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/README.md +152 -145
  3. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/backtest.py +3 -0
  4. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/data.py +83 -7
  5. qka-2.1.2.dev6/skills/qka/SKILL.md +37 -0
  6. qka-2.1.2.dev6/skills/qka/references/analysis.md +117 -0
  7. qka-2.1.2.dev6/skills/qka/references/backtest.md +155 -0
  8. qka-2.1.2.dev6/skills/qka/references/broker.md +38 -0
  9. qka-2.1.1.dev2/skills/qka/SKILL.md → qka-2.1.2.dev6/skills/qka/references/data.md +313 -731
  10. qka-2.1.2.dev6/skills/qka/references/sizing.md +17 -0
  11. qka-2.1.2.dev6/skills/qka/references/strategy.md +110 -0
  12. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/.github/workflows/release.yml +0 -0
  13. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/.gitignore +0 -0
  14. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/.vscode/settings.json +0 -0
  15. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/CHANGELOG.md +0 -0
  16. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/LICENSE +0 -0
  17. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/pyproject.toml +0 -0
  18. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/__init__.py +0 -0
  19. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/cli.py +0 -0
  20. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/__init__.py +0 -0
  21. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/accessor.py +0 -0
  22. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/analysis.py +0 -0
  23. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/broker.py +0 -0
  24. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/indicator.py +0 -0
  25. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/report.py +0 -0
  26. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/sizing.py +0 -0
  27. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/core/strategy.py +0 -0
  28. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/utils/__init__.py +0 -0
  29. {qka-2.1.1.dev2 → qka-2.1.2.dev6}/qka/utils/logger.py +0 -0
  30. {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.1.dev2
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
- **QKA(快量化 / Quant Kit for A-shares)** — 简洁易用的 A 股量化回测框架。
66
+ 简洁易用的 A 股量化回测框架。
67
67
 
68
- ```python
69
- from qka import Data, Strategy, Backtest
68
+ ---
70
69
 
71
- data = Data(
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
- class MaCross(Strategy):
80
- def __init__(self):
81
- super().__init__()
82
- self.pct = 0.2
72
+ ### 包安装
83
73
 
84
- def on_bar(self, date):
85
- close = self.get('close')
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
- pip install qka
85
+ npx skills add zsrl/qka
113
86
  ```
114
87
 
115
- 需要 Python 3.10+。
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
- - [ ] 实盘交易(QMT 接口)
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
- **QKA(快量化 / Quant Kit for A-shares)** — 简洁易用的 A 股量化回测框架。
17
-
18
- ```python
19
- from qka import Data, Strategy, Backtest
20
-
21
- data = Data(
22
- symbols=['sz.000001'],
23
- indicators={
24
- 'sma_5': ('ta.trend.sma_indicator', 'close', 5),
25
- 'sma_20': ('ta.trend.sma_indicator', 'close', 20),
26
- },
27
- )
28
-
29
- class MaCross(Strategy):
30
- def __init__(self):
31
- super().__init__()
32
- self.pct = 0.2
33
-
34
- def on_bar(self, date):
35
- close = self.get('close')
36
- fast = self.get('sma_5')
37
- slow = self.get('sma_20')
38
- for sym in close.index:
39
- price = float(close[sym])
40
- if price <= 0:
41
- continue
42
- if fast[sym] > slow[sym]:
43
- size = self.sizing.percent(self.pct, price)
44
- if size > 0:
45
- self.broker.buy(sym, price, size)
46
- else:
47
- pos = self.broker.positions.get(sym, {}).get('size', 0)
48
- if pos > 0:
49
- self.broker.sell(sym, price, pos)
50
-
51
- strategy = MaCross()
52
- bt = Backtest(data, strategy)
53
- bt.run(cash=200000, start_date='2024-01-01')
54
- print(bt.metrics['total_return_pct'])
55
- ```
56
-
57
- ---
58
-
59
- ## 安装
60
-
61
- ```bash
62
- pip install qka
63
- ```
64
-
65
- 需要 Python 3.10+。
66
-
67
- ## 快速上手
68
-
69
- ### 数据
70
-
71
- ```python
72
- from qka import Data
73
-
74
- data = Data(
75
- symbols=['sz.000001', 'sh.600000'],
76
- indicators={
77
- 'sma_5': ('ta.trend.sma_indicator', 'close', 5),
78
- 'rsi_14': ('ta.momentum.rsi', 'close', 14),
79
- },
80
- )
81
- df = data.get() # 返回宽表 DataFrame,列名 {symbol}|{factor}
82
- ```
83
-
84
- ### 策略
85
-
86
- ```python
87
- from qka import Strategy
88
-
89
- class MyStrategy(Strategy):
90
- def __init__(self):
91
- super().__init__()
92
- self.lookback = 20 # 自定义参数
93
-
94
- def on_bar(self, date):
95
- close = self.get('close') # 当前横截面
96
- hist = self.history('close', 20) # 历史窗口
97
- # 交易逻辑:self.broker.buy / self.broker.sell
98
- # 仓位计算:self.sizing.percent / self.sizing.fixed_shares
99
- ```
100
-
101
- ### 回测
102
-
103
- ```python
104
- from qka import Backtest
105
-
106
- strategy = MyStrategy()
107
- bt = Backtest(data, strategy)
108
- bt.run(cash=200000, start_date='2024-01-01', benchmark='sh.000300')
109
- print(bt.metrics['total_return_pct']) # 总收益率
110
- print(bt.metrics['sharpe_ratio']) # 夏普比率
111
- ```
112
-
113
- ## 核心能力
114
-
115
- - **多数据源** — baostock(默认)、akshare、QMT
116
- - **预计算指标** — ta 库全部 60+ 指标,`('ta.trend.sma_indicator', 'close', 5)` 格式直接透传
117
- - **事件驱动回测** — 按日推进,`self.get()` 横截面 + `self.history()` 窗口序列
118
- - **仓位管理** — `sizing.percent()` / `sizing.fixed_amount()` / `sizing.fixed_shares()` / `sizing.atr_risk()`
119
- - **交易模拟** — 佣金万 2.5、印花税万 5(仅卖出)、滑点 0.1%,最低佣金 5 元
120
- - **绩效指标** — 总收益率、年化、夏普比率、最大回撤、Calmar、胜率、盈亏比等 13 项
121
- - **基准对比** — 支持沪深 300(或指定指数)对比
122
-
123
- ## 文档
124
-
125
- 框架 API 完整文档见 [skills/qka/SKILL.md](skills/qka/SKILL.md)——所有类的方法签名、参数、返回值和约束都在里面。
126
-
127
- ## 下一步规划
128
-
129
- - [ ] 分钟级数据支持
130
- - [ ] 自适应参数优化
131
- - [ ] 实盘交易(QMT 接口)
132
-
133
- ## 许可证
134
-
135
- [MIT](LICENSE)
136
-
137
- ## 致谢
138
-
139
- - [baostock](http://baostock.com) — 免费 A 股数据
140
- - [ta](https://github.com/bukosabino/ta) — 技术指标库
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
- if not path.exists():
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
- "date,open,high,low,close,volume,amount",
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 = ["open", "high", "low", "close", "volume", "amount"]
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")