qka 2.2.4.dev2__tar.gz → 2.2.5.dev3__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.2.4.dev2 → qka-2.2.5.dev3}/PKG-INFO +11 -8
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/README.md +10 -7
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/__init__.py +2 -1
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/core/__init__.py +2 -1
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/core/data.py +237 -22
- qka-2.2.5.dev3/qka/core/simulate.py +81 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/skills/qka/SKILL.md +3 -1
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/skills/qka/references/data.md +48 -1
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/.github/workflows/release.yml +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/.gitignore +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/.vscode/settings.json +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/CHANGELOG.md +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/LICENSE +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/pyproject.toml +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/cli.py +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/core/accessor.py +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/core/analysis.py +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/core/backtest.py +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/core/broker.py +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/core/indicator.py +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/core/report.py +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/core/sizing.py +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/core/strategy.py +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/utils/__init__.py +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/qka/utils/logger.py +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/skills/qka/references/analysis.md +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/skills/qka/references/backtest.md +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/skills/qka/references/broker.md +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/skills/qka/references/sizing.md +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/skills/qka/references/strategy.md +0 -0
- {qka-2.2.4.dev2 → qka-2.2.5.dev3}/tools/generate_api_ref.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: qka
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.5.dev3
|
|
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
|
|
@@ -135,13 +135,16 @@ print(bt.metrics['sharpe_ratio']) # 夏普比率
|
|
|
135
135
|
|
|
136
136
|
## 核心能力
|
|
137
137
|
|
|
138
|
-
-
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
138
|
+
- **数据获取** — 写个股票代码就能取到本地,自动缓存、自动补齐最新数据,重复使用秒读
|
|
139
|
+
- **技术指标** — 均线、MACD、RSI、ATR 等 ta 库全部指标,外加 α/β、夏普、最大回撤等内置指标,声明一下即可,策略里不用自己 rolling
|
|
140
|
+
- **估值字段** — 换手率、涨跌幅、停牌与 ST 标记、市盈率、市净率等,需要哪些就取哪些
|
|
141
|
+
- **模拟行情** — 一条 `Simulate` 就能造出单边上涨、单边下跌、来回震荡的走势,不必等真实行情,用来验证策略是不是真在按逻辑交易
|
|
142
|
+
- **多标的回测** — 一次传一篮子股票,`self.get()` 取当日横截面,`self.history()` 取历史窗口
|
|
143
|
+
- **仓位管理** — 按资金比例、按金额、按股数、按 ATR 风险四种算法
|
|
144
|
+
- **交易成本** — 佣金、印花税、滑点均已内置,也可自行调整
|
|
145
|
+
- **绩效指标** — 总收益、年化、夏普、最大回撤、Calmar、胜率、盈亏比等 13 项,跑完即有
|
|
146
|
+
- **回测报告** — 净值曲线、回撤曲线、月度收益热力图、逐笔交易明细,一键生成 HTML,浏览器直接打开
|
|
147
|
+
- **基准对比** — 传入基准指数即可对比收益、计算 β / α
|
|
145
148
|
|
|
146
149
|
## 文档
|
|
147
150
|
|
|
@@ -85,13 +85,16 @@ print(bt.metrics['sharpe_ratio']) # 夏普比率
|
|
|
85
85
|
|
|
86
86
|
## 核心能力
|
|
87
87
|
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
88
|
+
- **数据获取** — 写个股票代码就能取到本地,自动缓存、自动补齐最新数据,重复使用秒读
|
|
89
|
+
- **技术指标** — 均线、MACD、RSI、ATR 等 ta 库全部指标,外加 α/β、夏普、最大回撤等内置指标,声明一下即可,策略里不用自己 rolling
|
|
90
|
+
- **估值字段** — 换手率、涨跌幅、停牌与 ST 标记、市盈率、市净率等,需要哪些就取哪些
|
|
91
|
+
- **模拟行情** — 一条 `Simulate` 就能造出单边上涨、单边下跌、来回震荡的走势,不必等真实行情,用来验证策略是不是真在按逻辑交易
|
|
92
|
+
- **多标的回测** — 一次传一篮子股票,`self.get()` 取当日横截面,`self.history()` 取历史窗口
|
|
93
|
+
- **仓位管理** — 按资金比例、按金额、按股数、按 ATR 风险四种算法
|
|
94
|
+
- **交易成本** — 佣金、印花税、滑点均已内置,也可自行调整
|
|
95
|
+
- **绩效指标** — 总收益、年化、夏普、最大回撤、Calmar、胜率、盈亏比等 13 项,跑完即有
|
|
96
|
+
- **回测报告** — 净值曲线、回撤曲线、月度收益热力图、逐笔交易明细,一键生成 HTML,浏览器直接打开
|
|
97
|
+
- **基准对比** — 传入基准指数即可对比收益、计算 β / α
|
|
95
98
|
|
|
96
99
|
## 文档
|
|
97
100
|
|
|
@@ -12,6 +12,7 @@ except PackageNotFoundError:
|
|
|
12
12
|
__version__ = "0.1.0" # fallback version
|
|
13
13
|
|
|
14
14
|
# 核心功能直接导入
|
|
15
|
+
from qka.core.simulate import Simulate
|
|
15
16
|
from qka.core.data import Data
|
|
16
17
|
from qka.core.accessor import DataAccessor
|
|
17
18
|
from qka.core.backtest import Backtest
|
|
@@ -25,7 +26,7 @@ from qka import core, utils
|
|
|
25
26
|
|
|
26
27
|
__all__ = [
|
|
27
28
|
# 核心功能
|
|
28
|
-
'Data', 'Backtest', 'Strategy', 'Broker', 'DataAccessor', 'SizingAccessor',
|
|
29
|
+
'Data', 'Simulate', 'Backtest', 'Strategy', 'Broker', 'DataAccessor', 'SizingAccessor',
|
|
29
30
|
# 分析
|
|
30
31
|
'Analysis', 'Segment', 'AlphaBeta',
|
|
31
32
|
# 子模块
|
|
@@ -5,6 +5,7 @@ QKA Core 模块
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
# 数据相关
|
|
8
|
+
from .simulate import Simulate
|
|
8
9
|
from .data import Data
|
|
9
10
|
from .accessor import DataAccessor
|
|
10
11
|
# 回测相关
|
|
@@ -16,7 +17,7 @@ from .sizing import SizingAccessor
|
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
# 数据相关
|
|
19
|
-
'Data', 'DataAccessor',
|
|
20
|
+
'Data', 'Simulate', 'DataAccessor',
|
|
20
21
|
# 回测相关
|
|
21
22
|
'Backtest', 'Strategy', 'Broker',
|
|
22
23
|
# 仓位管理
|
|
@@ -13,9 +13,10 @@ import pyarrow as pa
|
|
|
13
13
|
import pyarrow.parquet as pq
|
|
14
14
|
import baostock as bs
|
|
15
15
|
import dask.dataframe as dd
|
|
16
|
-
from typing import List,
|
|
16
|
+
from typing import List, Optional, Callable, Union
|
|
17
17
|
from qka.utils.logger import logger
|
|
18
18
|
from qka.core import indicator
|
|
19
|
+
from qka.core.simulate import Simulate
|
|
19
20
|
|
|
20
21
|
# qka 内置指标裸名集合,用于 dispatch 识别(无需 qka. 前缀)
|
|
21
22
|
_QKA_BUILTIN_NAMES = frozenset({
|
|
@@ -63,9 +64,26 @@ class Data():
|
|
|
63
64
|
"pcfNcfTTM", # 市现率(TTM)
|
|
64
65
|
]
|
|
65
66
|
|
|
67
|
+
# ── 模拟数据源默认值 ──
|
|
68
|
+
SIM_START = '2020-01-02' # 模拟行情默认起点(调用方未给 start_date 时使用)
|
|
69
|
+
SIM_START_PRICE = 20.0 # 起始价
|
|
70
|
+
SIM_LIMIT = 0.10 # 单日涨跌幅截断(模拟涨跌停)
|
|
71
|
+
SIM_FUND_PHI = 0.97 # 基本面扰动的持续性(半衰期 ≈ 23 个交易日)
|
|
72
|
+
# 估值列参数:field -> (基准值, 对价格的对数弹性, 基本面自身对数波动(稳态 std))
|
|
73
|
+
# 弹性 = 估值随价格等比变动的比例。弹性≈1 ⇒ 估值是价格的复刻(错);
|
|
74
|
+
# 弹性小甚至为负 ⇒ 盈利同步增长抵消了价格,估值走出自己的路(对)。
|
|
75
|
+
# 目标量级参照真实缓存实测:corr(peTTM,close)≈-0.09 / pbMRQ≈+0.78 /
|
|
76
|
+
# psTTM≈-0.59 / pcfNcfTTM≈-0.59。
|
|
77
|
+
SIM_EXTRA_BASE = {
|
|
78
|
+
'peTTM': (15.0, +0.05, 0.130),
|
|
79
|
+
'pbMRQ': (1.8, +0.40, 0.070),
|
|
80
|
+
'psTTM': (2.2, -0.30, 0.120),
|
|
81
|
+
'pcfNcfTTM': (12.0, -0.80, 0.300),
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
def __init__(
|
|
67
85
|
self,
|
|
68
|
-
symbols: Optional[List[str]] = None,
|
|
86
|
+
symbols: Optional[List[Union[str, Simulate]]] = None,
|
|
69
87
|
benchmark: Optional[str] = None,
|
|
70
88
|
period: str = '1d',
|
|
71
89
|
adjust: str = 'qfq',
|
|
@@ -80,7 +98,22 @@ class Data():
|
|
|
80
98
|
初始化数据对象
|
|
81
99
|
|
|
82
100
|
Args:
|
|
83
|
-
symbols:
|
|
101
|
+
symbols: 标的列表,元素**可以是真实代码字符串,也可以是 Simulate 对象**。
|
|
102
|
+
真实代码用 baostock 格式,如 ['sz.000001', 'sh.600000'];Simulate 对象
|
|
103
|
+
表示一只由 qka 现场生成的模拟标的,两者可以混排。写法完全同构,区别只在
|
|
104
|
+
元素本身:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
Data(symbols=[
|
|
108
|
+
'sh.600900',
|
|
109
|
+
Simulate('sim.a', drift=0.0, reversion=0.08, vol=0.018),
|
|
110
|
+
])
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
模拟标的同样支持 `indicators` / `extra_fields` / `warmup`,行为与真实源
|
|
114
|
+
一致(列名同样是 {symbol}|{field},扩展列同样是「不请求就不生成」)。
|
|
115
|
+
模拟标的的行情区间由 get(start_date, end_date) 决定 —— 用 baostock 的
|
|
116
|
+
交易日历把区间内的真实交易日取出来,一 日一根,与真实源同构。
|
|
84
117
|
benchmark: 基准代码,如 'sh.000300'。基准数据以 benchmark| 前缀加入最终 DataFrame,
|
|
85
118
|
仅供辅助计算(β/α 等),不参与指标计算
|
|
86
119
|
period: 数据周期,如 '1d'(日线)、'1m'(分钟)
|
|
@@ -117,8 +150,27 @@ class Data():
|
|
|
117
150
|
indicators=lambda df: df.assign(ma5=df['close'].rolling(5).mean())
|
|
118
151
|
```
|
|
119
152
|
函数接收单只股票的 DataFrame,返回添加了额外列的 DataFrame。
|
|
153
|
+
|
|
120
154
|
"""
|
|
121
|
-
|
|
155
|
+
# ── 归一化 symbols:str = 真实标的,Simulate 对象 = 模拟标的,可混排 ──
|
|
156
|
+
names, sim_specs = [], {}
|
|
157
|
+
for item in (symbols or []):
|
|
158
|
+
if isinstance(item, Simulate):
|
|
159
|
+
name = item.symbol
|
|
160
|
+
sim_specs[name] = item
|
|
161
|
+
elif isinstance(item, str):
|
|
162
|
+
name = item
|
|
163
|
+
else:
|
|
164
|
+
raise TypeError(
|
|
165
|
+
f"symbols 的元素必须是 str 或 Simulate,got {type(item)}: {item!r}"
|
|
166
|
+
)
|
|
167
|
+
if name in names:
|
|
168
|
+
raise ValueError(f"symbols 中存在重复标的: {name}")
|
|
169
|
+
names.append(name)
|
|
170
|
+
|
|
171
|
+
self.symbols = names # 全部标的(保持入参顺序)
|
|
172
|
+
self._sim_specs = sim_specs # {代码: Simulate}
|
|
173
|
+
self._real_symbols = [n for n in names if n not in sim_specs]
|
|
122
174
|
self.benchmark = benchmark
|
|
123
175
|
self.period = period
|
|
124
176
|
self.adjust = adjust
|
|
@@ -158,10 +210,145 @@ class Data():
|
|
|
158
210
|
else:
|
|
159
211
|
self.datadir = Path(datadir)
|
|
160
212
|
|
|
161
|
-
|
|
213
|
+
# 只有真实标的才需要缓存目录;纯模拟标的(_real_symbols 为空)不落盘
|
|
214
|
+
if self._real_symbols:
|
|
215
|
+
self.datadir.mkdir(parents=True, exist_ok=True)
|
|
162
216
|
|
|
163
217
|
self.target_dir = self.datadir / self.source / self.period / (self.adjust or "bfq")
|
|
164
|
-
self.
|
|
218
|
+
if self._real_symbols:
|
|
219
|
+
self.target_dir.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
|
|
221
|
+
def _trading_index(self, start_date: str, end_date: str) -> pd.DatetimeIndex:
|
|
222
|
+
"""
|
|
223
|
+
取 [start_date, end_date] 内的真实交易日,作为模拟行情的日期轴。
|
|
224
|
+
|
|
225
|
+
数据来自 baostock 的交易日历接口 query_trade_dates,故调用方需已登录。
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
start_date / end_date: 'YYYY-MM-DD'
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
pd.DatetimeIndex,名称 'date'(与真实缓存 parquet 的索引名一致)
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
RuntimeError: baostock 查询失败,或区间内一个交易日都没有
|
|
235
|
+
"""
|
|
236
|
+
rs = bs.query_trade_dates(start_date=start_date, end_date=end_date)
|
|
237
|
+
if rs.error_code != '0':
|
|
238
|
+
raise RuntimeError(
|
|
239
|
+
f"获取交易日历失败({start_date} ~ {end_date}): "
|
|
240
|
+
f"[{rs.error_code}] {rs.error_msg}"
|
|
241
|
+
)
|
|
242
|
+
dates = []
|
|
243
|
+
while rs.next():
|
|
244
|
+
calendar_date, is_trading_day = rs.get_row_data()
|
|
245
|
+
if is_trading_day == '1':
|
|
246
|
+
dates.append(calendar_date)
|
|
247
|
+
if not dates:
|
|
248
|
+
raise RuntimeError(
|
|
249
|
+
f"区间内没有交易日: {start_date} ~ {end_date},请检查日期范围"
|
|
250
|
+
)
|
|
251
|
+
idx = pd.DatetimeIndex(pd.to_datetime(dates), name='date')
|
|
252
|
+
return idx
|
|
253
|
+
|
|
254
|
+
def _generate_symbol(self, symbol: str, idx: pd.DatetimeIndex) -> pd.DataFrame:
|
|
255
|
+
"""
|
|
256
|
+
生成一只模拟标的的行情数据(纯内存,不走缓存、不下载)。
|
|
257
|
+
|
|
258
|
+
对数价格按「带趋势的均值回复」演化:
|
|
259
|
+
r_t = drift + reversion * (logAnchor - logP_{t-1}) + vol * eps_t
|
|
260
|
+
eps_t ~ N(0, 1)。drift=0 且 reversion=0 时退化为纯随机游走。
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
symbol: Simulate 标的的代码
|
|
264
|
+
idx: 日期轴(真实交易日,取自 _trading_index)
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
pd.DataFrame,索引即 idx,含 6 个基础列,
|
|
268
|
+
并按 self.extra_fields 追加扩展列 —— 与真实数据源行为一致(不给就不生成)。
|
|
269
|
+
"""
|
|
270
|
+
spec = self._sim_specs[symbol]
|
|
271
|
+
drift, reversion, vol = spec.drift, spec.reversion, spec.vol
|
|
272
|
+
|
|
273
|
+
bars = len(idx)
|
|
274
|
+
rng = np.random.default_rng() # 每次新建 ⇒ 每次调用都是新行情
|
|
275
|
+
|
|
276
|
+
# 1. 对数价格路径(锚点固定在起点价上,保证「震荡」真是围着起点来回)
|
|
277
|
+
log_p = np.empty(bars)
|
|
278
|
+
log_p[0] = np.log(self.SIM_START_PRICE)
|
|
279
|
+
anchor = log_p[0]
|
|
280
|
+
eps = rng.standard_normal(bars)
|
|
281
|
+
for t in range(1, bars):
|
|
282
|
+
log_p[t] = log_p[t - 1] + drift + reversion * (anchor - log_p[t - 1]) + vol * eps[t]
|
|
283
|
+
close = np.exp(log_p)
|
|
284
|
+
|
|
285
|
+
# 2. 涨跌停截断(相对前收),避免造出真实市场不会有的单日暴涨
|
|
286
|
+
step_ret = np.clip(np.diff(close) / close[:-1], -self.SIM_LIMIT, self.SIM_LIMIT)
|
|
287
|
+
close = np.concatenate([[close[0]], close[0] * np.cumprod(1.0 + step_ret)])
|
|
288
|
+
|
|
289
|
+
# 3. OHLC:保证 high >= max(open, close)、low <= min(open, close)
|
|
290
|
+
preclose = np.concatenate([[close[0]], close[:-1]])
|
|
291
|
+
open_ = preclose * (1.0 + rng.normal(0.0, 0.002, bars))
|
|
292
|
+
high = np.maximum(open_, close) * (1.0 + np.abs(rng.normal(0.0, 0.003, bars)))
|
|
293
|
+
low = np.minimum(open_, close) * (1.0 - np.abs(rng.normal(0.0, 0.003, bars)))
|
|
294
|
+
|
|
295
|
+
volume = rng.integers(1_000_000, 5_000_000, bars).astype(float)
|
|
296
|
+
df = pd.DataFrame({
|
|
297
|
+
'open': open_, 'high': high, 'low': low, 'close': close,
|
|
298
|
+
'volume': volume, 'amount': volume * close,
|
|
299
|
+
}, index=idx)
|
|
300
|
+
|
|
301
|
+
# 4. 扩展列:只生成请求的字段(与真实源「不给就没有」一致)
|
|
302
|
+
log_ratio = np.log(close / close[0])
|
|
303
|
+
for field in self.extra_fields:
|
|
304
|
+
if field == 'preclose':
|
|
305
|
+
df[field] = preclose
|
|
306
|
+
elif field == 'pctChg':
|
|
307
|
+
df[field] = (close / preclose - 1.0) * 100.0
|
|
308
|
+
elif field == 'tradestatus':
|
|
309
|
+
df[field] = 1
|
|
310
|
+
elif field == 'isST':
|
|
311
|
+
df[field] = 0
|
|
312
|
+
elif field == 'turn':
|
|
313
|
+
df[field] = np.abs(rng.normal(2.0, 0.6, bars))
|
|
314
|
+
elif field in self.SIM_EXTRA_BASE:
|
|
315
|
+
base, elasticity, idio_std = self.SIM_EXTRA_BASE[field]
|
|
316
|
+
# 估值 = 基准值 × 价格弹性项 × 基本面自身波动项
|
|
317
|
+
df[field] = base * np.exp(
|
|
318
|
+
elasticity * log_ratio
|
|
319
|
+
+ self._sim_fundamental_walk(rng, bars, idio_std)
|
|
320
|
+
)
|
|
321
|
+
return df
|
|
322
|
+
|
|
323
|
+
def _sim_fundamental_walk(self, rng, bars: int, idio_std: float) -> np.ndarray:
|
|
324
|
+
"""
|
|
325
|
+
生成一条「基本面」对数扰动序列(OU 均值回复过程)。
|
|
326
|
+
|
|
327
|
+
为什么需要它:真实世界里 PE = 市值 / 净利润,净利润是**独立于价格**演化
|
|
328
|
+
的慢变量,所以真实估值与股价并不同步(实测长江电力 corr(peTTM, close)
|
|
329
|
+
≈ -0.09)。若估值列直接写成价格的等比缩放,该相关系数会逼近 +1,
|
|
330
|
+
「低 PE 买入」这类策略在模拟数据上会被等价测成「价格策略」,语义失真。
|
|
331
|
+
|
|
332
|
+
模型:z_t = phi * z_{t-1} + sigma * eps_t
|
|
333
|
+
取 sigma = idio_std * sqrt(1 - phi^2) ⇒ 稳态标准差恰为 idio_std,
|
|
334
|
+
故入参语义就是「估值对数偏离的量级」,可直接与真实数据对照。
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
rng: numpy Generator
|
|
338
|
+
bars: 生成长度
|
|
339
|
+
idio_std: 基本面自身的稳态对数波动
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
np.ndarray,形状 (bars,),z[0] = 0(对齐基准值起点)
|
|
343
|
+
"""
|
|
344
|
+
phi = self.SIM_FUND_PHI
|
|
345
|
+
sigma = idio_std * np.sqrt(1.0 - phi * phi)
|
|
346
|
+
z = np.empty(bars)
|
|
347
|
+
z[0] = 0.0
|
|
348
|
+
eps = rng.standard_normal(bars)
|
|
349
|
+
for t in range(1, bars):
|
|
350
|
+
z[t] = phi * z[t - 1] + sigma * eps[t]
|
|
351
|
+
return z
|
|
165
352
|
|
|
166
353
|
def _cache_missing_extra_fields(self, path: Path) -> bool:
|
|
167
354
|
"""检查已有 parquet 缓存是否缺少 extra_fields 指定的列。"""
|
|
@@ -318,13 +505,15 @@ class Data():
|
|
|
318
505
|
"""
|
|
319
506
|
获取历史数据。
|
|
320
507
|
|
|
321
|
-
|
|
508
|
+
并发下载所有股票数据,应用因子计算,并返回合并后的数据。真实标的读本地缓存
|
|
509
|
+
(必要时下载),模拟标的(Simulate)按同一日期轴现场生成 —— 两者写法完全同构。
|
|
322
510
|
|
|
323
511
|
Args:
|
|
324
512
|
lazy: 是否以懒加载模式返回 dask DataFrame(支持大规模数据分区迭代)。
|
|
325
513
|
默认 False,返回 compute() 后的 pandas DataFrame(向后兼容)。
|
|
326
514
|
start_date: 起始日期,格式 YYYY-MM-DD。用于从缓存中截取数据范围,
|
|
327
|
-
避免全量加载。传 None
|
|
515
|
+
避免全量加载。传 None 表示从最早可用数据开始;模拟标的在
|
|
516
|
+
start_date=None 时从 SIM_START 起算。
|
|
328
517
|
end_date: 截止日期,格式 YYYY-MM-DD。传 None 表示到最新可用数据。
|
|
329
518
|
|
|
330
519
|
Returns:
|
|
@@ -334,6 +523,8 @@ class Data():
|
|
|
334
523
|
|
|
335
524
|
注意:有指标时,start_date 会自动向后扩展 max_window 个交易日读取缓存,
|
|
336
525
|
确保指标有足够的预热数据。最终返回的 DataFrame 仍严格限定在 [start_date, end_date]。
|
|
526
|
+
|
|
527
|
+
模拟标的的日期轴取自 baostock 交易日历(区间内一日一根),因此需要能登录 baostock。
|
|
337
528
|
"""
|
|
338
529
|
if not self.symbols:
|
|
339
530
|
return pd.DataFrame()
|
|
@@ -364,8 +555,8 @@ class Data():
|
|
|
364
555
|
download_start = read_start.strftime("%Y-%m-%d") if read_start is not None else None
|
|
365
556
|
download_end = end_date
|
|
366
557
|
|
|
367
|
-
#
|
|
368
|
-
all_symbols = list(self.
|
|
558
|
+
# 需要下载的真实标的(含基准);模拟标的全在内存生成,永不下载
|
|
559
|
+
all_symbols = list(self._real_symbols)
|
|
369
560
|
if self.benchmark and self.benchmark not in all_symbols:
|
|
370
561
|
all_symbols.append(self.benchmark)
|
|
371
562
|
need_download = [
|
|
@@ -373,16 +564,28 @@ class Data():
|
|
|
373
564
|
if self._needs_download(s, download_start, download_end)
|
|
374
565
|
]
|
|
375
566
|
|
|
376
|
-
#
|
|
567
|
+
# 模拟标的的日期轴:区间内的真实交易日(随指标预热一起向前扩展)
|
|
568
|
+
need_calendar = bool(self._sim_specs)
|
|
569
|
+
sim_start = sim_end = None
|
|
570
|
+
if need_calendar:
|
|
571
|
+
sim_start = (read_start.strftime("%Y-%m-%d")
|
|
572
|
+
if read_start is not None else self.SIM_START)
|
|
573
|
+
sim_end = (pd.Timestamp(end_date) if end_date is not None
|
|
574
|
+
else pd.Timestamp.now().floor('D')).strftime("%Y-%m-%d")
|
|
575
|
+
|
|
576
|
+
# 需要下载、或需要模拟交易日历时,登录 baostock
|
|
377
577
|
bs_logged_in = False
|
|
378
|
-
if need_download and self.source == 'baostock':
|
|
578
|
+
if (need_download and self.source == 'baostock') or need_calendar:
|
|
379
579
|
lg = bs.login()
|
|
380
580
|
if lg.error_code != '0':
|
|
381
581
|
raise RuntimeError(f"baostock 登录失败: {lg.error_msg}")
|
|
382
582
|
bs_logged_in = True
|
|
383
583
|
|
|
384
584
|
errors = []
|
|
585
|
+
sim_index = None
|
|
385
586
|
try:
|
|
587
|
+
if need_calendar:
|
|
588
|
+
sim_index = self._trading_index(sim_start, sim_end)
|
|
386
589
|
if need_download:
|
|
387
590
|
if self.source == 'baostock':
|
|
388
591
|
for symbol in tqdm(need_download, desc="下载数据"):
|
|
@@ -422,11 +625,18 @@ class Data():
|
|
|
422
625
|
# 懒加载模式:返回 dask DataFrame,列名 {symbol}|{factor}
|
|
423
626
|
dfs = []
|
|
424
627
|
for symbol in self.symbols:
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
628
|
+
if symbol in self._sim_specs:
|
|
629
|
+
# clear_divisions:真实缓存读出的 divisions 恒为未知((None, None)),
|
|
630
|
+
# 模拟帧若带已知 divisions,混合 concat 时 dask 会在 min/max 上炸
|
|
631
|
+
ddf = dd.from_pandas(
|
|
632
|
+
self._generate_symbol(symbol, sim_index), npartitions=1,
|
|
633
|
+
).clear_divisions()
|
|
634
|
+
else:
|
|
635
|
+
parquet_path = self.target_dir / f"{symbol}.parquet"
|
|
636
|
+
if not parquet_path.exists():
|
|
637
|
+
logger.warning(f"数据文件不存在,跳过: {parquet_path}")
|
|
638
|
+
continue
|
|
639
|
+
ddf = dd.read_parquet(str(parquet_path), filters=pq_filters)
|
|
430
640
|
ddf['returns'] = ddf['close'].diff() / ddf['close'].shift(1)
|
|
431
641
|
ddf = self._apply_indicators(ddf)
|
|
432
642
|
column_mapping = {col: f'{symbol}|{col}' for col in ddf.columns}
|
|
@@ -457,11 +667,16 @@ class Data():
|
|
|
457
667
|
# 全量模式(默认)
|
|
458
668
|
dfs = []
|
|
459
669
|
for symbol in self.symbols:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
670
|
+
if symbol in self._sim_specs:
|
|
671
|
+
df = dd.from_pandas(
|
|
672
|
+
self._generate_symbol(symbol, sim_index), npartitions=1,
|
|
673
|
+
).clear_divisions()
|
|
674
|
+
else:
|
|
675
|
+
parquet_path = self.target_dir / f"{symbol}.parquet"
|
|
676
|
+
if not parquet_path.exists():
|
|
677
|
+
logger.warning(f"数据文件不存在,跳过: {parquet_path}")
|
|
678
|
+
continue
|
|
679
|
+
df = dd.read_parquet(str(parquet_path), filters=pq_filters)
|
|
465
680
|
df['returns'] = df['close'].diff() / df['close'].shift(1)
|
|
466
681
|
df = self._apply_indicators(df)
|
|
467
682
|
column_mapping = {col: f'{symbol}|{col}' for col in df.columns}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QKA 模拟标的数据模块
|
|
3
|
+
|
|
4
|
+
提供 Simulate 类:描述一只由 qka 现场生成的行情序列(不下载、不读缓存)。
|
|
5
|
+
把它放进 Data(symbols=[...]) 即可,写法与真实标的完全同构。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from qka.utils.logger import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Simulate:
|
|
12
|
+
"""
|
|
13
|
+
模拟标的:一只由 qka 现场造出来的行情。
|
|
14
|
+
|
|
15
|
+
用法与真实标的写法完全一致,区别只在 symbols 里放的是它而不是代码字符串:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from qka import Data, Simulate
|
|
19
|
+
|
|
20
|
+
data = Data(symbols=[
|
|
21
|
+
'sh.600900', # 真实标的,照常下载
|
|
22
|
+
Simulate('sim.a', drift=0.0, reversion=0.08, vol=0.018),
|
|
23
|
+
])
|
|
24
|
+
df = data.get(start_date='2023-01-01', end_date='2025-12-31')
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
行情按「带趋势的均值回复」在对数价格上演化:
|
|
28
|
+
|
|
29
|
+
r_t = drift + reversion * (logAnchor - logP_{t-1}) + vol * eps_t
|
|
30
|
+
|
|
31
|
+
eps_t ~ N(0, 1)。三个参数各管一件事:
|
|
32
|
+
|
|
33
|
+
- drift (μ):日漂移。正=上涨、负=下跌、0=无方向
|
|
34
|
+
- reversion (θ):均值回复强度。越大越黏在中枢附近(半衰期 = ln2 / θ);0 = 不回复
|
|
35
|
+
- vol (σ):日波动率。如 0.015 约等于每日上下 1.5%
|
|
36
|
+
|
|
37
|
+
drift=0 且 reversion=0 时退化为纯随机游走。
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
symbol (str): 标的代码,同时用作 DataFrame 的列前缀(如 sim.a|close)
|
|
41
|
+
drift (float): 日漂移 μ
|
|
42
|
+
reversion (float): 均值回复强度 θ
|
|
43
|
+
vol (float): 日波动率 σ
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
symbol: str,
|
|
49
|
+
drift: float = 0.0,
|
|
50
|
+
reversion: float = 0.0,
|
|
51
|
+
vol: float = 0.012,
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
初始化模拟标的。
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
symbol: 标的代码,自定义即可(如 'sim.a')。不能为空,且不能与同一
|
|
58
|
+
Data 里的其他标的重复
|
|
59
|
+
drift: 日漂移 μ,正=上涨、负=下跌、0=无方向,默认 0
|
|
60
|
+
reversion: 均值回复强度 θ,必须 >= 0(越大越黏在中枢附近),默认 0
|
|
61
|
+
vol: 日波动率 σ,必须 > 0(如 0.015 约等于每日上下 1.5%),默认 0.012
|
|
62
|
+
"""
|
|
63
|
+
if not isinstance(symbol, str) or not symbol.strip():
|
|
64
|
+
raise ValueError(f"symbol 必须是非空字符串,got {symbol!r}")
|
|
65
|
+
self.symbol = symbol.strip()
|
|
66
|
+
self.drift = float(drift)
|
|
67
|
+
self.reversion = float(reversion)
|
|
68
|
+
self.vol = float(vol)
|
|
69
|
+
|
|
70
|
+
if self.reversion < 0:
|
|
71
|
+
raise ValueError(f"reversion 必须 >= 0,got {self.reversion}")
|
|
72
|
+
if self.vol <= 0:
|
|
73
|
+
raise ValueError(f"vol 必须 > 0,got {self.vol}")
|
|
74
|
+
|
|
75
|
+
logger.debug(f"Simulate 标的已创建: {self}")
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return (
|
|
79
|
+
f"Simulate(symbol={self.symbol!r}, drift={self.drift}, "
|
|
80
|
+
f"reversion={self.reversion}, vol={self.vol})"
|
|
81
|
+
)
|
|
@@ -5,11 +5,12 @@ description: 使用 QKA(快量化)框架生成 A 股量化回测策略、选
|
|
|
5
5
|
|
|
6
6
|
# QKA 框架
|
|
7
7
|
|
|
8
|
-
简洁易用的 A
|
|
8
|
+
简洁易用的 A 股量化回测框架,共七个公开类:
|
|
9
9
|
|
|
10
10
|
| 类 | 全限定名 | 作用 |
|
|
11
11
|
|-----|------|------|
|
|
12
12
|
| Data | `qka.Data` | 行情数据加载 + 指标预计算 |
|
|
13
|
+
| Simulate | `qka.Simulate` | 模拟标的 — 描述一只由 qka 现场生成的行情 |
|
|
13
14
|
| Strategy | `qka.Strategy` | 策略基类 — 实现 `on_bar` 做交易决策 |
|
|
14
15
|
| Broker | `qka.Broker` | 虚拟券商 — 执行买卖,管理资金和持仓 |
|
|
15
16
|
| SizingAccessor | `qka.SizingAccessor` | 仓位计算 — 四种仓位方法 |
|
|
@@ -23,6 +24,7 @@ description: 使用 QKA(快量化)框架生成 A 股量化回测策略、选
|
|
|
23
24
|
| 模块 | 文件 | 内容 |
|
|
24
25
|
|------|------|------|
|
|
25
26
|
| Data | `references/data.md` | 数据加载、指标预计算(ta 库全部指标 + qka 内置指标)、`get()` |
|
|
27
|
+
| Simulate | `references/data.md` | 模拟标的:`Simulate(symbol, drift, reversion, vol)`,放进 `Data(symbols=[...])` |
|
|
26
28
|
| Strategy | `references/strategy.md` | 策略基类、`on_bar`、`self.get()`、`self.history()` |
|
|
27
29
|
| Backtest | `references/backtest.md` | `run()`、`bt.metrics`、`bt.results`、`bt.trade_history` |
|
|
28
30
|
| Broker | `references/broker.md` | `buy()`、`sell()` |
|
|
@@ -20,7 +20,7 @@ data = Data(
|
|
|
20
20
|
|
|
21
21
|
| 参数 | 类型 | 默认值 | 说明 |
|
|
22
22
|
|------|------|--------|------|
|
|
23
|
-
| `symbols` | `list[str]` | `None` | A
|
|
23
|
+
| `symbols` | `list[str \| Simulate]` | `None` | 标的列表。元素可以是 A 股代码(baostock 格式 `sz.000001`、`sh.600000`),也可以是 `Simulate` 对象(模拟标的),两者可混排,见下方「模拟标的」 |
|
|
24
24
|
| `period` | `str` | `'1d'` | 数据周期,当前仅支持 `'1d'` |
|
|
25
25
|
| `adjust` | `str` | `'qfq'` | 复权方式:`'qfq'` 前复权,`'hfq'` 后复权,`'bfq'` 不复权 |
|
|
26
26
|
| `benchmark` | `str` | `None` | 基准指数代码,如 `'sh.000300'`。下载后在 `get()` 结果中追加 `benchmark|returns` 列 |
|
|
@@ -46,6 +46,53 @@ data = Data(
|
|
|
46
46
|
|------|------|------|
|
|
47
47
|
| `returns` | `close.diff() / close.shift(1)` | 日收益率,始终存在,无需在 indicators 中声明 |
|
|
48
48
|
|
|
49
|
+
### 模拟标的
|
|
50
|
+
|
|
51
|
+
`symbols` 的元素除了真实代码字符串,还可以是 `Simulate` 对象——一只由 qka **现场生成**的行情,不下载、不读缓存。写法与真实标的完全同构:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from qka import Data, Simulate
|
|
55
|
+
|
|
56
|
+
data = Data(symbols=[
|
|
57
|
+
'sh.600900', # 真实标的,照常下载
|
|
58
|
+
Simulate('sim.a', drift=0.0, reversion=0.08, vol=0.018), # 模拟标的
|
|
59
|
+
])
|
|
60
|
+
df = data.get(start_date='2023-01-01', end_date='2025-12-31')
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`Simulate(symbol, drift=0.0, reversion=0.0, vol=0.012)` 的构造参数:
|
|
64
|
+
|
|
65
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
66
|
+
|------|------|--------|------|
|
|
67
|
+
| `symbol` | `str` | 必填 | 标的代码,自定义即可(如 `'sim.a'`)。同时用作列前缀 `sim.a|close`;不能为空,也不能与同一 `Data` 内其他标的重复 |
|
|
68
|
+
| `drift` | `float` | `0.0` | 日漂移 μ。正=上涨、负=下跌、0=无方向 |
|
|
69
|
+
| `reversion` | `float` | `0.0` | 均值回复强度 θ,必须 `>= 0`。越大越黏在中枢附近(半衰期 = `ln2/θ`);`0` = 不回复 |
|
|
70
|
+
| `vol` | `float` | `0.012` | 日波动率 σ,必须 `> 0`。如 `0.015` 约等于每日上下 1.5% |
|
|
71
|
+
|
|
72
|
+
行情按「带趋势的均值回复」在对数价格上演化:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
r_t = drift + reversion * (logAnchor - logP_{t-1}) + vol * eps_t eps_t ~ N(0, 1)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`drift=0` 且 `reversion=0` 时退化为纯随机游走。常见组合:
|
|
79
|
+
|
|
80
|
+
| 想要的行情 | 写法 |
|
|
81
|
+
|------|------|
|
|
82
|
+
| 单边上涨 | `Simulate('sim.up', drift=0.002)` |
|
|
83
|
+
| 单边下跌 | `Simulate('sim.dn', drift=-0.003)` |
|
|
84
|
+
| 来回震荡 | `Simulate('sim.side', reversion=0.08, vol=0.018)` |
|
|
85
|
+
| 纯随机 | `Simulate('sim.rand')` |
|
|
86
|
+
|
|
87
|
+
与真实标的的异同:
|
|
88
|
+
|
|
89
|
+
- **日期轴**:取 `get(start_date, end_date)` 区间内的**真实交易日**(来自 baostock 交易日历,一日一根)。因此模拟数据同样需要能登录 baostock,且 `start_date=None` 时从 `Data.SIM_START`(`'2020-01-02'`)起算
|
|
90
|
+
- **指标预热**:与真实源一致——有 `indicators` / `warmup` 时日期轴会自动向前扩展,返回结果仍严格限定在 `[start_date, end_date]`
|
|
91
|
+
- **`indicators` / `extra_fields` / `warmup`**:全部照常生效,列名与真实源同样是 `{symbol}|{field}`,扩展列同样是「不请求就不生成」
|
|
92
|
+
- **`benchmark`**:与标的是真是模拟无关——`Backtest.run(benchmark=...)` 照常生效,基准始终走真实数据源单独加载
|
|
93
|
+
- **每次不同**:没有随机种子参数,每次调用都是新一批行情(不可复现)
|
|
94
|
+
- **不落盘**:模拟标的不写缓存;`symbols` 里若全是模拟标的,连 `datadir` 都不会创建
|
|
95
|
+
|
|
49
96
|
### extra_fields 扩展字段
|
|
50
97
|
|
|
51
98
|
`extra_fields` 追加 baostock `query_history_k_data_plus` 接口支持的扩展列(选股/估值用),列名同样遵循 `{symbol}|{field}` 约定(如 `sz.000001|peTTM`),数值自动转为 `float64`。**不是指标,不参与 indicators 预计算**,是随行情一起下载的原始字段。
|
|
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
|