qka 2.4.0__tar.gz → 2.4.1.dev2__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.4.0 → qka-2.4.1.dev2}/PKG-INFO +5 -3
- {qka-2.4.0 → qka-2.4.1.dev2}/README.md +4 -2
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/data.py +475 -72
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/indicator.py +2 -2
- {qka-2.4.0 → qka-2.4.1.dev2}/skills/qka/references/backtest.md +1 -1
- {qka-2.4.0 → qka-2.4.1.dev2}/skills/qka/references/data.md +62 -22
- {qka-2.4.0 → qka-2.4.1.dev2}/skills/qka/references/strategy.md +2 -2
- {qka-2.4.0 → qka-2.4.1.dev2}/.github/workflows/release.yml +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/.gitignore +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/.vscode/settings.json +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/CHANGELOG.md +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/LICENSE +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/pyproject.toml +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/__init__.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/cli.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/__init__.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/accessor.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/analysis.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/backtest.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/broker.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/report.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/simulate.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/sizing.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/strategy.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/core/tdx.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/utils/__init__.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/qka/utils/logger.py +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/skills/qka/SKILL.md +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/skills/qka/references/analysis.md +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/skills/qka/references/broker.md +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/skills/qka/references/sizing.md +0 -0
- {qka-2.4.0 → qka-2.4.1.dev2}/tools/generate_api_ref.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: qka
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.1.dev2
|
|
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
|
|
@@ -97,7 +97,7 @@ from qka import Data
|
|
|
97
97
|
|
|
98
98
|
data = Data(
|
|
99
99
|
symbols=['000001.SZ', '600000.SH'],
|
|
100
|
-
|
|
100
|
+
factors={
|
|
101
101
|
'sma_5': ('ta.trend.sma_indicator', 'close', 5),
|
|
102
102
|
'rsi_14': ('ta.momentum.rsi', 'close', 14),
|
|
103
103
|
},
|
|
@@ -105,6 +105,8 @@ data = Data(
|
|
|
105
105
|
df = data.get() # 返回宽表 DataFrame,列名 {symbol}|{factor}
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
> `factors=` 也可写作 `indicators=`(旧名,完全等价、保留兼容);两者只能传一个。
|
|
109
|
+
|
|
108
110
|
### 策略
|
|
109
111
|
|
|
110
112
|
```python
|
|
@@ -159,7 +161,7 @@ from qka import Data, Strategy, Backtest
|
|
|
159
161
|
|
|
160
162
|
data = Data(
|
|
161
163
|
symbols=['000001.SZ'],
|
|
162
|
-
|
|
164
|
+
factors={
|
|
163
165
|
'sma_5': ('ta.trend.sma_indicator', 'close', 5),
|
|
164
166
|
'sma_20': ('ta.trend.sma_indicator', 'close', 20),
|
|
165
167
|
},
|
|
@@ -46,7 +46,7 @@ from qka import Data
|
|
|
46
46
|
|
|
47
47
|
data = Data(
|
|
48
48
|
symbols=['000001.SZ', '600000.SH'],
|
|
49
|
-
|
|
49
|
+
factors={
|
|
50
50
|
'sma_5': ('ta.trend.sma_indicator', 'close', 5),
|
|
51
51
|
'rsi_14': ('ta.momentum.rsi', 'close', 14),
|
|
52
52
|
},
|
|
@@ -54,6 +54,8 @@ data = Data(
|
|
|
54
54
|
df = data.get() # 返回宽表 DataFrame,列名 {symbol}|{factor}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
> `factors=` 也可写作 `indicators=`(旧名,完全等价、保留兼容);两者只能传一个。
|
|
58
|
+
|
|
57
59
|
### 策略
|
|
58
60
|
|
|
59
61
|
```python
|
|
@@ -108,7 +110,7 @@ from qka import Data, Strategy, Backtest
|
|
|
108
110
|
|
|
109
111
|
data = Data(
|
|
110
112
|
symbols=['000001.SZ'],
|
|
111
|
-
|
|
113
|
+
factors={
|
|
112
114
|
'sma_5': ('ta.trend.sma_indicator', 'close', 5),
|
|
113
115
|
'sma_20': ('ta.trend.sma_indicator', 'close', 20),
|
|
114
116
|
},
|
|
@@ -5,8 +5,12 @@ QKA数据模块
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import contextlib
|
|
8
|
+
import hashlib
|
|
9
|
+
import inspect
|
|
8
10
|
import io
|
|
11
|
+
import json
|
|
9
12
|
import os
|
|
13
|
+
import textwrap
|
|
10
14
|
import threading
|
|
11
15
|
from pathlib import Path
|
|
12
16
|
from tqdm import tqdm
|
|
@@ -30,12 +34,20 @@ _QKA_BUILTIN_NAMES = frozenset({
|
|
|
30
34
|
})
|
|
31
35
|
|
|
32
36
|
|
|
33
|
-
def
|
|
37
|
+
def _is_qka_factor(ind_type):
|
|
34
38
|
"""判断指标类型是否为 qka 内置指标(支持 qka. 前缀或裸名)。"""
|
|
35
39
|
return (isinstance(ind_type, str) and
|
|
36
40
|
(ind_type.startswith('qka.') or ind_type in _QKA_BUILTIN_NAMES))
|
|
37
41
|
|
|
38
42
|
|
|
43
|
+
def _callable_source(fn):
|
|
44
|
+
"""取可调用对象的源码文本,用于算配方指纹;取不到返回 None(表示不缓存)。"""
|
|
45
|
+
try:
|
|
46
|
+
return textwrap.dedent(inspect.getsource(fn))
|
|
47
|
+
except (OSError, TypeError):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
39
51
|
# 当日日线数据「可能已经生成」的时刻(本地时间)。一天以它为界切成前后两个时段:
|
|
40
52
|
# 17:00 之前,数据源手里最新的一根还是上一交易日,当天这根根本还没生成;
|
|
41
53
|
# 17:00 之后,当天的已经出完了,要补也就这一次。
|
|
@@ -57,14 +69,15 @@ class Data():
|
|
|
57
69
|
数据管理类
|
|
58
70
|
|
|
59
71
|
负责股票数据的获取、缓存和管理,支持多数据源、并发下载和自定义因子计算。
|
|
60
|
-
通过 `
|
|
61
|
-
|
|
72
|
+
通过 `factors` 参数统一处理技术指标和自定义因子,在数据加载时一次性预计算。
|
|
73
|
+
(`indicators` 是同义旧名,两者完全等价、可互换,保留向后兼容。)
|
|
74
|
+
|
|
62
75
|
Attributes:
|
|
63
76
|
symbols (List[str]): 股票代码列表,格式「代码.市场」,如 ['600519.SH', '000001.SZ']
|
|
64
77
|
(也兼容 sh.600519 旧写法,两种写法等价)
|
|
65
78
|
period (str): 数据周期,如 '1d'、'1m' 等
|
|
66
79
|
adjust (str): 复权方式,如 'qfq'、'hfq'、'bfq'
|
|
67
|
-
|
|
80
|
+
factors (dict | Callable): 预计算指标/因子(旧名 `indicators` 等价)
|
|
68
81
|
source (str): 数据源,默认 'tickflow'(可选 'baostock'、'tdx')
|
|
69
82
|
pool_size (int): 并发下载线程数
|
|
70
83
|
datadir (Path): 数据缓存目录
|
|
@@ -77,6 +90,14 @@ class Data():
|
|
|
77
90
|
# 支持的行情源
|
|
78
91
|
SOURCES = ('tickflow', 'baostock', 'tdx')
|
|
79
92
|
|
|
93
|
+
# 允许「增量缓存」的复权方式。前复权(qfq)**不在其中**。
|
|
94
|
+
# 原因:前复权是「以最新价为锚」,今后的分红送股会往回改写整段历史 ——
|
|
95
|
+
# 缓存若只往后补新棒,文件里就会拼出「前段旧口径 + 后段新口径」的缝,
|
|
96
|
+
# 缝上凭空多出一根假 K 线(实测 000001.SZ 一次分红让整段等比缩 5.57%,
|
|
97
|
+
# 拼接处算出 -4.84%,真实是 +0.91%)。故前复权一律整段重取覆盖,
|
|
98
|
+
# 保证文件里永远只有一种口径。不复权 / 后复权的历史不会被改写,可以放心增量。
|
|
99
|
+
INCREMENTAL_ADJUSTS = ('bfq', 'hfq')
|
|
100
|
+
|
|
80
101
|
# baostock query_history_k_data_plus 完整支持的基础字段(除 date 索引外)
|
|
81
102
|
BAOSTOCK_BASE_FIELDS = ["open", "high", "low", "close", "volume", "amount"]
|
|
82
103
|
# 可通过 extra_fields 追加的扩展字段白名单(行情/估值/选股类)
|
|
@@ -127,9 +148,11 @@ class Data():
|
|
|
127
148
|
source: str = 'tickflow',
|
|
128
149
|
pool_size: int = 10,
|
|
129
150
|
datadir: Optional[Path] = None,
|
|
151
|
+
factors: Optional[dict] = None,
|
|
130
152
|
indicators: Optional[dict] = None,
|
|
131
153
|
extra_fields: Optional[List[str]] = None,
|
|
132
154
|
warmup: int = 0,
|
|
155
|
+
factor_cache: bool = False,
|
|
133
156
|
):
|
|
134
157
|
"""
|
|
135
158
|
初始化数据对象
|
|
@@ -148,31 +171,24 @@ class Data():
|
|
|
148
171
|
])
|
|
149
172
|
```
|
|
150
173
|
|
|
151
|
-
模拟标的同样支持 `
|
|
174
|
+
模拟标的同样支持 `factors` / `extra_fields` / `warmup`,行为与真实源
|
|
152
175
|
一致(列名同样是 {symbol}|{field},扩展列同样是「不请求就不生成」)。
|
|
153
176
|
模拟标的的行情区间由 get(start_date, end_date) 决定 —— 用 baostock 的
|
|
154
177
|
交易日历把区间内的真实交易日取出来,一 日一根,与真实源同构。
|
|
155
178
|
benchmark: 基准代码,如 '000300.SH'。基准数据以 benchmark| 前缀加入最终 DataFrame,
|
|
156
179
|
仅供辅助计算(β/α 等),不参与指标计算
|
|
157
180
|
period: 数据周期,如 '1d'(日线)、'1m'(分钟)
|
|
158
|
-
adjust: 复权方式,'qfq'(前复权)、'hfq'(后复权)、'bfq'
|
|
181
|
+
adjust: 复权方式,'qfq'(前复权)、'hfq'(后复权)、'bfq'(不复权)。
|
|
182
|
+
注意缓存口径:前复权的历史会被今后的分红送股改写,故它**不做增量缓存**
|
|
183
|
+
—— 需要问数据源时整段重取覆盖,保证缓存文件里只有一种口径;
|
|
184
|
+
不复权 / 后复权的历史不会被改写,走增量缓存(只补缺口)。详见 INCREMENTAL_ADJUSTS
|
|
159
185
|
source: 数据来源,默认 'tickflow'(可选 'baostock'、'tdx')。各源返回结构一致,
|
|
160
186
|
切换源不影响上层调用;差异见各源的扩展字段能力。'tdx' 走本机安装的
|
|
161
187
|
「支持 TQ 策略」通达信客户端(仅 Windows,需客户端在运行),安装目录的定位
|
|
162
188
|
方式见 `qka.set_tdx_root` / `qka.get_tdx_root_info`
|
|
163
189
|
pool_size: 并发下载线程数
|
|
164
190
|
datadir: 缓存目录路径
|
|
165
|
-
|
|
166
|
-
extra_fields: 扩展字段列表(选股/估值用,如 ['peTTM', 'pbMRQ', 'turn']),
|
|
167
|
-
可选值见 BAOSTOCK_EXTRA_FIELDS —— 仅 'baostock' 源提供;
|
|
168
|
-
'tickflow' 与 'tdx' 源无扩展列,传入的扩展字段会被静默忽略
|
|
169
|
-
(不报错、不生成该列)。
|
|
170
|
-
追加的列同样遵循 {symbol}|{field} 命名,如 '600519.SH|peTTM'。注意:
|
|
171
|
-
首次下载后缓存字段固定,变更 extra_fields 会自动检测列缺失并重新下载对应股票。
|
|
172
|
-
|
|
173
|
-
warmup: 指标预热天数(默认 0)。回测/取数时自动多读取 warmup 个交易日的历史数据
|
|
174
|
-
用于计算指标,使第 1 个交易日即可拿到有效指标值,无需在策略里手写
|
|
175
|
-
`if len(hist) < N: continue` 跳过前 N 根 bar。仅用于计算,不会增加 on_bar 调用次数。
|
|
191
|
+
factors: 预计算指标/因子(**推荐写法**)。支持两种写法(dict / callable;不传即不算):
|
|
176
192
|
|
|
177
193
|
**1. 字典(混搭 ta 函数和自定义因子):**
|
|
178
194
|
```python
|
|
@@ -187,13 +203,32 @@ class Data():
|
|
|
187
203
|
每个条目独立产一列,直接透传 ta 库全部指标。
|
|
188
204
|
列名参数('close'/'high' 等)放在 ta 路径之后、数值参数之前;
|
|
189
205
|
多列函数(如 ATR)连续列出所有列名即可。
|
|
190
|
-
|
|
206
|
+
|
|
191
207
|
**2. 函数(自定义因子):**
|
|
192
208
|
```python
|
|
193
|
-
|
|
209
|
+
factors=lambda df: df.assign(ma5=df['close'].rolling(5).mean())
|
|
194
210
|
```
|
|
195
211
|
函数接收单只股票的 DataFrame,返回添加了额外列的 DataFrame。
|
|
196
212
|
|
|
213
|
+
indicators: `factors` 的同义旧名,行为完全一致、保留向后兼容;两者同时传会报错。
|
|
214
|
+
|
|
215
|
+
extra_fields: 扩展字段列表(选股/估值用,如 ['peTTM', 'pbMRQ', 'turn']),
|
|
216
|
+
可选值见 BAOSTOCK_EXTRA_FIELDS —— 仅 'baostock' 源提供;
|
|
217
|
+
'tickflow' 与 'tdx' 源无扩展列,传入的扩展字段会被静默忽略
|
|
218
|
+
(不报错、不生成该列)。
|
|
219
|
+
追加的列同样遵循 {symbol}|{field} 命名,如 '600519.SH|peTTM'。注意:
|
|
220
|
+
首次下载后缓存字段固定,变更 extra_fields 会自动检测列缺失并重新下载对应股票。
|
|
221
|
+
|
|
222
|
+
warmup: 因子预热天数(默认 0)。回测/取数时自动多读取 warmup 个交易日的历史数据
|
|
223
|
+
用于计算因子,使第 1 个交易日即可拿到有效值,无需在策略里手写
|
|
224
|
+
`if len(hist) < N: continue` 跳过前 N 根 bar。仅用于计算,不会增加 on_bar 调用次数。
|
|
225
|
+
|
|
226
|
+
factor_cache: 是否把算出来的因子**纵向落盘**(默认 False,即不缓存)。
|
|
227
|
+
开启后,因子按 `datadir/factors/{因子名}/{symbol}.parquet` 存,
|
|
228
|
+
命中就直接读、只缺尾巴就只补尾巴;配方或数据口径变了则整条重算。
|
|
229
|
+
详见 `skills/qka/references/data.md` 的「因子缓存」。
|
|
230
|
+
只作用于默认(`lazy=False`)路径。
|
|
231
|
+
|
|
197
232
|
"""
|
|
198
233
|
# ── 归一化 symbols:str = 真实标的,Simulate 对象 = 模拟标的,可混排 ──
|
|
199
234
|
names, sim_specs = [], {}
|
|
@@ -217,11 +252,15 @@ class Data():
|
|
|
217
252
|
self.benchmark = benchmark
|
|
218
253
|
self.period = period
|
|
219
254
|
self.adjust = adjust
|
|
255
|
+
# 该复权方式是否允许增量缓存(前复权不允许,见 INCREMENTAL_ADJUSTS)
|
|
256
|
+
self.incremental = adjust in self.INCREMENTAL_ADJUSTS
|
|
220
257
|
if source not in self.SOURCES:
|
|
221
258
|
raise ValueError(f"不支持的数据源: {source!r}。可选: {self.SOURCES}")
|
|
222
259
|
self.source = source
|
|
223
260
|
self.pool_size = pool_size
|
|
224
261
|
self.warmup = warmup
|
|
262
|
+
# 因子缓存:默认关。要用得显式开(见 _factors_with_cache)
|
|
263
|
+
self.factor_cache = bool(factor_cache)
|
|
225
264
|
|
|
226
265
|
# 当前数据源实际支持的扩展字段(源能力不同,按源过滤)
|
|
227
266
|
whitelist = self._extra_fields_whitelist(source)
|
|
@@ -244,18 +283,24 @@ class Data():
|
|
|
244
283
|
if f not in self.extra_fields:
|
|
245
284
|
self.extra_fields.append(f)
|
|
246
285
|
|
|
247
|
-
# 统一处理 indicators
|
|
248
|
-
if
|
|
286
|
+
# 统一处理 factors / indicators 参数(同一个东西的两个名字,等价)
|
|
287
|
+
if factors is not None and indicators is not None:
|
|
288
|
+
raise TypeError(
|
|
289
|
+
"factors 与 indicators 是同一个参数的新旧两个名字,"
|
|
290
|
+
"等价且只能传一个,请只传 factors"
|
|
291
|
+
)
|
|
292
|
+
specs = factors if factors is not None else indicators
|
|
293
|
+
if callable(specs):
|
|
249
294
|
# 函数形式 → 保存为 callable
|
|
250
|
-
self.
|
|
251
|
-
elif isinstance(
|
|
295
|
+
self._factors = specs
|
|
296
|
+
elif isinstance(specs, dict):
|
|
252
297
|
# 字典形式
|
|
253
|
-
self.
|
|
254
|
-
elif
|
|
255
|
-
self.
|
|
298
|
+
self._factors = specs
|
|
299
|
+
elif specs is None:
|
|
300
|
+
self._factors = {}
|
|
256
301
|
else:
|
|
257
302
|
raise TypeError(
|
|
258
|
-
f"
|
|
303
|
+
f"factors 必须是 dict、callable 或 None,got {type(specs)}"
|
|
259
304
|
)
|
|
260
305
|
|
|
261
306
|
# 初始化缓存目录
|
|
@@ -494,6 +539,9 @@ class Data():
|
|
|
494
539
|
只补下载缺失的部分(前面缺失补前面,后面缺失补后面),合并去重写回。
|
|
495
540
|
若缓存缺少 extra_fields 指定的列(如从无扩展字段升级到有),则全量重新下载。
|
|
496
541
|
|
|
542
|
+
例外:不允许增量缓存的复权方式(前复权,见 INCREMENTAL_ADJUSTS)不走增量,
|
|
543
|
+
一旦需要问数据源就整段重取覆盖,详见 _rewrite_full。
|
|
544
|
+
|
|
497
545
|
每次问完数据源都会留下痕迹:补到新数据就写文件(修改时间自然刷新),
|
|
498
546
|
没补到就刷新修改时间 + 记下「已确认起点」。这两样让重复的请求不必再问网络,
|
|
499
547
|
详见 _needs_download。
|
|
@@ -516,6 +564,14 @@ class Data():
|
|
|
516
564
|
# 实际请求的扩展字段 = 当前配置 ∪ 缓存已有扩展列(列只增不减,不互相覆盖)
|
|
517
565
|
merged_extra = self._merged_extra_fields(path)
|
|
518
566
|
|
|
567
|
+
# ── 不通增量的复权方式(前复权):整段重取、整段覆盖 ──
|
|
568
|
+
# 前复权的历史会被今后的分红送股改写,只补新棒就会拼出「前段旧口径 + 后段新口径」
|
|
569
|
+
# 的缝(见 INCREMENTAL_ADJUSTS),故一旦需要问数据源就整段重新要一遍、整体替换。
|
|
570
|
+
if path.exists() and not self.incremental:
|
|
571
|
+
return self._rewrite_full(
|
|
572
|
+
path, symbol, download_start, download_end, merged_extra
|
|
573
|
+
)
|
|
574
|
+
|
|
519
575
|
# ── 首次下载:只拉请求范围(缓存缺失 extra_fields 列时也全量重下)──
|
|
520
576
|
if not path.exists() or self._cache_missing_extra_fields(path):
|
|
521
577
|
df = self._fetch(
|
|
@@ -597,6 +653,54 @@ class Data():
|
|
|
597
653
|
pq.write_table(table, path)
|
|
598
654
|
return path
|
|
599
655
|
|
|
656
|
+
def _rewrite_full(
|
|
657
|
+
self, path: Path, symbol: str,
|
|
658
|
+
download_start: str = None, download_end: str = None,
|
|
659
|
+
extra_fields: Optional[List[str]] = None,
|
|
660
|
+
) -> Path:
|
|
661
|
+
"""整段重取并覆盖缓存(供不允许增量缓存的复权方式使用)。
|
|
662
|
+
|
|
663
|
+
为什么要盖住缓存已存的整段:只下「请求范围」会把长历史截短;只补缺口又会
|
|
664
|
+
留下范围外的老口径数据 —— 那也是缝。所以范围取「缓存已有范围 ∪ 请求范围」,
|
|
665
|
+
重取回来的整段是同一个口径,整体替换。
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
path: 缓存文件路径
|
|
669
|
+
symbol: 标的代码
|
|
670
|
+
download_start / download_end: 本次请求范围(None 表示不设边界)
|
|
671
|
+
extra_fields: 本次要请求的扩展字段
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
Path: 数据文件路径
|
|
675
|
+
|
|
676
|
+
Raises:
|
|
677
|
+
RuntimeError: 数据源返回空数据(此时保留原缓存,绝不拿空数据覆盖)
|
|
678
|
+
"""
|
|
679
|
+
existing = pd.read_parquet(path)
|
|
680
|
+
cache_min = existing.index.min()
|
|
681
|
+
cache_max = existing.index.max()
|
|
682
|
+
|
|
683
|
+
req_start = pd.Timestamp(download_start) if download_start else None
|
|
684
|
+
req_end = (pd.Timestamp(download_end) if download_end
|
|
685
|
+
else pd.Timestamp.now().floor('D'))
|
|
686
|
+
|
|
687
|
+
range_start = min(x for x in (cache_min, req_start) if x is not None)
|
|
688
|
+
range_end = max(cache_max, req_end)
|
|
689
|
+
|
|
690
|
+
df = self._fetch(
|
|
691
|
+
symbol,
|
|
692
|
+
start_date=range_start.strftime('%Y-%m-%d'),
|
|
693
|
+
end_date=range_end.strftime('%Y-%m-%d'),
|
|
694
|
+
extra_fields=extra_fields,
|
|
695
|
+
)
|
|
696
|
+
if len(df) == 0:
|
|
697
|
+
raise RuntimeError(
|
|
698
|
+
f"{symbol}: 数据源({self.source})返回空数据,已保留原缓存"
|
|
699
|
+
)
|
|
700
|
+
# 这次就是从这个起点问的,一并记下「已确认起点」
|
|
701
|
+
self._write_verified_from(path, pa.Table.from_pandas(df), range_start)
|
|
702
|
+
return path
|
|
703
|
+
|
|
600
704
|
def _needs_download(
|
|
601
705
|
self, symbol: str,
|
|
602
706
|
download_start: str = None,
|
|
@@ -659,18 +763,18 @@ class Data():
|
|
|
659
763
|
lazy=True: dd.DataFrame,列名格式 {symbol}|{factor}
|
|
660
764
|
没有数据时抛出 RuntimeError
|
|
661
765
|
|
|
662
|
-
|
|
663
|
-
|
|
766
|
+
注意:有因子时,start_date 会自动向后扩展 max_window 个交易日读取缓存,
|
|
767
|
+
确保因子有足够的预热数据。最终返回的 DataFrame 仍严格限定在 [start_date, end_date]。
|
|
664
768
|
|
|
665
769
|
模拟标的的日期轴取自 baostock 交易日历(区间内一日一根),因此需要能登录 baostock。
|
|
666
770
|
"""
|
|
667
771
|
if not self.symbols:
|
|
668
772
|
return pd.DataFrame()
|
|
669
773
|
|
|
670
|
-
#
|
|
671
|
-
max_window = self.
|
|
774
|
+
# 计算因子预热窗口
|
|
775
|
+
max_window = self._min_rows_for_factors()
|
|
672
776
|
|
|
673
|
-
# 构建 parquet predicate pushdown 过滤(日期 +
|
|
777
|
+
# 构建 parquet predicate pushdown 过滤(日期 + 因子预热扩展)
|
|
674
778
|
pq_filters = None
|
|
675
779
|
read_start = None
|
|
676
780
|
if start_date is not None or end_date is not None or max_window > 0:
|
|
@@ -679,9 +783,9 @@ class Data():
|
|
|
679
783
|
read_start = pd.Timestamp(start_date)
|
|
680
784
|
if max_window > 0:
|
|
681
785
|
if read_start is not None:
|
|
682
|
-
#
|
|
786
|
+
# 向后扩展,确保因子有足够的预热数据
|
|
683
787
|
read_start = read_start - pd.offsets.BDay(max_window * 2)
|
|
684
|
-
# 无 start_date
|
|
788
|
+
# 无 start_date 但有因子时,不需要扩展(从头读即可)
|
|
685
789
|
if read_start is not None:
|
|
686
790
|
pq_filters.append(('date', '>=', read_start))
|
|
687
791
|
if end_date is not None:
|
|
@@ -779,7 +883,7 @@ class Data():
|
|
|
779
883
|
continue
|
|
780
884
|
ddf = dd.read_parquet(str(parquet_path), filters=pq_filters)
|
|
781
885
|
ddf['returns'] = ddf['close'].diff() / ddf['close'].shift(1)
|
|
782
|
-
ddf = self.
|
|
886
|
+
ddf = self._apply_factors(ddf)
|
|
783
887
|
column_mapping = {col: f'{symbol}|{col}' for col in ddf.columns}
|
|
784
888
|
dfs.append(ddf.rename(columns=column_mapping))
|
|
785
889
|
|
|
@@ -800,7 +904,7 @@ class Data():
|
|
|
800
904
|
ddf = self._add_benchmark(ddf, self.target_dir, pq_filters)
|
|
801
905
|
|
|
802
906
|
# qka 内置指标(需要 benchmark 和 symbol 前缀)
|
|
803
|
-
ddf = self.
|
|
907
|
+
ddf = self._apply_qka_factors(ddf)
|
|
804
908
|
|
|
805
909
|
return ddf
|
|
806
910
|
|
|
@@ -819,7 +923,6 @@ class Data():
|
|
|
819
923
|
continue
|
|
820
924
|
df = dd.read_parquet(str(parquet_path), filters=pq_filters)
|
|
821
925
|
df['returns'] = df['close'].diff() / df['close'].shift(1)
|
|
822
|
-
df = self._apply_indicators(df)
|
|
823
926
|
column_mapping = {col: f'{symbol}|{col}' for col in df.columns}
|
|
824
927
|
dfs.append(df.rename(columns=column_mapping))
|
|
825
928
|
|
|
@@ -831,6 +934,11 @@ class Data():
|
|
|
831
934
|
|
|
832
935
|
ddf = dd.concat(dfs, axis=1, join='outer')
|
|
833
936
|
result = ddf.compute()
|
|
937
|
+
|
|
938
|
+
# 因子:逐票逐因子贴上(带缓存 —— 全盖住就不算,只缺尾巴就只补尾巴)
|
|
939
|
+
# 注意在切片之前算:此时还带着预热数据,滚动窗口才够长。
|
|
940
|
+
result = self._factors_with_cache(result)
|
|
941
|
+
|
|
834
942
|
# 切片回用户请求的日期范围
|
|
835
943
|
if start_date is not None and max_window > 0:
|
|
836
944
|
result = result[result.index >= pd.Timestamp(start_date)]
|
|
@@ -839,10 +947,11 @@ class Data():
|
|
|
839
947
|
if self.benchmark:
|
|
840
948
|
result = self._add_benchmark(result, self.target_dir, pq_filters)
|
|
841
949
|
|
|
842
|
-
# qka
|
|
843
|
-
result = self.
|
|
950
|
+
# qka 内置因子(需要 benchmark 和 symbol 前缀)
|
|
951
|
+
result = self._qka_factors_with_cache(result)
|
|
844
952
|
|
|
845
|
-
|
|
953
|
+
# 列顺序复原,与重构前一致
|
|
954
|
+
return self._order_columns(result, self._qka_factor_names())
|
|
846
955
|
|
|
847
956
|
def _add_benchmark(self, df, target_dir, pq_filters):
|
|
848
957
|
"""将基准数据的 returns 以 benchmark| 前缀追加到 DataFrame。"""
|
|
@@ -861,7 +970,7 @@ class Data():
|
|
|
861
970
|
df['benchmark|returns'] = bench_returns.reindex(df.index)
|
|
862
971
|
return df
|
|
863
972
|
|
|
864
|
-
def
|
|
973
|
+
def _apply_factors(self, df):
|
|
865
974
|
"""
|
|
866
975
|
对单只股票的数据应用预定义的指标/因子。
|
|
867
976
|
|
|
@@ -876,7 +985,7 @@ class Data():
|
|
|
876
985
|
Returns:
|
|
877
986
|
DataFrame: 包含原始列和指标列
|
|
878
987
|
"""
|
|
879
|
-
inds = self.
|
|
988
|
+
inds = self._factors
|
|
880
989
|
|
|
881
990
|
# 空 → 跳过
|
|
882
991
|
if not inds:
|
|
@@ -892,23 +1001,30 @@ class Data():
|
|
|
892
1001
|
if isinstance(df, dd.DataFrame):
|
|
893
1002
|
# 样本行数完全由 warmup 决定(用户显式声明前置窗口),不写死兜底值。
|
|
894
1003
|
# warmup=0 表示用户不预留前置数据,lambda 指标可能因此无法物化——属用户责任。
|
|
895
|
-
sample = df.head(self.
|
|
896
|
-
meta = self.
|
|
1004
|
+
sample = df.head(self._min_rows_for_factors())
|
|
1005
|
+
meta = self._compute_factor_cols(sample.copy())
|
|
897
1006
|
return df.map_partitions(
|
|
898
|
-
lambda partition: self.
|
|
1007
|
+
lambda partition: self._compute_factor_cols(partition.copy()),
|
|
899
1008
|
meta=meta,
|
|
900
1009
|
)
|
|
901
|
-
return self.
|
|
1010
|
+
return self._compute_factor_cols(df.copy())
|
|
1011
|
+
|
|
1012
|
+
def _compute_factor_cols(self, df, only=None):
|
|
1013
|
+
"""在 pandas DataFrame 上计算指标/因子列(单只股票)。
|
|
902
1014
|
|
|
903
|
-
|
|
904
|
-
|
|
1015
|
+
Args:
|
|
1016
|
+
df: 单票 DataFrame
|
|
1017
|
+
only: 只算这几个列名(None = 全算)。因子缓存按因子粒度补算时用。
|
|
1018
|
+
"""
|
|
905
1019
|
|
|
906
1020
|
# 分区过小时跳过,避免 ta-lib 在 dask meta 推断时崩溃
|
|
907
|
-
min_rows = self.
|
|
1021
|
+
min_rows = self._min_rows_for_factors()
|
|
908
1022
|
if len(df) < min_rows:
|
|
909
1023
|
return df
|
|
910
1024
|
|
|
911
|
-
for col_name, spec in self.
|
|
1025
|
+
for col_name, spec in self._factors.items():
|
|
1026
|
+
if only is not None and col_name not in only:
|
|
1027
|
+
continue
|
|
912
1028
|
# 自定义因子(callable 值)
|
|
913
1029
|
if callable(spec):
|
|
914
1030
|
result = spec(df)
|
|
@@ -928,22 +1044,22 @@ class Data():
|
|
|
928
1044
|
|
|
929
1045
|
ind_type = spec[0]
|
|
930
1046
|
|
|
931
|
-
# qka 内置指标:跳过,由
|
|
932
|
-
if
|
|
1047
|
+
# qka 内置指标:跳过,由 _apply_qka_factors 集中处理
|
|
1048
|
+
if _is_qka_factor(ind_type):
|
|
933
1049
|
continue
|
|
934
1050
|
|
|
935
1051
|
args = list(spec[1:])
|
|
936
1052
|
|
|
937
1053
|
# 收集前面连续的字符串参数作为列名(遇到第一个非字符串即停)
|
|
938
|
-
|
|
1054
|
+
cols = []
|
|
939
1055
|
rest_start = 0
|
|
940
1056
|
for i, arg in enumerate(args):
|
|
941
1057
|
if isinstance(arg, str):
|
|
942
|
-
|
|
1058
|
+
cols.append(arg)
|
|
943
1059
|
rest_start = i + 1
|
|
944
1060
|
else:
|
|
945
1061
|
break
|
|
946
|
-
if not
|
|
1062
|
+
if not cols:
|
|
947
1063
|
logger.warning(f"指标 {col_name} 缺少计算列名,跳过")
|
|
948
1064
|
continue
|
|
949
1065
|
rest = args[rest_start:]
|
|
@@ -966,14 +1082,14 @@ class Data():
|
|
|
966
1082
|
logger.warning(f"无法加载指标函数 {ind_type}: {e}")
|
|
967
1083
|
continue
|
|
968
1084
|
|
|
969
|
-
if len(
|
|
970
|
-
df[col_name] = fn(df[
|
|
1085
|
+
if len(cols) == 1:
|
|
1086
|
+
df[col_name] = fn(df[cols[0]], *rest)
|
|
971
1087
|
else:
|
|
972
|
-
df[col_name] = fn(*[df[f] for f in
|
|
1088
|
+
df[col_name] = fn(*[df[f] for f in cols], *rest)
|
|
973
1089
|
|
|
974
1090
|
return df
|
|
975
1091
|
|
|
976
|
-
def
|
|
1092
|
+
def _apply_qka_factors(self, df):
|
|
977
1093
|
"""应用 qka 内置指标(带 symbol 前缀,含 benchmark 数据)。
|
|
978
1094
|
|
|
979
1095
|
必须在 concat + 加 benchmark 之后调用,此时列名格式为
|
|
@@ -985,28 +1101,28 @@ class Data():
|
|
|
985
1101
|
Returns:
|
|
986
1102
|
DataFrame: 含 qka 指标列
|
|
987
1103
|
"""
|
|
988
|
-
inds = self.
|
|
1104
|
+
inds = self._factors
|
|
989
1105
|
if not inds or not isinstance(inds, dict):
|
|
990
1106
|
return df
|
|
991
1107
|
|
|
992
1108
|
qka_specs = [
|
|
993
1109
|
(col_name, spec) for col_name, spec in inds.items()
|
|
994
1110
|
if isinstance(spec, (list, tuple))
|
|
995
|
-
and
|
|
1111
|
+
and _is_qka_factor(spec[0])
|
|
996
1112
|
]
|
|
997
1113
|
if not qka_specs:
|
|
998
1114
|
return df
|
|
999
1115
|
|
|
1000
1116
|
if isinstance(df, dd.DataFrame):
|
|
1001
|
-
sample = df.head(self.
|
|
1002
|
-
meta = self.
|
|
1117
|
+
sample = df.head(self._min_rows_for_factors())
|
|
1118
|
+
meta = self._compute_qka_factor_cols(sample, qka_specs)
|
|
1003
1119
|
return df.map_partitions(
|
|
1004
|
-
lambda p: self.
|
|
1120
|
+
lambda p: self._compute_qka_factor_cols(p, qka_specs),
|
|
1005
1121
|
meta=meta,
|
|
1006
1122
|
)
|
|
1007
|
-
return self.
|
|
1123
|
+
return self._compute_qka_factor_cols(df, qka_specs)
|
|
1008
1124
|
|
|
1009
|
-
def
|
|
1125
|
+
def _compute_qka_factor_cols(self, df, qka_specs, symbols=None):
|
|
1010
1126
|
"""在已合并的 DataFrame 上计算 qka 内置指标。"""
|
|
1011
1127
|
for col_key, spec in qka_specs:
|
|
1012
1128
|
fn_name = spec[0].split('.', 1)[1] if spec[0].startswith('qka.') else spec[0]
|
|
@@ -1019,7 +1135,7 @@ class Data():
|
|
|
1019
1135
|
continue
|
|
1020
1136
|
|
|
1021
1137
|
# 为每只股票计算
|
|
1022
|
-
for symbol in self.symbols:
|
|
1138
|
+
for symbol in (symbols if symbols is not None else self.symbols):
|
|
1023
1139
|
col = f'{symbol}|{col_key}'
|
|
1024
1140
|
try:
|
|
1025
1141
|
df[col] = fn(df, symbol, *args)
|
|
@@ -1029,16 +1145,16 @@ class Data():
|
|
|
1029
1145
|
|
|
1030
1146
|
return df
|
|
1031
1147
|
|
|
1032
|
-
def
|
|
1033
|
-
"""
|
|
1148
|
+
def _min_rows_for_factors(self):
|
|
1149
|
+
"""计算所有因子所需的最小行数(含显式预热天数)。
|
|
1034
1150
|
|
|
1035
|
-
|
|
1151
|
+
遍历所有因子条目的整数参数,取最大值作为窗口上限的保守估计,
|
|
1036
1152
|
再与显式 warmup 取较大者。
|
|
1037
1153
|
"""
|
|
1038
|
-
if not self.
|
|
1154
|
+
if not self._factors or not isinstance(self._factors, dict):
|
|
1039
1155
|
return self.warmup
|
|
1040
1156
|
max_window = 0
|
|
1041
|
-
for spec in self.
|
|
1157
|
+
for spec in self._factors.values():
|
|
1042
1158
|
if callable(spec) or not isinstance(spec, (list, tuple)):
|
|
1043
1159
|
continue
|
|
1044
1160
|
# spec[1:] 中跳过所有前导字符串(列名),收集剩余整数
|
|
@@ -1051,6 +1167,293 @@ class Data():
|
|
|
1051
1167
|
max_window = max(max_window, v)
|
|
1052
1168
|
return max(max_window, self.warmup)
|
|
1053
1169
|
|
|
1170
|
+
# ── 因子缓存 ────────────────────────────────────────────────
|
|
1171
|
+
# 因子就是从行情算出来的「下一列」,所以跟行情一样纵向落盘。
|
|
1172
|
+
# 但目录**不按数据源/周期/复权分**(那三样是「行情」的口径),而是按**因子名**分:
|
|
1173
|
+
# 一个因子 = 一份独立的资产,可以单独查、单独发布、单独排名。
|
|
1174
|
+
#
|
|
1175
|
+
# datadir/factors/{因子名}/{symbol}.parquet (单列,索引同行情)
|
|
1176
|
+
#
|
|
1177
|
+
# 数据源 / 周期 / 复权 + 因子定义本身,一起压进文件的「配方指纹」——
|
|
1178
|
+
# 只要其中任一样变了,指纹就对不上,该票该因子整条重算并覆盖。
|
|
1179
|
+
|
|
1180
|
+
FACTOR_DIR = 'factors'
|
|
1181
|
+
FACTOR_META_KEY = b'qka_factor_meta'
|
|
1182
|
+
FACTOR_CACHE_VERSION = 1
|
|
1183
|
+
_UNSET = object()
|
|
1184
|
+
|
|
1185
|
+
def _factor_plan(self):
|
|
1186
|
+
"""{因子名: (定义, 指纹)};只要有因子认不出配方就整体返回 None(这次不缓存)。
|
|
1187
|
+
|
|
1188
|
+
认不出的典型是「取不到源码的可调用对象」(REPL 里现写的 lambda、
|
|
1189
|
+
动态拼出来的函数)——它的指纹没法跨进程稳定,宁可每次现算,
|
|
1190
|
+
也不能拿旧配方算出来的值冒充新值。
|
|
1191
|
+
"""
|
|
1192
|
+
plan = getattr(self, '_factor_plan_cache', self._UNSET)
|
|
1193
|
+
if plan is not self._UNSET:
|
|
1194
|
+
return plan
|
|
1195
|
+
|
|
1196
|
+
plan = None
|
|
1197
|
+
if isinstance(self._factors, dict) and self._factors:
|
|
1198
|
+
built = {}
|
|
1199
|
+
for name, spec in self._factors.items():
|
|
1200
|
+
fp = self._factor_signature(name, spec)
|
|
1201
|
+
if fp is None:
|
|
1202
|
+
built = None
|
|
1203
|
+
break
|
|
1204
|
+
built[name] = (spec, fp)
|
|
1205
|
+
plan = built
|
|
1206
|
+
self._factor_plan_cache = plan
|
|
1207
|
+
return plan
|
|
1208
|
+
|
|
1209
|
+
def _factor_signature(self, name, spec):
|
|
1210
|
+
"""单个因子的配方指纹(定义 + 数据口径);认不出返回 None。"""
|
|
1211
|
+
payload = self._spec_payload(spec)
|
|
1212
|
+
if payload is None:
|
|
1213
|
+
return None
|
|
1214
|
+
blob = json.dumps({
|
|
1215
|
+
'v': self.FACTOR_CACHE_VERSION,
|
|
1216
|
+
'name': name,
|
|
1217
|
+
'spec': payload,
|
|
1218
|
+
'source': self.source,
|
|
1219
|
+
'period': self.period,
|
|
1220
|
+
'adjust': self.adjust,
|
|
1221
|
+
}, sort_keys=True, ensure_ascii=False)
|
|
1222
|
+
return hashlib.sha1(blob.encode('utf-8')).hexdigest()
|
|
1223
|
+
|
|
1224
|
+
@staticmethod
|
|
1225
|
+
def _spec_payload(spec):
|
|
1226
|
+
"""把一份因子定义转成可稳定序列化的结构;认不出返回 None。"""
|
|
1227
|
+
if callable(spec):
|
|
1228
|
+
src = _callable_source(spec)
|
|
1229
|
+
return None if src is None else {'fn': src}
|
|
1230
|
+
if isinstance(spec, (list, tuple)):
|
|
1231
|
+
if all(isinstance(x, (str, int, float, bool)) for x in spec):
|
|
1232
|
+
return list(spec)
|
|
1233
|
+
return None
|
|
1234
|
+
return None
|
|
1235
|
+
|
|
1236
|
+
def _factor_cache_path(self, name: str, symbol: str) -> Path:
|
|
1237
|
+
return self.datadir / self.FACTOR_DIR / name / f'{symbol}.parquet'
|
|
1238
|
+
|
|
1239
|
+
def _read_factor_cache(self, name, symbol, fingerprint):
|
|
1240
|
+
"""读该票该因子的缓存序列;文件不在 / 指纹不符 / 读不出来 → None。
|
|
1241
|
+
|
|
1242
|
+
不在这里判断「盖没盖住范围」——那是 `_recompute_target` 的事:
|
|
1243
|
+
盖住了就一个字不算,只缺尾巴就只补尾巴。
|
|
1244
|
+
"""
|
|
1245
|
+
path = self._factor_cache_path(name, symbol)
|
|
1246
|
+
if not path.exists():
|
|
1247
|
+
return None
|
|
1248
|
+
try:
|
|
1249
|
+
raw = (pq.read_schema(path).metadata or {}).get(self.FACTOR_META_KEY)
|
|
1250
|
+
if not raw or json.loads(raw.decode()).get('fingerprint') != fingerprint:
|
|
1251
|
+
return None
|
|
1252
|
+
df = pd.read_parquet(path)
|
|
1253
|
+
except Exception:
|
|
1254
|
+
return None
|
|
1255
|
+
if df.empty or name not in df.columns or not isinstance(df.index, pd.DatetimeIndex):
|
|
1256
|
+
return None
|
|
1257
|
+
return df[name]
|
|
1258
|
+
|
|
1259
|
+
def _write_factor_cache(self, name, symbol, fingerprint, series) -> None:
|
|
1260
|
+
"""把该票该因子的值并进缓存文件。
|
|
1261
|
+
|
|
1262
|
+
同一指纹下只增不改(老值按索引保留,新算的覆盖同日);指纹变了说明配方或
|
|
1263
|
+
口径换了,整条重写——绝不把两套口径的值混在一个文件里。
|
|
1264
|
+
"""
|
|
1265
|
+
if series is None or series.empty:
|
|
1266
|
+
return
|
|
1267
|
+
path = self._factor_cache_path(name, symbol)
|
|
1268
|
+
merged = pd.DataFrame({name: series})
|
|
1269
|
+
if path.exists():
|
|
1270
|
+
try:
|
|
1271
|
+
old_meta = (pq.read_schema(path).metadata or {}).get(self.FACTOR_META_KEY)
|
|
1272
|
+
same = bool(old_meta) and \
|
|
1273
|
+
json.loads(old_meta.decode()).get('fingerprint') == fingerprint
|
|
1274
|
+
old = pd.read_parquet(path)
|
|
1275
|
+
if same and name in old.columns:
|
|
1276
|
+
merged = pd.concat([old[[name]], merged])
|
|
1277
|
+
merged = merged[~merged.index.duplicated(keep='last')].sort_index()
|
|
1278
|
+
except Exception:
|
|
1279
|
+
pass
|
|
1280
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1281
|
+
table = pa.Table.from_pandas(merged)
|
|
1282
|
+
meta = dict(table.schema.metadata or {})
|
|
1283
|
+
meta[self.FACTOR_META_KEY] = json.dumps({
|
|
1284
|
+
'fingerprint': fingerprint,
|
|
1285
|
+
'v': self.FACTOR_CACHE_VERSION,
|
|
1286
|
+
'source': self.source,
|
|
1287
|
+
'period': self.period,
|
|
1288
|
+
'adjust': self.adjust,
|
|
1289
|
+
}, ensure_ascii=False).encode()
|
|
1290
|
+
pq.write_table(table.replace_schema_metadata(meta), path)
|
|
1291
|
+
|
|
1292
|
+
def _recompute_target(self, full, cached):
|
|
1293
|
+
"""算出「这次要重算哪一段」。
|
|
1294
|
+
|
|
1295
|
+
- 没有缓存 → 整段算(返回 full)
|
|
1296
|
+
- 缓存全盖住 → 不用算(返回 None)
|
|
1297
|
+
- 只缺尾巴 → **只算尾巴**(往前留出预热窗口,滚动指标才准)
|
|
1298
|
+
- 前面也缺 → 整段算
|
|
1299
|
+
"""
|
|
1300
|
+
if cached is None:
|
|
1301
|
+
return full
|
|
1302
|
+
if cached.index.min() <= full.index.min() and cached.index.max() >= full.index.max():
|
|
1303
|
+
return None
|
|
1304
|
+
if cached.index.min() > full.index.min():
|
|
1305
|
+
return full
|
|
1306
|
+
min_rows = self._min_rows_for_factors()
|
|
1307
|
+
if min_rows <= 0:
|
|
1308
|
+
# 窗口完全靠猜(lambda + 没声明 warmup)⇒ 不敢只补尾巴,整段重算
|
|
1309
|
+
return full
|
|
1310
|
+
margin = pd.offsets.BDay(min_rows * 2)
|
|
1311
|
+
tail = full.loc[cached.index.max() - margin:].copy()
|
|
1312
|
+
if len(tail) < min_rows:
|
|
1313
|
+
return full
|
|
1314
|
+
return tail
|
|
1315
|
+
|
|
1316
|
+
@staticmethod
|
|
1317
|
+
def _symbol_slice(result, symbol):
|
|
1318
|
+
"""从宽表里取出该票的列、去掉 {symbol}| 前缀(缺失返回空表)。"""
|
|
1319
|
+
prefix = f'{symbol}|'
|
|
1320
|
+
cols = [c for c in result.columns if c.startswith(prefix)]
|
|
1321
|
+
sub = result[cols].copy()
|
|
1322
|
+
sub.columns = [c[len(prefix):] for c in cols]
|
|
1323
|
+
return sub
|
|
1324
|
+
|
|
1325
|
+
@staticmethod
|
|
1326
|
+
def _attach_slice(result, symbol, sub, names):
|
|
1327
|
+
for name in names:
|
|
1328
|
+
if name in sub.columns:
|
|
1329
|
+
result[f'{symbol}|{name}'] = sub[name]
|
|
1330
|
+
|
|
1331
|
+
def _qka_factor_names(self):
|
|
1332
|
+
"""`self._factors` 里属于 qka 内置因子(α/β/夏普…)的列名,按声明顺序。"""
|
|
1333
|
+
if not isinstance(self._factors, dict):
|
|
1334
|
+
return []
|
|
1335
|
+
return [
|
|
1336
|
+
n for n, spec in self._factors.items()
|
|
1337
|
+
if isinstance(spec, (list, tuple)) and _is_qka_factor(spec[0])
|
|
1338
|
+
]
|
|
1339
|
+
|
|
1340
|
+
def _order_columns(self, result, qka_names):
|
|
1341
|
+
"""把列重排回重构前的顺序,纯粹为了不改变既有行为。
|
|
1342
|
+
|
|
1343
|
+
重构前:每票的行情列 → returns → ta/自定义因子,逐票成组;然后是 benchmark,
|
|
1344
|
+
最后是 qka 内置因子(**先按因子名、再按标的**)。
|
|
1345
|
+
因子缓存把那两类因子都挪到帧尾贴上,顺序就散了。消费者虽然都按列名取值,
|
|
1346
|
+
但没理由让它们替我们承担这个差异。
|
|
1347
|
+
"""
|
|
1348
|
+
known = set(self.symbols)
|
|
1349
|
+
qka_cols = {f'{s}|{n}' for n in qka_names for s in self.symbols}
|
|
1350
|
+
groups, rest, tail = {}, [], []
|
|
1351
|
+
for col in result.columns:
|
|
1352
|
+
if col in qka_cols:
|
|
1353
|
+
tail.append(col)
|
|
1354
|
+
continue
|
|
1355
|
+
symbol = col.split('|', 1)[0] if '|' in col else None
|
|
1356
|
+
if symbol in known:
|
|
1357
|
+
groups.setdefault(symbol, []).append(col)
|
|
1358
|
+
else:
|
|
1359
|
+
rest.append(col)
|
|
1360
|
+
order = []
|
|
1361
|
+
for symbol in self.symbols:
|
|
1362
|
+
order.extend(groups.get(symbol, ()))
|
|
1363
|
+
order.extend(rest)
|
|
1364
|
+
order.extend(tail)
|
|
1365
|
+
return result[order]
|
|
1366
|
+
|
|
1367
|
+
def _factors_with_cache(self, result):
|
|
1368
|
+
"""逐票把 ta / 自定义因子贴到 result 上:命中缓存直接用,没命中才现算并写回。
|
|
1369
|
+
|
|
1370
|
+
只用于默认(非懒加载)路径。懒加载路径行为完全不变。
|
|
1371
|
+
"""
|
|
1372
|
+
if not self._factors:
|
|
1373
|
+
return result
|
|
1374
|
+
# 没开因子缓存 → plan 强制为 None,走下面「现算不落盘」那条路
|
|
1375
|
+
plan = self._factor_plan() if self.factor_cache else None
|
|
1376
|
+
for symbol in self.symbols:
|
|
1377
|
+
sub = self._symbol_slice(result, symbol)
|
|
1378
|
+
if sub.empty:
|
|
1379
|
+
continue
|
|
1380
|
+
|
|
1381
|
+
# 模拟标的没有缓存文件;没开缓存 / 配方认不出也一样 —— 一律现算不落盘
|
|
1382
|
+
if plan is None or symbol in self._sim_specs:
|
|
1383
|
+
before = set(sub.columns)
|
|
1384
|
+
sub = self._apply_factors(sub)
|
|
1385
|
+
self._attach_slice(
|
|
1386
|
+
result, symbol, sub, [c for c in sub.columns if c not in before]
|
|
1387
|
+
)
|
|
1388
|
+
continue
|
|
1389
|
+
|
|
1390
|
+
names = [
|
|
1391
|
+
n for n, (spec, _fp) in plan.items()
|
|
1392
|
+
if not (isinstance(spec, (list, tuple)) and _is_qka_factor(spec[0]))
|
|
1393
|
+
]
|
|
1394
|
+
for name in names:
|
|
1395
|
+
spec, fp = plan[name]
|
|
1396
|
+
cached = self._read_factor_cache(name, symbol, fp)
|
|
1397
|
+
target = self._recompute_target(sub, cached)
|
|
1398
|
+
if target is None:
|
|
1399
|
+
sub[name] = cached.reindex(sub.index)
|
|
1400
|
+
continue
|
|
1401
|
+
self._compute_factor_cols(target, only={name})
|
|
1402
|
+
if target is sub:
|
|
1403
|
+
series = sub[name]
|
|
1404
|
+
else:
|
|
1405
|
+
# 补算的尾巴头部自带滚动预热 NaN,用缓存里的老值兜住,
|
|
1406
|
+
# 不能拿 NaN 去覆盖本来好好的值。
|
|
1407
|
+
series = target[name].reindex(sub.index).combine_first(
|
|
1408
|
+
cached.reindex(sub.index)
|
|
1409
|
+
)
|
|
1410
|
+
sub[name] = series
|
|
1411
|
+
self._write_factor_cache(name, symbol, fp, series)
|
|
1412
|
+
self._attach_slice(result, symbol, sub, names)
|
|
1413
|
+
return result
|
|
1414
|
+
|
|
1415
|
+
def _qka_factors_with_cache(self, result):
|
|
1416
|
+
"""逐票把 qka 内置因子(α/β/夏普…)贴上;开了因子缓存才吃缓存。需在 benchmark 之后调用。"""
|
|
1417
|
+
if not self.factor_cache:
|
|
1418
|
+
return self._apply_qka_factors(result)
|
|
1419
|
+
plan = self._factor_plan()
|
|
1420
|
+
names = []
|
|
1421
|
+
if plan is not None:
|
|
1422
|
+
names = [
|
|
1423
|
+
n for n, (spec, _fp) in plan.items()
|
|
1424
|
+
if isinstance(spec, (list, tuple)) and _is_qka_factor(spec[0])
|
|
1425
|
+
]
|
|
1426
|
+
if not names:
|
|
1427
|
+
return self._apply_qka_factors(result)
|
|
1428
|
+
|
|
1429
|
+
for name in names:
|
|
1430
|
+
spec, fp = plan[name]
|
|
1431
|
+
# 按「先因子名、再标的」的顺序贴,保持与重构前一致
|
|
1432
|
+
for symbol in self.symbols:
|
|
1433
|
+
if f'{symbol}|returns' not in result.columns:
|
|
1434
|
+
continue
|
|
1435
|
+
col = f'{symbol}|{name}'
|
|
1436
|
+
if symbol in self._sim_specs:
|
|
1437
|
+
# 模拟标的没有缓存文件,直接算
|
|
1438
|
+
self._compute_qka_factor_cols(result, [(name, spec)], symbols=[symbol])
|
|
1439
|
+
continue
|
|
1440
|
+
cached = self._read_factor_cache(name, symbol, fp)
|
|
1441
|
+
target = self._recompute_target(result, cached)
|
|
1442
|
+
if target is None:
|
|
1443
|
+
result[col] = cached.reindex(result.index)
|
|
1444
|
+
continue
|
|
1445
|
+
self._compute_qka_factor_cols(target, [(name, spec)])
|
|
1446
|
+
if target is result:
|
|
1447
|
+
series = result[col]
|
|
1448
|
+
else:
|
|
1449
|
+
# 同上:补算尾巴头部的预热 NaN 用缓存老值兜住
|
|
1450
|
+
series = target[col].reindex(result.index).combine_first(
|
|
1451
|
+
cached.reindex(result.index)
|
|
1452
|
+
)
|
|
1453
|
+
result[col] = series
|
|
1454
|
+
self._write_factor_cache(name, symbol, fp, series)
|
|
1455
|
+
return result
|
|
1456
|
+
|
|
1054
1457
|
def _get_from_baostock(
|
|
1055
1458
|
self, symbol: str,
|
|
1056
1459
|
start_date: str = '1990-01-01',
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
2
|
QKA 内置指标模块
|
|
3
3
|
|
|
4
|
-
提供滚动窗口的内置指标函数,供 Data.
|
|
4
|
+
提供滚动窗口的内置指标函数,供 Data.factors 的 dispatch 使用。
|
|
5
5
|
|
|
6
6
|
与 analysis.py 的分工:
|
|
7
7
|
- analysis.py:接收原始 Series,返回单个标量(事后分析,如一次性 α/β/夏普)
|
|
8
8
|
- indicator.py:接收 DataFrame + symbol,返回滚动 Series(回测指标,每 bar 一个值)
|
|
9
9
|
|
|
10
|
-
所有函数签名统一为 fn(df, symbol, *args),由
|
|
10
|
+
所有函数签名统一为 fn(df, symbol, *args),由 _apply_qka_factors 调用。
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import numpy as np
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
## Data
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
数据获取和因子预计算。
|
|
4
4
|
|
|
5
5
|
### 构造
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
创建一个数据对象,配置数据源、股票代码和预计算因子。此时数据尚未下载,调用 `get()` 时才真正获取。
|
|
8
8
|
|
|
9
9
|
```python
|
|
10
10
|
from qka import Data
|
|
@@ -14,7 +14,7 @@ data = Data(
|
|
|
14
14
|
period='1d',
|
|
15
15
|
adjust='qfq',
|
|
16
16
|
benchmark=None,
|
|
17
|
-
|
|
17
|
+
factors=None,
|
|
18
18
|
)
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -22,22 +22,23 @@ data = Data(
|
|
|
22
22
|
|------|------|--------|------|
|
|
23
23
|
| `symbols` | `list[str \| Simulate]` | `None` | 标的列表。元素可以是 A 股代码(**`代码.市场`**,如 `'000001.SZ'`、`'600000.SH'`;也兼容 `sz.000001` 旧写法),也可以是 `Simulate` 对象(模拟标的),两者可混排,见下方「模拟标的」 |
|
|
24
24
|
| `period` | `str` | `'1d'` | 数据周期,当前仅支持 `'1d'` |
|
|
25
|
-
| `adjust` | `str` | `'qfq'` | 复权方式:`'qfq'` 前复权,`'hfq'` 后复权,`'bfq'`
|
|
25
|
+
| `adjust` | `str` | `'qfq'` | 复权方式:`'qfq'` 前复权,`'hfq'` 后复权,`'bfq'` 不复权。**它同时决定缓存策略**,见下方「缓存口径」 |
|
|
26
26
|
| `source` | `str` | `'tickflow'` | 数据源。`'tickflow'`(默认)行情覆盖全,**含 ETF / 可转债全历史**;`'baostock'` 提供估值等扩展字段(见 `extra_fields`);`'tdx'` 走本机「TQ 版」通达信客户端(仅 Windows,需客户端在运行),仅行情、无扩展字段,安装目录见下方「通达信数据源」 |
|
|
27
27
|
| `benchmark` | `str` | `None` | 基准指数代码,如 `'000300.SH'`。下载后在 `get()` 结果中追加 `benchmark|returns` 列 |
|
|
28
|
-
| `
|
|
28
|
+
| `factors` | `dict \| Callable` | `None` | 预计算指标/因子,见下方。**旧名 `indicators` 完全等价、保留兼容,两者只能传一个** |
|
|
29
29
|
| `extra_fields` | `list[str]` | `None` | 扩展字段(选股/估值用)。**仅 `source='baostock'` 提供**;`tickflow` / `tdx` 源无这些列,传入会被静默忽略,见下方 |
|
|
30
|
-
| `warmup` | `int` | `0` |
|
|
30
|
+
| `warmup` | `int` | `0` | 因子预热天数。回测/取数时自动多读 `warmup` 个交易日历史用于计算因子,使第 1 个交易日即拿到有效值(无需手写跳过前 N 根 bar)。仅用于计算,不增加回测 bar 数 |
|
|
31
|
+
| `factor_cache` | `bool` | `False` | 是否把算出来的因子**纵向落盘**(默认关)。开了才读写 `datadir/factors/…`,见下方「因子缓存」 |
|
|
31
32
|
|
|
32
|
-
> **warmup
|
|
33
|
-
> qka 会在 `[start_date, end_date]` 之前额外多读 `warmup`
|
|
34
|
-
> `start_date` 起——策略第 1 个 bar
|
|
33
|
+
> **warmup(因子预热)**:若因子需要较长历史窗口(如动量排名需看前 200 天),设 `warmup=200`。
|
|
34
|
+
> qka 会在 `[start_date, end_date]` 之前额外多读 `warmup` 天数据用于计算因子,但返回和回测仍严格从
|
|
35
|
+
> `start_date` 起——策略第 1 个 bar 因子即有值,无需 `continue` 跳过前 N 根 bar。
|
|
35
36
|
>
|
|
36
37
|
> ```python
|
|
37
38
|
> data = Data(
|
|
38
39
|
> symbols=['000001.SZ'],
|
|
39
|
-
>
|
|
40
|
-
> warmup=200, #
|
|
40
|
+
> factors={'mom200': lambda df: df['close'].pct_change(200)},
|
|
41
|
+
> warmup=200, # 自定义因子 qka 推断不出窗口,显式声明预热天数
|
|
41
42
|
> )
|
|
42
43
|
> ```
|
|
43
44
|
|
|
@@ -45,7 +46,44 @@ data = Data(
|
|
|
45
46
|
|
|
46
47
|
| 常驻列 | 公式 | 说明 |
|
|
47
48
|
|------|------|------|
|
|
48
|
-
| `returns` | `close.diff() / close.shift(1)` | 日收益率,始终存在,无需在
|
|
49
|
+
| `returns` | `close.diff() / close.shift(1)` | 日收益率,始终存在,无需在 factors 中声明 |
|
|
50
|
+
|
|
51
|
+
### 缓存口径
|
|
52
|
+
|
|
53
|
+
缓存按 `datadir/{source}/{period}/{adjust}/` 分目录存放,各复权方式互不干扰。**是否走增量,由复权方式决定**:
|
|
54
|
+
|
|
55
|
+
| 复权方式 | 缓存策略 | 原因 |
|
|
56
|
+
|------|------|------|
|
|
57
|
+
| `bfq` 不复权、`hfq` 后复权 | **增量**:只补缓存缺的那几天 | 历史是死的,不会被将来的分红送股改写 |
|
|
58
|
+
| `qfq` 前复权 | **不做增量**:需要新数据就整段重取、整段覆盖 | 前复权以「最新价」为锚,今后的分红送股会**往回改写整段历史** |
|
|
59
|
+
|
|
60
|
+
前复权为什么不能增量:只往后补新棒,文件里就成了「前段旧口径 + 后段新口径」的拼接,缝上会凭空多出一根假 K 线(实测某股一次分红后整段等比缩 5.57%,拼接处算出 −4.84%,而真实是 +0.91%),而且这个错一旦写进缓存就不会自愈。
|
|
61
|
+
|
|
62
|
+
代价:前复权下只要请求碰到缓存之外(最常见的是要看最新一根),就会重新下载整段历史;请求完全落在缓存内时,仍然直接读缓存。
|
|
63
|
+
|
|
64
|
+
### 因子缓存
|
|
65
|
+
|
|
66
|
+
**默认关闭**,要用得显式开:`Data(..., factor_cache=True)`。
|
|
67
|
+
|
|
68
|
+
开启后,算出来的因子会像行情一样**纵向落盘**,但目录**按因子名分**(不按数据源/周期/复权):
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
datadir/factors/{因子名}/{symbol}.parquet # 单列,日期索引
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
「因子名」= `factors` 字典里的 key(也就是策略里 `self.get('mom20')` 用的那个名字)。一个因子 = 一份独立的资产,可以单独查、单独发布、单独排名。
|
|
75
|
+
|
|
76
|
+
**什么时候用缓存、什么时候重算**:
|
|
77
|
+
|
|
78
|
+
| 情况 | 行为 |
|
|
79
|
+
|------|------|
|
|
80
|
+
| 缓存盖住了请求的日期范围,配方也没变 | **一个字都不算**,直接读 |
|
|
81
|
+
| 只缺后面的新数据 | **只补尾巴**(往前留出预热窗口,滚动指标才准),老值原样保留 |
|
|
82
|
+
| 前面也缺 / 缓存换了配方 / 换了数据源·周期·复权 | 该票该因子**整条重算**并覆盖 |
|
|
83
|
+
|
|
84
|
+
**配方指纹**:因子定义 + 数据源 + 周期 + 复权一起压成一个指纹写进文件。任一样变了指纹就对不上,自动重算——不会把两套口径的值混在一个文件里。可调用对象(lambda / 函数)取它的**源码文本**当指纹;取不到源码(REPL 里现写的、动态拼出来的)就**不缓存**,每次现算——宁可慢,也不冒"拿旧配方的值冒充新值"的风险。
|
|
85
|
+
|
|
86
|
+
**作用范围**:只作用于默认(`lazy=False`)路径;`lazy=True`(回测走的那条)行为不变。不传 `factor_cache` 时,一切跟没有这个功能一样:不读、不写、不建目录。
|
|
49
87
|
|
|
50
88
|
### 通达信数据源
|
|
51
89
|
|
|
@@ -112,15 +150,15 @@ r_t = drift + reversion * (logAnchor - logP_{t-1}) + vol * eps_t eps_t ~
|
|
|
112
150
|
与真实标的的异同:
|
|
113
151
|
|
|
114
152
|
- **日期轴**:取 `get(start_date, end_date)` 区间内的**真实交易日**(来自 baostock 交易日历,一日一根)。因此模拟数据同样需要能登录 baostock,且 `start_date=None` 时从 `Data.SIM_START`(`'2020-01-02'`)起算
|
|
115
|
-
-
|
|
116
|
-
- **`
|
|
153
|
+
- **因子预热**:与真实源一致——有 `factors` / `warmup` 时日期轴会自动向前扩展,返回结果仍严格限定在 `[start_date, end_date]`
|
|
154
|
+
- **`factors` / `extra_fields` / `warmup`**:全部照常生效,列名与真实源同样是 `{symbol}|{field}`,扩展列同样是「不请求就不生成」
|
|
117
155
|
- **`benchmark`**:与标的是真是模拟无关——`Backtest.run(benchmark=...)` 照常生效,基准始终走真实数据源单独加载
|
|
118
156
|
- **每次不同**:没有随机种子参数,每次调用都是新一批行情(不可复现)
|
|
119
157
|
- **不落盘**:模拟标的不写缓存;`symbols` 里若全是模拟标的,连 `datadir` 都不会创建
|
|
120
158
|
|
|
121
159
|
### extra_fields 扩展字段
|
|
122
160
|
|
|
123
|
-
`extra_fields` 追加扩展列(选股/估值用),列名同样遵循 `{symbol}|{field}` 约定(如 `000001.SZ|peTTM`),数值自动转为 `float64
|
|
161
|
+
`extra_fields` 追加扩展列(选股/估值用),列名同样遵循 `{symbol}|{field}` 约定(如 `000001.SZ|peTTM`),数值自动转为 `float64`。**不是因子,不参与 factors 预计算**,是随行情一起下载的原始字段。
|
|
124
162
|
|
|
125
163
|
> ⚠️ **仅 `source='baostock'` 提供**:这些扩展列来自 baostock 的 `query_history_k_data_plus` 接口。默认源 `'tickflow'` 与 `'tdx'` 的历史日线固定只有 OHLCV + 成交额,**没有**这些扩展列——此时传入的 `extra_fields` 会被静默忽略(不报错、也不生成对应列)。需要估值/换手率等字段时,显式用 `Data(..., source='baostock')`。
|
|
126
164
|
|
|
@@ -170,9 +208,11 @@ df['000001.SZ|peTTM'] # 市盈率(TTM),float64
|
|
|
170
208
|
- 基准 `benchmark` 同样受 `extra_fields` 影响(同一套下载逻辑),但通常不需要
|
|
171
209
|
- 数据源为 baostock 时 `extra_fields` 才生效;其他数据源忽略该参数
|
|
172
210
|
|
|
173
|
-
###
|
|
211
|
+
### factors
|
|
212
|
+
|
|
213
|
+
在构造 `Data` 时传入,利用数据加载过程一次性预计算所有因子。**不能**在构造后补充。
|
|
174
214
|
|
|
175
|
-
|
|
215
|
+
> `factors` 的旧名为 `indicators`,两者完全等价、保留兼容,同时传会报错;新代码请一律用 `factors`。
|
|
176
216
|
|
|
177
217
|
#### 内置指标
|
|
178
218
|
|
|
@@ -181,7 +221,7 @@ df['000001.SZ|peTTM'] # 市盈率(TTM),float64
|
|
|
181
221
|
```python
|
|
182
222
|
data = Data(
|
|
183
223
|
symbols=['000001.SZ'],
|
|
184
|
-
|
|
224
|
+
factors={
|
|
185
225
|
'sma_5': ('ta.trend.sma_indicator', 'close', 5),
|
|
186
226
|
'sma_20': ('ta.trend.sma_indicator', 'close', 20),
|
|
187
227
|
'ema_20': ('ta.trend.ema_indicator', 'close', 20),
|
|
@@ -291,7 +331,7 @@ data = Data(
|
|
|
291
331
|
非 ta 库的自定义因子用 callable:
|
|
292
332
|
|
|
293
333
|
```python
|
|
294
|
-
|
|
334
|
+
factors={
|
|
295
335
|
'ma5': lambda df: df['close'].rolling(5).mean(),
|
|
296
336
|
}
|
|
297
337
|
```
|
|
@@ -306,7 +346,7 @@ qka 框架内置的滚动窗口指标,格式为 `{'列名': ('qka.函数名',
|
|
|
306
346
|
data = Data(
|
|
307
347
|
symbols=['000001.SZ', '600000.SH'],
|
|
308
348
|
benchmark='000300.SH', # alpha/beta/information_ratio 必需
|
|
309
|
-
|
|
349
|
+
factors={
|
|
310
350
|
'sma_20': ('ta.trend.sma_indicator', 'close', 20),
|
|
311
351
|
'beta_60': ('qka.beta', 60),
|
|
312
352
|
'alpha_60': ('qka.alpha', 60),
|
|
@@ -363,7 +403,7 @@ df = data.get(start_date='2024-01-01', end_date='2024-12-31')
|
|
|
363
403
|
**返回的宽表示例:**
|
|
364
404
|
|
|
365
405
|
```python
|
|
366
|
-
data = Data(symbols=['000001.SZ', '600000.SH'],
|
|
406
|
+
data = Data(symbols=['000001.SZ', '600000.SH'], factors={
|
|
367
407
|
'sma_5': ('ta.trend.sma_indicator', 'close', 5),
|
|
368
408
|
})
|
|
369
409
|
df = data.get(start_date='2024-01-02', end_date='2024-01-05')
|
|
@@ -380,7 +420,7 @@ df = data.get(start_date='2024-01-02', end_date='2024-01-05')
|
|
|
380
420
|
```
|
|
381
421
|
|
|
382
422
|
- 每只股票独占一组列,列前缀 = symbol
|
|
383
|
-
- `returns` 列自动存在,无需在
|
|
423
|
+
- `returns` 列自动存在,无需在 factors 中声明
|
|
384
424
|
- 前 4 行 SMA 为 NaN(窗口=5,不足)
|
|
385
425
|
- 若设了 `benchmark='000300.SH'`,末尾多一列 `benchmark|returns`
|
|
386
426
|
|
|
@@ -43,7 +43,7 @@ class MyStrategy(Strategy):
|
|
|
43
43
|
```python
|
|
44
44
|
close = self.get('close') # pd.Series,index=股票代码,value=当前价格
|
|
45
45
|
high = self.get('high')
|
|
46
|
-
sma5 = self.get('sma_5') #
|
|
46
|
+
sma5 = self.get('sma_5') # factors 中定义的因子列
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
| 属性 | 说明 |
|
|
@@ -84,7 +84,7 @@ qka 自动在 `[start_date, end_date]` 之前多读 `warmup` 天数据用于计
|
|
|
84
84
|
首个交易日开始(`on_bar` 调用次数不变),指标自始有效:
|
|
85
85
|
|
|
86
86
|
```python
|
|
87
|
-
data = Data(symbols=[...],
|
|
87
|
+
data = Data(symbols=[...], factors={...}, warmup=200) # 构造时声明预热天数
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
> 注意:qka 只能从 ta 指标的整数参数自动推断预热窗口;自定义 lambda 指标推断不到,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|