qka 2.2.5.dev3__tar.gz → 2.2.6.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.
Files changed (31) hide show
  1. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/PKG-INFO +7 -5
  2. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/README.md +5 -4
  3. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/pyproject.toml +1 -0
  4. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/core/analysis.py +3 -3
  5. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/core/backtest.py +1 -1
  6. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/core/data.py +191 -29
  7. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/core/simulate.py +1 -1
  8. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/skills/qka/references/analysis.md +7 -7
  9. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/skills/qka/references/backtest.md +2 -2
  10. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/skills/qka/references/broker.md +2 -2
  11. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/skills/qka/references/data.md +23 -20
  12. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/skills/qka/references/sizing.md +2 -2
  13. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/skills/qka/references/strategy.md +3 -3
  14. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/.github/workflows/release.yml +0 -0
  15. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/.gitignore +0 -0
  16. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/.vscode/settings.json +0 -0
  17. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/CHANGELOG.md +0 -0
  18. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/LICENSE +0 -0
  19. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/__init__.py +0 -0
  20. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/cli.py +0 -0
  21. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/core/__init__.py +0 -0
  22. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/core/accessor.py +0 -0
  23. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/core/broker.py +0 -0
  24. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/core/indicator.py +0 -0
  25. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/core/report.py +0 -0
  26. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/core/sizing.py +0 -0
  27. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/core/strategy.py +0 -0
  28. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/utils/__init__.py +0 -0
  29. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/qka/utils/logger.py +0 -0
  30. {qka-2.2.5.dev3 → qka-2.2.6.dev2}/skills/qka/SKILL.md +0 -0
  31. {qka-2.2.5.dev3 → qka-2.2.6.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.2.5.dev3
3
+ Version: 2.2.6.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
@@ -40,6 +40,7 @@ Requires-Dist: plotly>=6.1.1
40
40
  Requires-Dist: pyarrow>=21.0.0
41
41
  Requires-Dist: statsmodels>=0.14.6
42
42
  Requires-Dist: ta>=0.11.0
43
+ Requires-Dist: tickflow>=0.1.25
43
44
  Requires-Dist: tqdm>=4.67.1
44
45
  Provides-Extra: dev
45
46
  Requires-Dist: hatch-vcs>=0.3.0; extra == 'dev'
@@ -95,7 +96,7 @@ npx skills add zsrl/qka
95
96
  from qka import Data
96
97
 
97
98
  data = Data(
98
- symbols=['sz.000001', 'sh.600000'],
99
+ symbols=['000001.SZ', '600000.SH'],
99
100
  indicators={
100
101
  'sma_5': ('ta.trend.sma_indicator', 'close', 5),
101
102
  'rsi_14': ('ta.momentum.rsi', 'close', 14),
@@ -128,7 +129,7 @@ from qka import Backtest
128
129
 
129
130
  strategy = MyStrategy()
130
131
  bt = Backtest(data, strategy)
131
- bt.run(cash=200000, start_date='2024-01-01', benchmark='sh.000300')
132
+ bt.run(cash=200000, start_date='2024-01-01', benchmark='000300.SH')
132
133
  print(bt.metrics['total_return_pct']) # 总收益率
133
134
  print(bt.metrics['sharpe_ratio']) # 夏普比率
134
135
  ```
@@ -156,7 +157,7 @@ print(bt.metrics['sharpe_ratio']) # 夏普比率
156
157
  from qka import Data, Strategy, Backtest
157
158
 
158
159
  data = Data(
159
- symbols=['sz.000001'],
160
+ symbols=['000001.SZ'],
160
161
  indicators={
161
162
  'sma_5': ('ta.trend.sma_indicator', 'close', 5),
162
163
  'sma_20': ('ta.trend.sma_indicator', 'close', 20),
@@ -197,7 +198,8 @@ print(bt.metrics['total_return_pct'])
197
198
 
198
199
  ## 致谢
199
200
 
200
- - [baostock](http://baostock.com) — 免费 A 股数据
201
+ - [tickflow](https://tickflow.org) — 免费 A 股 / ETF / 可转债行情(默认数据源)
202
+ - [baostock](http://baostock.com) — 免费 A 股数据(估值等扩展字段)
201
203
  - [ta](https://github.com/bukosabino/ta) — 技术指标库
202
204
 
203
205
  ---
@@ -45,7 +45,7 @@ npx skills add zsrl/qka
45
45
  from qka import Data
46
46
 
47
47
  data = Data(
48
- symbols=['sz.000001', 'sh.600000'],
48
+ symbols=['000001.SZ', '600000.SH'],
49
49
  indicators={
50
50
  'sma_5': ('ta.trend.sma_indicator', 'close', 5),
51
51
  'rsi_14': ('ta.momentum.rsi', 'close', 14),
@@ -78,7 +78,7 @@ from qka import Backtest
78
78
 
79
79
  strategy = MyStrategy()
80
80
  bt = Backtest(data, strategy)
81
- bt.run(cash=200000, start_date='2024-01-01', benchmark='sh.000300')
81
+ bt.run(cash=200000, start_date='2024-01-01', benchmark='000300.SH')
82
82
  print(bt.metrics['total_return_pct']) # 总收益率
83
83
  print(bt.metrics['sharpe_ratio']) # 夏普比率
84
84
  ```
@@ -106,7 +106,7 @@ print(bt.metrics['sharpe_ratio']) # 夏普比率
106
106
  from qka import Data, Strategy, Backtest
107
107
 
108
108
  data = Data(
109
- symbols=['sz.000001'],
109
+ symbols=['000001.SZ'],
110
110
  indicators={
111
111
  'sma_5': ('ta.trend.sma_indicator', 'close', 5),
112
112
  'sma_20': ('ta.trend.sma_indicator', 'close', 20),
@@ -147,7 +147,8 @@ print(bt.metrics['total_return_pct'])
147
147
 
148
148
  ## 致谢
149
149
 
150
- - [baostock](http://baostock.com) — 免费 A 股数据
150
+ - [tickflow](https://tickflow.org) — 免费 A 股 / ETF / 可转债行情(默认数据源)
151
+ - [baostock](http://baostock.com) — 免费 A 股数据(估值等扩展字段)
151
152
  - [ta](https://github.com/bukosabino/ta) — 技术指标库
152
153
 
153
154
  ---
@@ -24,6 +24,7 @@ dependencies = [
24
24
  "dask[dataframe]>=2025.7.0",
25
25
  "ta>=0.11.0",
26
26
  "baostock>=0.9.1",
27
+ "tickflow>=0.1.25",
27
28
  "statsmodels>=0.14.6",
28
29
  ]
29
30
 
@@ -31,11 +31,11 @@ class Analysis:
31
31
 
32
32
  Example:
33
33
  >>> from qka import Data, Analysis
34
- >>> data = Data(symbols=['sh.000300'])
34
+ >>> data = Data(symbols=['000300.SH'])
35
35
  >>> df = data.get()
36
36
  >>> analysis = Analysis()
37
- >>> segs = analysis.zigzag(df['sh.000300|close'], threshold=0.3, min_days=90)
38
- >>> ab = analysis.alpha_beta(df['sh.000300|returns'].dropna(),
37
+ >>> segs = analysis.zigzag(df['000300.SH|close'], threshold=0.3, min_days=90)
38
+ >>> ab = analysis.alpha_beta(df['000300.SH|returns'].dropna(),
39
39
  ... df['benchmark|returns'].dropna())
40
40
  """
41
41
 
@@ -188,7 +188,7 @@ class Backtest:
188
188
  """
189
189
  try:
190
190
  from qka.core.data import Data
191
- bm_data = Data(symbols=[benchmark_code], source='baostock')
191
+ bm_data = Data(symbols=[benchmark_code])
192
192
  bm_df = bm_data.get(lazy=False)
193
193
  if bm_df is not None and not bm_df.empty:
194
194
  # 提取 close 列(可能是多股票 MultiIndex,取第一只)
@@ -38,17 +38,22 @@ class Data():
38
38
  通过 `indicators` 参数统一处理技术指标和自定义因子,在数据加载时一次性预计算。
39
39
 
40
40
  Attributes:
41
- symbols (List[str]): 股票代码列表,baostock 格式如 sz.000001、sh.600000
41
+ symbols (List[str]): 股票代码列表,格式「代码.市场」,如 ['600519.SH', '000001.SZ']
42
+ (也兼容 sh.600519 旧写法,两种写法等价)
42
43
  period (str): 数据周期,如 '1d'、'1m' 等
43
44
  adjust (str): 复权方式,如 'qfq'、'hfq'、'bfq'
44
45
  indicators (dict | Callable): 预计算指标/因子
45
- source (str): 数据源,默认 'baostock'
46
+ source (str): 数据源,默认 'tickflow'(可选 'baostock')
46
47
  pool_size (int): 并发下载线程数
47
48
  datadir (Path): 数据缓存目录
48
49
  target_dir (Path): 目标存储目录
49
- extra_fields (List[str]): baostock 扩展字段(选股/估值用),如 ['peTTM', 'pbMRQ', 'turn']
50
+ extra_fields (List[str]): 扩展字段(选股/估值用),如 ['peTTM', 'pbMRQ', 'turn']。
51
+ 仅 'baostock' 源提供(可选值见 BAOSTOCK_EXTRA_FIELDS);'tickflow' 源无扩展列,会忽略
50
52
  """
51
53
 
54
+ # 支持的行情源
55
+ SOURCES = ('tickflow', 'baostock')
56
+
52
57
  # baostock query_history_k_data_plus 完整支持的基础字段(除 date 索引外)
53
58
  BAOSTOCK_BASE_FIELDS = ["open", "high", "low", "close", "volume", "amount"]
54
59
  # 可通过 extra_fields 追加的扩展字段白名单(行情/估值/选股类)
@@ -63,6 +68,9 @@ class Data():
63
68
  "psTTM", # 市销率(TTM)
64
69
  "pcfNcfTTM", # 市现率(TTM)
65
70
  ]
71
+ # TickFlow 可提供的扩展字段:其历史日线固定返回 OHLCV + amount,
72
+ # 无估值/换手率等扩展列(季报级 financials 不是每日序列,不接入)。
73
+ TICKFLOW_EXTRA_FIELDS = []
66
74
 
67
75
  # ── 模拟数据源默认值 ──
68
76
  SIM_START = '2020-01-02' # 模拟行情默认起点(调用方未给 start_date 时使用)
@@ -87,7 +95,7 @@ class Data():
87
95
  benchmark: Optional[str] = None,
88
96
  period: str = '1d',
89
97
  adjust: str = 'qfq',
90
- source: str = 'baostock',
98
+ source: str = 'tickflow',
91
99
  pool_size: int = 10,
92
100
  datadir: Optional[Path] = None,
93
101
  indicators: Optional[dict] = None,
@@ -99,13 +107,14 @@ class Data():
99
107
 
100
108
  Args:
101
109
  symbols: 标的列表,元素**可以是真实代码字符串,也可以是 Simulate 对象**。
102
- 真实代码用 baostock 格式,如 ['sz.000001', 'sh.600000'];Simulate 对象
110
+ 真实代码用「代码.市场」格式,如 ['600519.SH', '000001.SZ'](同样兼容
111
+ sh.600519 旧写法);Simulate 对象
103
112
  表示一只由 qka 现场生成的模拟标的,两者可以混排。写法完全同构,区别只在
104
113
  元素本身:
105
114
 
106
115
  ```python
107
116
  Data(symbols=[
108
- 'sh.600900',
117
+ '600900.SH',
109
118
  Simulate('sim.a', drift=0.0, reversion=0.08, vol=0.018),
110
119
  ])
111
120
  ```
@@ -114,18 +123,20 @@ class Data():
114
123
  一致(列名同样是 {symbol}|{field},扩展列同样是「不请求就不生成」)。
115
124
  模拟标的的行情区间由 get(start_date, end_date) 决定 —— 用 baostock 的
116
125
  交易日历把区间内的真实交易日取出来,一 日一根,与真实源同构。
117
- benchmark: 基准代码,如 'sh.000300'。基准数据以 benchmark| 前缀加入最终 DataFrame,
126
+ benchmark: 基准代码,如 '000300.SH'。基准数据以 benchmark| 前缀加入最终 DataFrame,
118
127
  仅供辅助计算(β/α 等),不参与指标计算
119
128
  period: 数据周期,如 '1d'(日线)、'1m'(分钟)
120
129
  adjust: 复权方式,'qfq'(前复权)、'hfq'(后复权)、'bfq'(不复权)
121
- source: 数据来源,默认 'baostock'
130
+ source: 数据来源,默认 'tickflow'(可选 'baostock')。两者返回结构一致,
131
+ 切换源不影响上层调用;差异见各源的扩展字段能力
122
132
  pool_size: 并发下载线程数
123
133
  datadir: 缓存目录路径
124
134
  indicators: 预计算指标/因子,支持三种格式:
125
- extra_fields: baostock 扩展字段列表(选股/估值用,如 ['peTTM', 'pbMRQ', 'turn']),
126
- 可选值见 BAOSTOCK_EXTRA_FIELDS。追加的列同样遵循 {symbol}|{field} 命名,
127
- 如 'sh.600000|peTTM'。注意:首次下载后缓存字段固定,变更 extra_fields
128
- 会自动检测列缺失并重新下载对应股票。
135
+ extra_fields: 扩展字段列表(选股/估值用,如 ['peTTM', 'pbMRQ', 'turn']),
136
+ 可选值见 BAOSTOCK_EXTRA_FIELDS —— 仅 'baostock' 源提供;
137
+ 'tickflow' 源无扩展列,传入的扩展字段会被静默忽略(不报错、不生成该列)。
138
+ 追加的列同样遵循 {symbol}|{field} 命名,如 '600519.SH|peTTM'。注意:
139
+ 首次下载后缓存字段固定,变更 extra_fields 会自动检测列缺失并重新下载对应股票。
129
140
 
130
141
  warmup: 指标预热天数(默认 0)。回测/取数时自动多读取 warmup 个交易日的历史数据
131
142
  用于计算指标,使第 1 个交易日即可拿到有效指标值,无需在策略里手写
@@ -174,18 +185,31 @@ class Data():
174
185
  self.benchmark = benchmark
175
186
  self.period = period
176
187
  self.adjust = adjust
188
+ if source not in self.SOURCES:
189
+ raise ValueError(f"不支持的数据源: {source!r}。可选: {self.SOURCES}")
177
190
  self.source = source
178
191
  self.pool_size = pool_size
179
192
  self.warmup = warmup
180
193
 
181
- # extra_fields 白名单校验 + 去重
182
- self.extra_fields = []
194
+ # 当前数据源实际支持的扩展字段(源能力不同,按源过滤)
195
+ whitelist = (self.BAOSTOCK_EXTRA_FIELDS if source == 'baostock'
196
+ else self.TICKFLOW_EXTRA_FIELDS)
197
+
198
+ # extra_fields 校验 + 去重
199
+ # 未知字段(拼写错误)报错;当前源提供不了的字段静默忽略
200
+ # (如 tickflow 无估值列,调用方仍传 peTTM 时不报错、只是不生成该列)
201
+ self.extra_fields = [] # 当前源实际会下载的扩展字段
202
+ self._requested_extra_fields = [] # 调用方原始请求(不按源过滤,模拟标的按此生成)
183
203
  for f in (extra_fields or []):
184
204
  if f not in self.BAOSTOCK_EXTRA_FIELDS:
185
205
  raise ValueError(
186
206
  f"extra_fields 含不支持的字段: {f}。"
187
207
  f"可选: {self.BAOSTOCK_EXTRA_FIELDS}"
188
208
  )
209
+ if f not in self._requested_extra_fields:
210
+ self._requested_extra_fields.append(f)
211
+ if f not in whitelist:
212
+ continue
189
213
  if f not in self.extra_fields:
190
214
  self.extra_fields.append(f)
191
215
 
@@ -299,8 +323,10 @@ class Data():
299
323
  }, index=idx)
300
324
 
301
325
  # 4. 扩展列:只生成请求的字段(与真实源「不给就没有」一致)
326
+ # 用「调用方原始请求」而非 self.extra_fields —— 模拟数据由 qka 现场生成,
327
+ # 不依赖任何外部源,故不受数据源的扩展字段能力限制
302
328
  log_ratio = np.log(close / close[0])
303
- for field in self.extra_fields:
329
+ for field in self._requested_extra_fields:
304
330
  if field == 'preclose':
305
331
  df[field] = preclose
306
332
  elif field == 'pctChg':
@@ -374,7 +400,9 @@ class Data():
374
400
  existing = set(pq.read_schema(path).names)
375
401
  except Exception:
376
402
  return merged
377
- for f in self.BAOSTOCK_EXTRA_FIELDS:
403
+ whitelist = (self.BAOSTOCK_EXTRA_FIELDS if self.source == 'baostock'
404
+ else self.TICKFLOW_EXTRA_FIELDS)
405
+ for f in whitelist:
378
406
  if f in existing and f not in merged:
379
407
  merged.append(f)
380
408
  return merged
@@ -411,22 +439,19 @@ class Data():
411
439
 
412
440
  # ── 首次下载:只拉请求范围(缓存缺失 extra_fields 列时也全量重下)──
413
441
  if not path.exists() or self._cache_missing_extra_fields(path):
414
- df = self._get_from_baostock(
442
+ df = self._fetch(
415
443
  symbol,
416
444
  start_date=download_start or default_start,
417
445
  end_date=download_end or default_end,
418
446
  extra_fields=merged_extra,
419
447
  )
420
448
  if len(df) == 0:
421
- raise RuntimeError(f"{symbol}: baostock 返回空数据")
449
+ raise RuntimeError(f"{symbol}: 数据源({self.source})返回空数据")
422
450
  table = pa.Table.from_pandas(df)
423
451
  pq.write_table(table, path)
424
452
  return path
425
453
 
426
454
  # ── 增量更新:检查缓存覆盖,补缺失范围 ──
427
- if self.source != 'baostock':
428
- return path
429
-
430
455
  existing = pd.read_parquet(path)
431
456
  if not isinstance(existing.index, pd.DatetimeIndex):
432
457
  return path
@@ -440,7 +465,7 @@ class Data():
440
465
  req_start = pd.Timestamp(download_start) if download_start else None
441
466
  if req_start is not None and req_start < cache_min:
442
467
  end_before = (cache_min - pd.Timedelta(days=1)).strftime("%Y-%m-%d")
443
- df_before = self._get_from_baostock(
468
+ df_before = self._fetch(
444
469
  symbol, start_date=download_start, end_date=end_before,
445
470
  extra_fields=merged_extra,
446
471
  )
@@ -449,10 +474,14 @@ class Data():
449
474
  changed = True
450
475
 
451
476
  # 往后补
452
- req_end = pd.Timestamp(download_end) if download_end else pd.Timestamp.now()
477
+ # 口径与 _needs_download 保持一致:按「日」比较。若用带时刻的 now(),
478
+ # 缓存末日恰为今天时 now() > cache_max 恒成立,会去补 [明天, 今天] 这一段,
479
+ # 起始日期大于终止日期 ⇒ baostock 直接报错。
480
+ req_end = (pd.Timestamp(download_end) if download_end
481
+ else pd.Timestamp.now().floor('D'))
453
482
  if req_end > cache_max:
454
483
  start_after = (cache_max + pd.Timedelta(days=1)).strftime("%Y-%m-%d")
455
- df_after = self._get_from_baostock(
484
+ df_after = self._fetch(
456
485
  symbol,
457
486
  start_date=start_after,
458
487
  end_date=download_end or default_end,
@@ -483,8 +512,6 @@ class Data():
483
512
  path = self.target_dir / f"{symbol}.parquet"
484
513
  if not path.exists() or self._cache_missing_extra_fields(path):
485
514
  return True
486
- if self.source != 'baostock':
487
- return False
488
515
  existing = pd.read_parquet(path)
489
516
  if not isinstance(existing.index, pd.DatetimeIndex):
490
517
  return False
@@ -920,7 +947,7 @@ class Data():
920
947
  从 baostock 获取单个股票的数据。
921
948
 
922
949
  Args:
923
- symbol: baostock 格式股票代码,如 sz.000001、sh.600000
950
+ symbol: 标的代码,任意写法(600519.SH 或 sh.600519),内部统一转换
924
951
  start_date: 起始日期,格式 YYYY-MM-DD,默认 1990-01-01
925
952
  end_date: 截止日期,格式 YYYY-MM-DD,默认 2050-12-31
926
953
  extra_fields: 本次请求的扩展字段列表。None 时使用 self.extra_fields;
@@ -938,8 +965,11 @@ class Data():
938
965
  extra = list(extra_fields) if extra_fields is not None else self.extra_fields
939
966
  fields = ",".join(["date"] + self.BAOSTOCK_BASE_FIELDS + extra)
940
967
 
968
+ # baostock 只认 sh.600519 写法 ⇒ 无论调用方用哪种写法都统一转换
969
+ bs_symbol = self.to_baostock_symbol(symbol)
970
+
941
971
  rs = bs.query_history_k_data_plus(
942
- symbol,
972
+ bs_symbol,
943
973
  fields,
944
974
  start_date=start_date,
945
975
  end_date=end_date,
@@ -947,7 +977,7 @@ class Data():
947
977
  adjustflag=adjustflag,
948
978
  )
949
979
  if rs.error_code != '0':
950
- raise RuntimeError(f"baostock 查询 {symbol}({bs_code}) 失败: {rs.error_msg}")
980
+ raise RuntimeError(f"baostock 查询 {symbol}({bs_symbol}) 失败: {rs.error_msg}")
951
981
  # 官网标准写法:get_row_data() 逐行取 + next() 翻页,避免 get_data() 的 df.append()
952
982
  data_list = []
953
983
  while (rs.error_code == '0') & rs.next():
@@ -966,3 +996,135 @@ class Data():
966
996
  df["date"] = pd.to_datetime(df["date"])
967
997
  df = df.set_index("date")
968
998
  return df
999
+
1000
+ # ── 取数分发 ────────────────────────────────────────────────
1001
+
1002
+ def _fetch(
1003
+ self, symbol: str,
1004
+ start_date: str = '1990-01-01',
1005
+ end_date: str = '2050-12-31',
1006
+ extra_fields: Optional[List[str]] = None,
1007
+ ) -> pd.DataFrame:
1008
+ """按当前数据源分发到对应实现,返回结构一致(date 索引 + OHLCV/amount)。"""
1009
+ if self.source == 'baostock':
1010
+ return self._get_from_baostock(symbol, start_date, end_date, extra_fields)
1011
+ return self._get_from_tickflow(symbol, start_date, end_date, extra_fields)
1012
+
1013
+ # ── TickFlow 数据源 ────────────────────────────────────────
1014
+
1015
+ _tf_client = None # 进程级复用,避免重复初始化
1016
+
1017
+ @classmethod
1018
+ def _tickflow_client(cls):
1019
+ """惰性创建并复用 TickFlow 客户端(当前走免费服务,无需 api_key)。"""
1020
+ if cls._tf_client is None:
1021
+ from tickflow import TickFlow
1022
+ cls._tf_client = TickFlow.free()
1023
+ return cls._tf_client
1024
+
1025
+ @staticmethod
1026
+ def _split_symbol(symbol: str):
1027
+ """
1028
+ 拆出 (小写市场, 代码)。两种写法等价、都支持:
1029
+
1030
+ 600519.SH (推荐,数字在前)
1031
+ sh.600519 (旧写法,兼容保留)
1032
+
1033
+ Returns:
1034
+ (market, code),如 ('sh', '600519')
1035
+ """
1036
+ if '.' not in symbol:
1037
+ raise ValueError(
1038
+ f"标的代码缺少市场标识: {symbol!r}(应形如 600519.SH 或 sh.600519)"
1039
+ )
1040
+ head, tail = symbol.split('.', 1)
1041
+ # 市场标识是两位字母(sh/sz/bj/hk/us),据此判断它在前还是在后
1042
+ if len(head) == 2 and head.isalpha():
1043
+ market, code = head, tail # sh.600519
1044
+ else:
1045
+ code, market = head, tail # 600519.SH
1046
+ return market.lower(), code
1047
+
1048
+ @classmethod
1049
+ def to_baostock_symbol(cls, symbol: str) -> str:
1050
+ """任意写法 → baostock 写法,如 sh.600519"""
1051
+ market, code = cls._split_symbol(symbol)
1052
+ return f"{market}.{code}"
1053
+
1054
+ @classmethod
1055
+ def to_tickflow_symbol(cls, symbol: str) -> str:
1056
+ """任意写法 → tickflow 写法,如 600519.SH"""
1057
+ market, code = cls._split_symbol(symbol)
1058
+ return f"{code}.{market.upper()}"
1059
+
1060
+ def _get_from_tickflow(
1061
+ self, symbol: str,
1062
+ start_date: str = '1990-01-01',
1063
+ end_date: str = '2050-12-31',
1064
+ extra_fields: Optional[List[str]] = None,
1065
+ ) -> pd.DataFrame:
1066
+ """
1067
+ 从 TickFlow 获取单只标的的日线数据。
1068
+
1069
+ 返回结构与 _get_from_baostock 一致(date 索引 + open/high/low/close/
1070
+ volume/amount),但有两处口径必须转换:
1071
+ - **volume**:TickFlow 单位为「手」,×100 换算成 baostock 口径的「股」
1072
+ - **timestamp**:TickFlow 给的是「北京时间当天零点」对应的 UTC 毫秒,
1073
+ 需 +8 小时还原为北京日期,否则日期整体偏一天
1074
+
1075
+ extra_fields 对 TickFlow 无意义(其历史日线不含扩展列),此处不处理。
1076
+ """
1077
+ # 周期映射(当前仅日/周/月线;分钟线 TickFlow 属付费能力,未接)
1078
+ period_map = {'1d': '1d', '1w': '1w', '1M': '1M'}
1079
+ if self.period not in period_map:
1080
+ raise RuntimeError(
1081
+ f"tickflow 源暂不支持周期 {self.period!r}(可选: {list(period_map)})"
1082
+ )
1083
+ period = period_map[self.period]
1084
+
1085
+ # 复权映射:tickflow forward=前复权 / backward=后复权 / none=不复权
1086
+ adjust_map = {'bfq': 'none', 'qfq': 'forward', 'hfq': 'backward'}
1087
+ adjust = adjust_map.get(self.adjust, 'forward')
1088
+
1089
+ tf_symbol = self.to_tickflow_symbol(symbol)
1090
+
1091
+ # 日期 → 毫秒时间戳(右端 +1 天,保证 end_date 当天落在闭区间内)
1092
+ start_ms = int(pd.Timestamp(start_date).timestamp() * 1000)
1093
+ end_ms = int((pd.Timestamp(end_date) + pd.Timedelta(days=1)).timestamp() * 1000)
1094
+
1095
+ client = self._tickflow_client()
1096
+ try:
1097
+ data = client.klines.get(
1098
+ tf_symbol,
1099
+ period=period,
1100
+ # count 必须显式给足:TickFlow 默认只返回最近 100 条,
1101
+ # 不给会静默截断长区间(全历史会只剩最近 100 个交易日)
1102
+ count=10000,
1103
+ start_time=start_ms,
1104
+ end_time=end_ms,
1105
+ adjust=adjust,
1106
+ )
1107
+ except Exception as e:
1108
+ raise RuntimeError(f"tickflow 查询 {symbol}({tf_symbol}) 失败: {e}")
1109
+
1110
+ ts = (data or {}).get('timestamp')
1111
+ if not ts:
1112
+ return pd.DataFrame()
1113
+
1114
+ # UTC 毫秒 → 北京日期(当天零点)
1115
+ idx = (pd.to_datetime(pd.Series(ts), unit='ms')
1116
+ + pd.Timedelta(hours=8)).dt.normalize()
1117
+ idx.name = 'date'
1118
+
1119
+ df = pd.DataFrame({
1120
+ 'open': data['open'],
1121
+ 'high': data['high'],
1122
+ 'low': data['low'],
1123
+ 'close': data['close'],
1124
+ 'volume': [v * 100 for v in data['volume']], # 手 → 股
1125
+ 'amount': data['amount'],
1126
+ }, index=idx)
1127
+
1128
+ for col in self.BAOSTOCK_BASE_FIELDS:
1129
+ df[col] = pd.to_numeric(df[col], errors='coerce')
1130
+ return df
@@ -18,7 +18,7 @@ class Simulate:
18
18
  from qka import Data, Simulate
19
19
 
20
20
  data = Data(symbols=[
21
- 'sh.600900', # 真实标的,照常下载
21
+ '600900.SH', # 真实标的,照常下载
22
22
  Simulate('sim.a', drift=0.0, reversion=0.08, vol=0.018),
23
23
  ])
24
24
  df = data.get(start_date='2023-01-01', end_date='2025-12-31')
@@ -64,7 +64,7 @@ from qka import Analysis
64
64
 
65
65
  analysis = Analysis()
66
66
  result = analysis.alpha_beta(
67
- df['sz.000001|returns'].dropna(),
67
+ df['000001.SZ|returns'].dropna(),
68
68
  df['benchmark|returns'].dropna(),
69
69
  )
70
70
  print(f'α={result.alpha:.2%}, β={result.beta:.2f}')
@@ -93,25 +93,25 @@ print(f'α={result.alpha:.2%}, β={result.beta:.2f}')
93
93
  ```python
94
94
  from qka import Data, Analysis
95
95
 
96
- data = Data(symbols=['sz.000001'], benchmark='sh.000300')
96
+ data = Data(symbols=['000001.SZ'], benchmark='000300.SH')
97
97
  df = data.get()
98
98
 
99
99
  analysis = Analysis()
100
100
 
101
101
  # 趋势分段
102
- segs = analysis.zigzag(df['sz.000001|close'], threshold=0.3, min_days=90)
102
+ segs = analysis.zigzag(df['000001.SZ|close'], threshold=0.3, min_days=90)
103
103
  for s in segs:
104
104
  print(f'{s.start.date()} ~ {s.end.date()} {s.direction} {s.change:+.1f}%')
105
105
 
106
106
  # 单次指标(标量,非滚动)
107
107
  ab = analysis.alpha_beta(
108
- df['sz.000001|returns'].dropna(),
108
+ df['000001.SZ|returns'].dropna(),
109
109
  df['benchmark|returns'].dropna(),
110
110
  )
111
111
  print(f'α={ab.alpha:.2%}, β={ab.beta:.2f}')
112
112
 
113
- sharpe = analysis.sharpe_ratio(df['sz.000001|returns'])
114
- mdd = analysis.max_drawdown(df['sz.000001|returns'])
115
- ir = analysis.information_ratio(df['sz.000001|returns'], df['benchmark|returns'])
113
+ sharpe = analysis.sharpe_ratio(df['000001.SZ|returns'])
114
+ mdd = analysis.max_drawdown(df['000001.SZ|returns'])
115
+ ir = analysis.information_ratio(df['000001.SZ|returns'], df['benchmark|returns'])
116
116
  print(f'夏普={sharpe:.2f}, 最大回撤={mdd:.1%}, IR={ir:.2f}')
117
117
  ```
@@ -28,7 +28,7 @@ bt = Backtest(data, strategy)
28
28
  | `cash` | `100000.0` | 初始资金 |
29
29
  | `start_date` | `None` | 回测起始日期 `'YYYY-MM-DD'` |
30
30
  | `end_date` | `None` | 回测截止日期 |
31
- | `benchmark` | `None` | 基准指数代码,baostock 格式如 `'sh.000300'` |
31
+ | `benchmark` | `None` | 基准指数代码,如 `'000300.SH'` |
32
32
 
33
33
  > 500 bar 以上自动分块迭代,避免一次性加载全量数据。
34
34
 
@@ -122,7 +122,7 @@ bt.results.iloc[-1] # 最终状态
122
122
  from qka import Data, Strategy, Backtest
123
123
 
124
124
  data = Data(
125
- symbols=['sz.000001'],
125
+ symbols=['000001.SZ'],
126
126
  indicators={
127
127
  'sma_5': ('ta.trend.sma_indicator', 'close', 5),
128
128
  'sma_20': ('ta.trend.sma_indicator', 'close', 20),
@@ -16,7 +16,7 @@
16
16
  买入,`size` 必须是 100 的整数倍(A 股 1 手 = 100 股)。
17
17
 
18
18
  ```python
19
- success = self.broker.buy('sz.000001', float(close['sz.000001']), 100)
19
+ success = self.broker.buy('000001.SZ', float(close['000001.SZ']), 100)
20
20
  ```
21
21
 
22
22
  - 实际成交价 = `price * (1 + slippage)`(默认滑点 0.1%)
@@ -31,7 +31,7 @@ success = self.broker.buy('sz.000001', float(close['sz.000001']), 100)
31
31
  卖出,`size` 必须是 100 的整数倍。
32
32
 
33
33
  ```python
34
- success = self.broker.sell('sz.000001', float(close['sz.000001']), 100)
34
+ success = self.broker.sell('000001.SZ', float(close['000001.SZ']), 100)
35
35
  ```
36
36
 
37
37
  - 自动扣佣金 + 印花税(万 5,仅卖出)
@@ -10,7 +10,7 @@
10
10
  from qka import Data
11
11
 
12
12
  data = Data(
13
- symbols=['sz.000001', 'sh.600000'],
13
+ symbols=['000001.SZ', '600000.SH'],
14
14
  period='1d',
15
15
  adjust='qfq',
16
16
  benchmark=None,
@@ -20,12 +20,13 @@ data = Data(
20
20
 
21
21
  | 参数 | 类型 | 默认值 | 说明 |
22
22
  |------|------|--------|------|
23
- | `symbols` | `list[str \| Simulate]` | `None` | 标的列表。元素可以是 A 股代码(baostock 格式 `sz.000001`、`sh.600000`),也可以是 `Simulate` 对象(模拟标的),两者可混排,见下方「模拟标的」 |
23
+ | `symbols` | `list[str \| Simulate]` | `None` | 标的列表。元素可以是 A 股代码(**`代码.市场`**,如 `'000001.SZ'`、`'600000.SH'`;也兼容 `sz.000001` 旧写法),也可以是 `Simulate` 对象(模拟标的),两者可混排,见下方「模拟标的」 |
24
24
  | `period` | `str` | `'1d'` | 数据周期,当前仅支持 `'1d'` |
25
25
  | `adjust` | `str` | `'qfq'` | 复权方式:`'qfq'` 前复权,`'hfq'` 后复权,`'bfq'` 不复权 |
26
- | `benchmark` | `str` | `None` | 基准指数代码,如 `'sh.000300'`。下载后在 `get()` 结果中追加 `benchmark|returns` 列 |
26
+ | `source` | `str` | `'tickflow'` | 数据源。`'tickflow'`(默认)行情覆盖全,**含 ETF / 可转债全历史**;`'baostock'` 提供估值等扩展字段(见 `extra_fields`) |
27
+ | `benchmark` | `str` | `None` | 基准指数代码,如 `'000300.SH'`。下载后在 `get()` 结果中追加 `benchmark|returns` 列 |
27
28
  | `indicators` | `dict` | `None` | 预计算指标,见下方 |
28
- | `extra_fields` | `list[str]` | `None` | baostock 扩展字段(选股/估值用),见下方 |
29
+ | `extra_fields` | `list[str]` | `None` | 扩展字段(选股/估值用)。**仅 `source='baostock'` 提供**;`tickflow` 源无这些列,传入会被静默忽略,见下方 |
29
30
  | `warmup` | `int` | `0` | 指标预热天数。回测/取数时自动多读 `warmup` 个交易日历史用于计算指标,使第 1 个交易日即拿到有效指标值(无需手写跳过前 N 根 bar)。仅用于计算,不增加回测 bar 数 |
30
31
 
31
32
  > **warmup(指标预热)**:若指标/因子需要较长历史窗口(如动量排名需看前 200 天),设 `warmup=200`。
@@ -34,7 +35,7 @@ data = Data(
34
35
  >
35
36
  > ```python
36
37
  > data = Data(
37
- > symbols=['sz.000001'],
38
+ > symbols=['000001.SZ'],
38
39
  > indicators={'mom200': lambda df: df['close'].pct_change(200)},
39
40
  > warmup=200, # 自定义指标 qka 推断不出窗口,显式声明预热天数
40
41
  > )
@@ -54,7 +55,7 @@ data = Data(
54
55
  from qka import Data, Simulate
55
56
 
56
57
  data = Data(symbols=[
57
- 'sh.600900', # 真实标的,照常下载
58
+ '600900.SH', # 真实标的,照常下载
58
59
  Simulate('sim.a', drift=0.0, reversion=0.08, vol=0.018), # 模拟标的
59
60
  ])
60
61
  df = data.get(start_date='2023-01-01', end_date='2025-12-31')
@@ -95,7 +96,9 @@ r_t = drift + reversion * (logAnchor - logP_{t-1}) + vol * eps_t eps_t ~
95
96
 
96
97
  ### extra_fields 扩展字段
97
98
 
98
- `extra_fields` 追加 baostock `query_history_k_data_plus` 接口支持的扩展列(选股/估值用),列名同样遵循 `{symbol}|{field}` 约定(如 `sz.000001|peTTM`),数值自动转为 `float64`。**不是指标,不参与 indicators 预计算**,是随行情一起下载的原始字段。
99
+ `extra_fields` 追加扩展列(选股/估值用),列名同样遵循 `{symbol}|{field}` 约定(如 `000001.SZ|peTTM`),数值自动转为 `float64`。**不是指标,不参与 indicators 预计算**,是随行情一起下载的原始字段。
100
+
101
+ > ⚠️ **仅 `source='baostock'` 提供**:这些扩展列来自 baostock 的 `query_history_k_data_plus` 接口。默认源 `'tickflow'` 的历史日线固定只有 OHLCV + 成交额,**没有**这些扩展列——此时传入的 `extra_fields` 会被静默忽略(不报错、也不生成对应列)。需要估值/换手率等字段时,显式用 `Data(..., source='baostock')`。
99
102
 
100
103
  **不传 `extra_fields` 时,每只股票默认只有 6 个行情列 + 1 个常驻列:**
101
104
 
@@ -113,11 +116,11 @@ r_t = drift + reversion * (logAnchor - logP_{t-1}) + vol * eps_t eps_t ~
113
116
 
114
117
  ```python
115
118
  data = Data(
116
- symbols=['sz.000001'],
119
+ symbols=['000001.SZ'],
117
120
  extra_fields=['peTTM', 'pbMRQ', 'turn'], # 选股常用:估值 + 换手率
118
121
  )
119
122
  df = data.get(start_date='2024-01-02', end_date='2024-01-05')
120
- df['sz.000001|peTTM'] # 市盈率(TTM),float64
123
+ df['000001.SZ|peTTM'] # 市盈率(TTM),float64
121
124
  ```
122
125
 
123
126
  可用的扩展字段(白名单,传其他字段会抛 `ValueError`):
@@ -153,7 +156,7 @@ df['sz.000001|peTTM'] # 市盈率(TTM),float64
153
156
 
154
157
  ```python
155
158
  data = Data(
156
- symbols=['sz.000001'],
159
+ symbols=['000001.SZ'],
157
160
  indicators={
158
161
  'sma_5': ('ta.trend.sma_indicator', 'close', 5),
159
162
  'sma_20': ('ta.trend.sma_indicator', 'close', 20),
@@ -277,8 +280,8 @@ qka 框架内置的滚动窗口指标,格式为 `{'列名': ('qka.函数名',
277
280
 
278
281
  ```python
279
282
  data = Data(
280
- symbols=['sz.000001', 'sh.600000'],
281
- benchmark='sh.000300', # alpha/beta/information_ratio 必需
283
+ symbols=['000001.SZ', '600000.SH'],
284
+ benchmark='000300.SH', # alpha/beta/information_ratio 必需
282
285
  indicators={
283
286
  'sma_20': ('ta.trend.sma_indicator', 'close', 20),
284
287
  'beta_60': ('qka.beta', 60),
@@ -326,9 +329,9 @@ df = data.get(start_date='2024-01-01', end_date='2024-12-31')
326
329
  |------|------|
327
330
  | 类型 | `pd.DataFrame`(`lazy=True` 时返回 `dask.DataFrame`) |
328
331
  | 索引 | 日期索引,**索引名为 `"date"`**。`reset_index()` 后日期列名也是 `"date"` |
329
- | 列名 | `{symbol}|{factor}` — 例如 `sz.000001|close`、`sz.000001|sma_5`、`sh.600000|volume` |
332
+ | 列名 | `{symbol}|{factor}` — 例如 `000001.SZ|close`、`000001.SZ|sma_5`、`600000.SH|volume` |
330
333
  | 常驻列 | 除 `open/high/low/close/volume/amount` 外,自动内置 `{symbol}|returns` |
331
- | 扩展列 | 构造时设了 `extra_fields` 时,追加 `{symbol}|{field}` 原始字段列(如 `sz.000001|peTTM`),见上方 extra_fields 小节 |
334
+ | 扩展列 | 构造时设了 `extra_fields` 时,追加 `{symbol}|{field}` 原始字段列(如 `000001.SZ|peTTM`),见上方 extra_fields 小节 |
332
335
  | 基准列 | 若构造时设了 `benchmark`,追加 `benchmark|returns`(无 `{symbol}|` 前缀) |
333
336
  | 列值 | 全部为 `float64`,指标列的早期行可能含 `NaN` |
334
337
  | 异常 | 无数据时抛出 `RuntimeError` |
@@ -336,7 +339,7 @@ df = data.get(start_date='2024-01-01', end_date='2024-12-31')
336
339
  **返回的宽表示例:**
337
340
 
338
341
  ```python
339
- data = Data(symbols=['sz.000001', 'sh.600000'], indicators={
342
+ data = Data(symbols=['000001.SZ', '600000.SH'], indicators={
340
343
  'sma_5': ('ta.trend.sma_indicator', 'close', 5),
341
344
  })
342
345
  df = data.get(start_date='2024-01-02', end_date='2024-01-05')
@@ -345,7 +348,7 @@ df = data.get(start_date='2024-01-02', end_date='2024-01-05')
345
348
  返回的 DataFrame 结构(行=日期,列=每只股票的完整字段堆叠):
346
349
 
347
350
  ```
348
- sz.000001|open sz.000001|close ... sz.000001|sma_5 sh.600000|open sh.600000|close ... sh.600000|sma_5
351
+ 000001.SZ|open 000001.SZ|close ... 000001.SZ|sma_5 600000.SH|open 600000.SH|close ... 600000.SH|sma_5
349
352
  2024-01-02 10.0 10.2 ... NaN 15.0 15.3 ... NaN
350
353
  2024-01-03 10.1 10.5 ... NaN 15.2 15.6 ... NaN
351
354
  2024-01-04 10.3 10.8 ... NaN 14.9 15.1 ... NaN
@@ -355,13 +358,13 @@ df = data.get(start_date='2024-01-02', end_date='2024-01-05')
355
358
  - 每只股票独占一组列,列前缀 = symbol
356
359
  - `returns` 列自动存在,无需在 indicators 中声明
357
360
  - 前 4 行 SMA 为 NaN(窗口=5,不足)
358
- - 若设了 `benchmark='sh.000300'`,末尾多一列 `benchmark|returns`
361
+ - 若设了 `benchmark='000300.SH'`,末尾多一列 `benchmark|returns`
359
362
 
360
363
  ```python
361
364
  # 列名格式:{symbol}|{factor}
362
- df.columns # ['sz.000001|open', 'sz.000001|close', 'sz.000001|returns',
363
- # 'sz.000001|sma_5', 'sh.600000|open', 'sh.600000|close',
364
- # 'sh.600000|returns', 'sh.600000|sma_5']
365
+ df.columns # ['000001.SZ|open', '000001.SZ|close', '000001.SZ|returns',
366
+ # '000001.SZ|sma_5', '600000.SH|open', '600000.SH|close',
367
+ # '600000.SH|returns', '600000.SH|sma_5']
365
368
 
366
369
  # 索引名为 "date",reset_index 后转为 pd.Timestamp 列
367
370
  df = df.reset_index()
@@ -10,8 +10,8 @@
10
10
  | `atr_risk(risk_ratio, price, atr_value, multiplier=2.0)` | ATR 风险仓位 |
11
11
 
12
12
  ```python
13
- price = float(close['sz.000001'])
13
+ price = float(close['000001.SZ'])
14
14
  size = self.sizing.percent(0.1, price) # 10% 仓位,已按手取整
15
15
  if size > 0:
16
- self.broker.buy('sz.000001', price, size)
16
+ self.broker.buy('000001.SZ', price, size)
17
17
  ```
@@ -49,14 +49,14 @@ sma5 = self.get('sma_5') # indicators 中定义的指标列
49
49
  | 属性 | 说明 |
50
50
  |------|------|
51
51
  | 返回类型 | `pd.Series` |
52
- | index | 股票代码,如 `'sz.000001'`、`'sh.600000'` |
52
+ | index | 股票代码,如 `'000001.SZ'`、`'600000.SH'` |
53
53
  | values | 当前 bar 的最新值,`float` |
54
54
  | 空值 | 无数据时返回空 `pd.Series`,不是 `None` |
55
55
 
56
56
  ```python
57
57
  # 安全访问
58
- if 'sz.000001' in close.index:
59
- price = float(close['sz.000001'])
58
+ if '000001.SZ' in close.index:
59
+ price = float(close['000001.SZ'])
60
60
  ```
61
61
 
62
62
  ### self.history()
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