qka 2.3.2.dev2__tar.gz → 2.4.0__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 (32) hide show
  1. {qka-2.3.2.dev2 → qka-2.4.0}/PKG-INFO +3 -1
  2. {qka-2.3.2.dev2 → qka-2.4.0}/README.md +2 -0
  3. {qka-2.3.2.dev2 → qka-2.4.0}/qka/__init__.py +4 -0
  4. {qka-2.3.2.dev2 → qka-2.4.0}/qka/core/data.py +124 -11
  5. qka-2.4.0/qka/core/tdx.py +207 -0
  6. {qka-2.3.2.dev2 → qka-2.4.0}/skills/qka/references/data.md +27 -3
  7. {qka-2.3.2.dev2 → qka-2.4.0}/.github/workflows/release.yml +0 -0
  8. {qka-2.3.2.dev2 → qka-2.4.0}/.gitignore +0 -0
  9. {qka-2.3.2.dev2 → qka-2.4.0}/.vscode/settings.json +0 -0
  10. {qka-2.3.2.dev2 → qka-2.4.0}/CHANGELOG.md +0 -0
  11. {qka-2.3.2.dev2 → qka-2.4.0}/LICENSE +0 -0
  12. {qka-2.3.2.dev2 → qka-2.4.0}/pyproject.toml +0 -0
  13. {qka-2.3.2.dev2 → qka-2.4.0}/qka/cli.py +0 -0
  14. {qka-2.3.2.dev2 → qka-2.4.0}/qka/core/__init__.py +0 -0
  15. {qka-2.3.2.dev2 → qka-2.4.0}/qka/core/accessor.py +0 -0
  16. {qka-2.3.2.dev2 → qka-2.4.0}/qka/core/analysis.py +0 -0
  17. {qka-2.3.2.dev2 → qka-2.4.0}/qka/core/backtest.py +0 -0
  18. {qka-2.3.2.dev2 → qka-2.4.0}/qka/core/broker.py +0 -0
  19. {qka-2.3.2.dev2 → qka-2.4.0}/qka/core/indicator.py +0 -0
  20. {qka-2.3.2.dev2 → qka-2.4.0}/qka/core/report.py +0 -0
  21. {qka-2.3.2.dev2 → qka-2.4.0}/qka/core/simulate.py +0 -0
  22. {qka-2.3.2.dev2 → qka-2.4.0}/qka/core/sizing.py +0 -0
  23. {qka-2.3.2.dev2 → qka-2.4.0}/qka/core/strategy.py +0 -0
  24. {qka-2.3.2.dev2 → qka-2.4.0}/qka/utils/__init__.py +0 -0
  25. {qka-2.3.2.dev2 → qka-2.4.0}/qka/utils/logger.py +0 -0
  26. {qka-2.3.2.dev2 → qka-2.4.0}/skills/qka/SKILL.md +0 -0
  27. {qka-2.3.2.dev2 → qka-2.4.0}/skills/qka/references/analysis.md +0 -0
  28. {qka-2.3.2.dev2 → qka-2.4.0}/skills/qka/references/backtest.md +0 -0
  29. {qka-2.3.2.dev2 → qka-2.4.0}/skills/qka/references/broker.md +0 -0
  30. {qka-2.3.2.dev2 → qka-2.4.0}/skills/qka/references/sizing.md +0 -0
  31. {qka-2.3.2.dev2 → qka-2.4.0}/skills/qka/references/strategy.md +0 -0
  32. {qka-2.3.2.dev2 → qka-2.4.0}/tools/generate_api_ref.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: qka
3
- Version: 2.3.2.dev2
3
+ Version: 2.4.0
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
@@ -137,6 +137,7 @@ print(bt.metrics['sharpe_ratio']) # 夏普比率
137
137
  ## 核心能力
138
138
 
139
139
  - **数据获取** — 写个股票代码就能取到本地,自动缓存、自动补齐最新数据,重复使用秒读
140
+ - **多数据源** — 内置 `tickflow`、`baostock`、`tdx` 三个行情源,改一个 `source=` 即可切换,各源缓存分目录存放、互不干扰
140
141
  - **技术指标** — 均线、MACD、RSI、ATR 等 ta 库全部指标,外加 α/β、夏普、最大回撤等内置指标,声明一下即可,策略里不用自己 rolling
141
142
  - **估值字段** — 换手率、涨跌幅、停牌与 ST 标记、市盈率、市净率等,需要哪些就取哪些
142
143
  - **模拟行情** — 一条 `Simulate` 就能造出单边上涨、单边下跌、来回震荡的走势,不必等真实行情,用来验证策略是不是真在按逻辑交易
@@ -200,6 +201,7 @@ print(bt.metrics['total_return_pct'])
200
201
 
201
202
  - [tickflow](https://tickflow.org) — 免费 A 股 / ETF / 可转债行情(默认数据源)
202
203
  - [baostock](http://baostock.com) — 免费 A 股数据(估值等扩展字段)
204
+ - [通达信](https://www.tdx.com.cn) — 本机「TQ 版」客户端的行情(`source='tdx'`,仅 Windows)
203
205
  - [ta](https://github.com/bukosabino/ta) — 技术指标库
204
206
 
205
207
  ---
@@ -86,6 +86,7 @@ print(bt.metrics['sharpe_ratio']) # 夏普比率
86
86
  ## 核心能力
87
87
 
88
88
  - **数据获取** — 写个股票代码就能取到本地,自动缓存、自动补齐最新数据,重复使用秒读
89
+ - **多数据源** — 内置 `tickflow`、`baostock`、`tdx` 三个行情源,改一个 `source=` 即可切换,各源缓存分目录存放、互不干扰
89
90
  - **技术指标** — 均线、MACD、RSI、ATR 等 ta 库全部指标,外加 α/β、夏普、最大回撤等内置指标,声明一下即可,策略里不用自己 rolling
90
91
  - **估值字段** — 换手率、涨跌幅、停牌与 ST 标记、市盈率、市净率等,需要哪些就取哪些
91
92
  - **模拟行情** — 一条 `Simulate` 就能造出单边上涨、单边下跌、来回震荡的走势,不必等真实行情,用来验证策略是不是真在按逻辑交易
@@ -149,6 +150,7 @@ print(bt.metrics['total_return_pct'])
149
150
 
150
151
  - [tickflow](https://tickflow.org) — 免费 A 股 / ETF / 可转债行情(默认数据源)
151
152
  - [baostock](http://baostock.com) — 免费 A 股数据(估值等扩展字段)
153
+ - [通达信](https://www.tdx.com.cn) — 本机「TQ 版」客户端的行情(`source='tdx'`,仅 Windows)
152
154
  - [ta](https://github.com/bukosabino/ta) — 技术指标库
153
155
 
154
156
  ---
@@ -20,6 +20,8 @@ from qka.core.strategy import Strategy
20
20
  from qka.core.broker import Broker
21
21
  from qka.core.sizing import SizingAccessor
22
22
  from qka.core.analysis import Analysis, Segment, AlphaBeta
23
+ # 通达信数据源:安装目录的设置与查询(source='tdx' 时使用)
24
+ from qka.core.tdx import set_tdx_root, get_tdx_root_info
23
25
 
24
26
  # 子模块导入
25
27
  from qka import core, utils
@@ -29,6 +31,8 @@ __all__ = [
29
31
  'Data', 'Simulate', 'Backtest', 'Strategy', 'Broker', 'DataAccessor', 'SizingAccessor',
30
32
  # 分析
31
33
  'Analysis', 'Segment', 'AlphaBeta',
34
+ # 通达信数据源
35
+ 'set_tdx_root', 'get_tdx_root_info',
32
36
  # 子模块
33
37
  'core', 'utils'
34
38
  ]
@@ -21,6 +21,7 @@ from typing import List, Optional, Callable, Union
21
21
  from qka.utils.logger import logger
22
22
  from qka.core import indicator
23
23
  from qka.core.simulate import Simulate
24
+ from qka.core import tdx
24
25
 
25
26
  # qka 内置指标裸名集合,用于 dispatch 识别(无需 qka. 前缀)
26
27
  _QKA_BUILTIN_NAMES = frozenset({
@@ -64,16 +65,17 @@ class Data():
64
65
  period (str): 数据周期,如 '1d'、'1m' 等
65
66
  adjust (str): 复权方式,如 'qfq'、'hfq'、'bfq'
66
67
  indicators (dict | Callable): 预计算指标/因子
67
- source (str): 数据源,默认 'tickflow'(可选 'baostock')
68
+ source (str): 数据源,默认 'tickflow'(可选 'baostock'、'tdx')
68
69
  pool_size (int): 并发下载线程数
69
70
  datadir (Path): 数据缓存目录
70
71
  target_dir (Path): 目标存储目录
71
72
  extra_fields (List[str]): 扩展字段(选股/估值用),如 ['peTTM', 'pbMRQ', 'turn']。
72
- 仅 'baostock' 源提供(可选值见 BAOSTOCK_EXTRA_FIELDS);'tickflow' 源无扩展列,会忽略
73
+ 仅 'baostock' 源提供(可选值见 BAOSTOCK_EXTRA_FIELDS);'tickflow' 与
74
+ 'tdx' 源无扩展列,会忽略
73
75
  """
74
76
 
75
77
  # 支持的行情源
76
- SOURCES = ('tickflow', 'baostock')
78
+ SOURCES = ('tickflow', 'baostock', 'tdx')
77
79
 
78
80
  # baostock query_history_k_data_plus 完整支持的基础字段(除 date 索引外)
79
81
  BAOSTOCK_BASE_FIELDS = ["open", "high", "low", "close", "volume", "amount"]
@@ -92,6 +94,8 @@ class Data():
92
94
  # TickFlow 可提供的扩展字段:其历史日线固定返回 OHLCV + amount,
93
95
  # 无估值/换手率等扩展列(季报级 financials 不是每日序列,不接入)。
94
96
  TICKFLOW_EXTRA_FIELDS = []
97
+ # 通达信源当前只接行情(OHLCV + amount),暂无扩展列
98
+ TDX_EXTRA_FIELDS = []
95
99
 
96
100
  # parquet 元数据键:这只标的「已确认起点」——数据源在该日期之前没有更早的数据。
97
101
  # 没有它,请求起点只要早于缓存首日(往往只是撞上长假)就得白跑一趟网络去确认。
@@ -152,14 +156,17 @@ class Data():
152
156
  仅供辅助计算(β/α 等),不参与指标计算
153
157
  period: 数据周期,如 '1d'(日线)、'1m'(分钟)
154
158
  adjust: 复权方式,'qfq'(前复权)、'hfq'(后复权)、'bfq'(不复权)
155
- source: 数据来源,默认 'tickflow'(可选 'baostock')。两者返回结构一致,
156
- 切换源不影响上层调用;差异见各源的扩展字段能力
159
+ source: 数据来源,默认 'tickflow'(可选 'baostock'、'tdx')。各源返回结构一致,
160
+ 切换源不影响上层调用;差异见各源的扩展字段能力。'tdx' 走本机安装的
161
+ 「支持 TQ 策略」通达信客户端(仅 Windows,需客户端在运行),安装目录的定位
162
+ 方式见 `qka.set_tdx_root` / `qka.get_tdx_root_info`
157
163
  pool_size: 并发下载线程数
158
164
  datadir: 缓存目录路径
159
165
  indicators: 预计算指标/因子,支持三种格式:
160
166
  extra_fields: 扩展字段列表(选股/估值用,如 ['peTTM', 'pbMRQ', 'turn']),
161
167
  可选值见 BAOSTOCK_EXTRA_FIELDS —— 仅 'baostock' 源提供;
162
- 'tickflow' 源无扩展列,传入的扩展字段会被静默忽略(不报错、不生成该列)。
168
+ 'tickflow' 与 'tdx' 源无扩展列,传入的扩展字段会被静默忽略
169
+ (不报错、不生成该列)。
163
170
  追加的列同样遵循 {symbol}|{field} 命名,如 '600519.SH|peTTM'。注意:
164
171
  首次下载后缓存字段固定,变更 extra_fields 会自动检测列缺失并重新下载对应股票。
165
172
 
@@ -217,8 +224,7 @@ class Data():
217
224
  self.warmup = warmup
218
225
 
219
226
  # 当前数据源实际支持的扩展字段(源能力不同,按源过滤)
220
- whitelist = (self.BAOSTOCK_EXTRA_FIELDS if source == 'baostock'
221
- else self.TICKFLOW_EXTRA_FIELDS)
227
+ whitelist = self._extra_fields_whitelist(source)
222
228
 
223
229
  # extra_fields 校验 + 去重
224
230
  # 未知字段(拼写错误)报错;当前源提供不了的字段静默忽略
@@ -448,6 +454,14 @@ class Data():
448
454
  return True
449
455
  return any(f not in cols for f in self.extra_fields)
450
456
 
457
+ def _extra_fields_whitelist(self, source: str) -> List[str]:
458
+ """该数据源实际支持的扩展字段(源能力不同,按源过滤)。"""
459
+ if source == 'baostock':
460
+ return self.BAOSTOCK_EXTRA_FIELDS
461
+ if source == 'tdx':
462
+ return self.TDX_EXTRA_FIELDS
463
+ return self.TICKFLOW_EXTRA_FIELDS
464
+
451
465
  def _merged_extra_fields(self, path: Path) -> List[str]:
452
466
  """
453
467
  计算本次下载实际请求的扩展字段:当前 extra_fields 与缓存已有扩展列的并集。
@@ -462,8 +476,7 @@ class Data():
462
476
  existing = set(pq.read_schema(path).names)
463
477
  except Exception:
464
478
  return merged
465
- whitelist = (self.BAOSTOCK_EXTRA_FIELDS if self.source == 'baostock'
466
- else self.TICKFLOW_EXTRA_FIELDS)
479
+ whitelist = self._extra_fields_whitelist(self.source)
467
480
  for f in whitelist:
468
481
  if f in existing and f not in merged:
469
482
  merged.append(f)
@@ -712,7 +725,10 @@ class Data():
712
725
  if need_calendar:
713
726
  sim_index = self._trading_index(sim_start, sim_end)
714
727
  if need_download:
715
- if self.source == 'baostock':
728
+ # baostock 与 tdx 都是「进程级单例连接」(tdx 的 tq 类方法
729
+ # 共用同一 run_id),并发调用会共用同一连接状态,故逐个下载;
730
+ # tickflow 的客户端无此约束,可并发。
731
+ if self.source in ('baostock', 'tdx'):
716
732
  for symbol in tqdm(need_download, desc="下载数据"):
717
733
  try:
718
734
  self._download(symbol, download_start, download_end)
@@ -1106,6 +1122,8 @@ class Data():
1106
1122
  """按当前数据源分发到对应实现,返回结构一致(date 索引 + OHLCV/amount)。"""
1107
1123
  if self.source == 'baostock':
1108
1124
  return self._get_from_baostock(symbol, start_date, end_date, extra_fields)
1125
+ if self.source == 'tdx':
1126
+ return self._get_from_tdx(symbol, start_date, end_date, extra_fields)
1109
1127
  return self._get_from_tickflow(symbol, start_date, end_date, extra_fields)
1110
1128
 
1111
1129
  # ── TickFlow 数据源 ────────────────────────────────────────
@@ -1164,6 +1182,11 @@ class Data():
1164
1182
  market, code = cls._split_symbol(symbol)
1165
1183
  return f"{code}.{market.upper()}"
1166
1184
 
1185
+ @classmethod
1186
+ def to_tdx_symbol(cls, symbol: str) -> str:
1187
+ """任意写法 → 通达信写法,如 600519.SH(与 tickflow 同形)"""
1188
+ return cls.to_tickflow_symbol(symbol)
1189
+
1167
1190
  def _get_from_tickflow(
1168
1191
  self, symbol: str,
1169
1192
  start_date: str = '1990-01-01',
@@ -1235,3 +1258,93 @@ class Data():
1235
1258
  for col in self.BAOSTOCK_BASE_FIELDS:
1236
1259
  df[col] = pd.to_numeric(df[col], errors='coerce')
1237
1260
  return df
1261
+
1262
+ # ── 通达信数据源(本机 TQ 版客户端,仅 Windows)────────────────
1263
+
1264
+ # 通达信 get_market_data 字段名 → qka 标准列名
1265
+ TDX_FIELD_MAP = {
1266
+ 'Open': 'open', 'High': 'high', 'Low': 'low',
1267
+ 'Close': 'close', 'Volume': 'volume', 'Amount': 'amount',
1268
+ }
1269
+
1270
+ def _get_from_tdx(
1271
+ self, symbol: str,
1272
+ start_date: str = '1990-01-01',
1273
+ end_date: str = '2050-12-31',
1274
+ extra_fields: Optional[List[str]] = None,
1275
+ ) -> pd.DataFrame:
1276
+ """
1277
+ 从本机「支持 TQ 策略」的通达信客户端获取单只标的的日线数据。
1278
+
1279
+ 返回结构与 _get_from_baostock 一致(date 索引 + open/high/low/close/
1280
+ volume/amount),两处口径在本源内直接对齐:
1281
+ - **volume**:通达信给的就是「股」,与 baostock 口径一致,原样使用
1282
+ - **amount**:通达信单位是「万元」,×10000 换算成「元」
1283
+
1284
+ 复权由通达信自己按 dividend_type 算好(front=前复权 / back=后复权 /
1285
+ none=不复权),本处只做口径映射,不自行计算复权。
1286
+
1287
+ 取数前必须先 ``refresh_kline``:通达信本地只存「下载过」的 K 线,直接查
1288
+ 没下载过的标的只会得到最新一根(历史静默缺失)。refresh_kline 让客户端
1289
+ 把该标的的 K 线补齐,本地已有时也会很快返回。
1290
+
1291
+ extra_fields 对通达信行情无意义(当前仅接行情,无扩展列),此处不处理。
1292
+ """
1293
+ # 周期映射(当前仅日线)
1294
+ period_map = {'1d': '1d'}
1295
+ if self.period not in period_map:
1296
+ raise RuntimeError(
1297
+ f"tdx 源暂不支持周期 {self.period!r}(可选: {list(period_map)})"
1298
+ )
1299
+
1300
+ # 复权映射:通达信 front=前复权 / back=后复权 / none=不复权
1301
+ adjust_map = {'bfq': 'none', 'qfq': 'front', 'hfq': 'back'}
1302
+ dividend_type = adjust_map.get(self.adjust, 'front')
1303
+
1304
+ tdx_symbol = self.to_tdx_symbol(symbol)
1305
+ tq = tdx.get_tq()
1306
+
1307
+ # 通达信只认 YYYYMMDD
1308
+ start = pd.Timestamp(start_date).strftime('%Y%m%d')
1309
+ end = pd.Timestamp(end_date).strftime('%Y%m%d')
1310
+
1311
+ # 先让客户端补齐该标的的 K 线(失败返回 None),否则历史会静默缺失
1312
+ if tq.refresh_kline(stock_list=[tdx_symbol],
1313
+ period=period_map[self.period]) is None:
1314
+ raise RuntimeError(
1315
+ f"通达信刷新 K 线缓存失败({tdx_symbol}):"
1316
+ "请确认「TQ 版通达信」客户端已启动并已登录。"
1317
+ )
1318
+
1319
+ try:
1320
+ raw = tq.get_market_data(
1321
+ stock_list=[tdx_symbol],
1322
+ start_time=start,
1323
+ end_time=end,
1324
+ dividend_type=dividend_type,
1325
+ period=period_map[self.period],
1326
+ )
1327
+ except Exception as e: # noqa: BLE001
1328
+ raise RuntimeError(f"通达信查询 {symbol}({tdx_symbol}) 失败: {e}") from e
1329
+
1330
+ # 无任何数据时返回空 dict
1331
+ if not raw:
1332
+ return pd.DataFrame()
1333
+
1334
+ # 每张字段表都是「日期为索引、列为代码」的宽表,只取当前这一列
1335
+ cols = {}
1336
+ for src, dst in self.TDX_FIELD_MAP.items():
1337
+ tbl = raw.get(src)
1338
+ if tbl is None or tdx_symbol not in getattr(tbl, 'columns', []):
1339
+ return pd.DataFrame()
1340
+ cols[dst] = tbl[tdx_symbol]
1341
+
1342
+ df = pd.DataFrame(cols)
1343
+ df.index = pd.DatetimeIndex(df.index)
1344
+ df.index.name = 'date'
1345
+ df['amount'] = df['amount'] * 10000 # 万元 → 元
1346
+ df = df[df['close'].notna()] # 去掉多标的并集索引带出的空行
1347
+ df = df[~df.index.duplicated(keep='last')].sort_index()
1348
+ for col in self.BAOSTOCK_BASE_FIELDS:
1349
+ df[col] = pd.to_numeric(df[col], errors='coerce')
1350
+ return df
@@ -0,0 +1,207 @@
1
+ """
2
+ 通达信(TQ)数据源支持
3
+
4
+ qka 通过本机安装的「支持 TQ 策略」的通达信客户端取数:在进程内 ``import tqcenter``
5
+ (该模块随客户端自带),由 ``tqcenter`` 加载客户端目录下的 ``TPythClient.dll`` 与本机
6
+ 运行中的 ``TdxW.exe`` 通信。因此本数据源:
7
+
8
+ - 仅支持 Windows;
9
+ - 要求用它的机器上装着 TQ 版通达信,且客户端在运行。
10
+
11
+ 「通达信装在哪」是**这台机器怎么连上它**的问题,与「这次要取什么数据」(Data 的
12
+ symbols/period/adjust 等)无关,故独立于 Data 管理:优先用代码显式设置的,其次看
13
+ 环境变量 ``QKA_TDX_ROOT``,最后自动从注册表识别。
14
+ """
15
+
16
+ import os
17
+ import sys
18
+ import threading
19
+ from pathlib import Path
20
+ from typing import List, Optional
21
+
22
+ from qka.utils.logger import logger
23
+
24
+ # 环境变量:指向「支持 TQ 策略」的通达信安装目录
25
+ ENV_VAR = 'QKA_TDX_ROOT'
26
+
27
+ # 判定「是不是 TQ 版」的标志文件(相对安装目录)。普通版没有这个模块。
28
+ _TQ_MARKER = ('PYPlugins', 'user', 'tqcenter.py')
29
+
30
+ # 代码级设置(优先级最高,见 set_tdx_root)
31
+ _code_root: Optional[Path] = None
32
+
33
+ # 进程级复用:tqcenter 的 tq 是类级单例连接,全进程共用一个实例
34
+ _tq_client = None
35
+ _tq_lock = threading.Lock()
36
+
37
+
38
+ # ── 路径:识别 / 设置 / 查询 ────────────────────────────────────
39
+
40
+ def _is_tq_install(root: Path) -> bool:
41
+ """目录是否是一套带 TQ 组件的通达信安装(依据标志文件是否存在)。"""
42
+ try:
43
+ return root.joinpath(*_TQ_MARKER).is_file()
44
+ except OSError:
45
+ return False
46
+
47
+
48
+ def _iter_registry_tdx_dirs() -> List[str]:
49
+ """枚举注册表「卸载」表里所有含「通达信」的安装目录。
50
+
51
+ 覆盖 HKLM / HKCU 两个配置单元,并显式尝试 32 位与 64 位两个视图
52
+ (TQ 版注册名形如「通达信金融终端64」)。非 Windows 或读不到时返回空列表。
53
+ """
54
+ try:
55
+ import winreg
56
+ except ImportError:
57
+ return []
58
+
59
+ sub = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
60
+ hives = (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER)
61
+ views = (winreg.KEY_WOW64_64KEY, winreg.KEY_WOW64_32KEY, 0)
62
+
63
+ found: List[str] = []
64
+ for hive in hives:
65
+ for view in views:
66
+ try:
67
+ key = winreg.OpenKey(hive, sub, 0, winreg.KEY_READ | view)
68
+ except OSError:
69
+ continue
70
+ try:
71
+ idx = 0
72
+ while True:
73
+ try:
74
+ name = winreg.EnumKey(key, idx)
75
+ except OSError:
76
+ break
77
+ idx += 1
78
+ try:
79
+ sk = winreg.OpenKey(key, name)
80
+ except OSError:
81
+ continue
82
+ display = _reg_value(sk, "DisplayName")
83
+ location = _reg_value(sk, "InstallLocation")
84
+ if location and display and "通达信" in str(display):
85
+ found.append(str(location))
86
+ finally:
87
+ key.Close()
88
+ return found
89
+
90
+
91
+ def _reg_value(key, name: str):
92
+ """读一个注册表值,不存在返回 None。"""
93
+ import winreg
94
+ try:
95
+ return winreg.QueryValueEx(key, name)[0]
96
+ except OSError:
97
+ return None
98
+
99
+
100
+ def set_tdx_root(path) -> Path:
101
+ """显式指定「支持 TQ 策略」的通达信安装目录(优先级最高)。
102
+
103
+ Args:
104
+ path: 安装目录,如 ``r'D:\\new_tdx64'``
105
+
106
+ Returns:
107
+ Path: 规范化后的目录
108
+
109
+ Raises:
110
+ ValueError: 目录下没有 TQ 组件(``PYPlugins/user/tqcenter.py``)
111
+ """
112
+ global _code_root
113
+ root = Path(path).expanduser()
114
+ if not _is_tq_install(root):
115
+ raise ValueError(
116
+ f"{root} 不是支持 TQ 策略的通达信目录(未找到 "
117
+ f"{Path(*_TQ_MARKER)})。请指向装有 TQ 组件的那套安装。"
118
+ )
119
+ _code_root = root
120
+ logger.info(f"已指定通达信安装目录: {root}")
121
+ return root
122
+
123
+
124
+ def get_tdx_root_info() -> dict:
125
+ """查询「当前会使用哪个通达信目录、以及它是怎么来的」。
126
+
127
+ 只做识别、**不建立连接**,因此可在客户端未启动时安全调用(如界面展示)。
128
+
129
+ Returns:
130
+ dict: ``{'root': str | None, 'source': 'code' | 'env' | 'auto' | None}``
131
+
132
+ - ``source='code'``:代码里 ``set_tdx_root()`` 指定的
133
+ - ``source='env'``:环境变量 ``QKA_TDX_ROOT`` 指定的
134
+ - ``source='auto'``:从注册表自动识别到的
135
+ - ``root=None``:以上都没有找到
136
+ """
137
+ if _code_root is not None:
138
+ return {'root': str(_code_root), 'source': 'code'}
139
+
140
+ env = os.environ.get(ENV_VAR)
141
+ if env and _is_tq_install(Path(env)):
142
+ return {'root': str(Path(env)), 'source': 'env'}
143
+
144
+ for candidate in _iter_registry_tdx_dirs():
145
+ root = Path(str(candidate).rstrip('\\/'))
146
+ if _is_tq_install(root):
147
+ return {'root': str(root), 'source': 'auto'}
148
+
149
+ return {'root': None, 'source': None}
150
+
151
+
152
+ def resolve_tdx_root() -> Path:
153
+ """取当前生效的通达信目录;找不到时给出可照做的三条出路。"""
154
+ info = get_tdx_root_info()
155
+ if info['root']:
156
+ return Path(info['root'])
157
+ raise RuntimeError(
158
+ "未找到支持 TQ 策略的通达信安装目录。请任选其一:\n"
159
+ " 1) 安装「支持 TQ 策略」的通达信(new_tdx64 版)\n"
160
+ f" 2) 设置环境变量 {ENV_VAR} 指向其安装目录\n"
161
+ " 3) 在代码里调用 qka.set_tdx_root(r'...')"
162
+ )
163
+
164
+
165
+ # ── 连接:惰性加载 tqcenter 并初始化 ────────────────────────────
166
+
167
+ def get_tq():
168
+ """惰性加载并返回通达信的 TQ 客户端(进程级复用、线程安全)。
169
+
170
+ 首次调用时:定位安装目录 → 把 ``PYPlugins/user`` 加入 sys.path → ``import tqcenter``
171
+ → ``tq.initialize(...)``。之后复用同一实例。
172
+
173
+ Returns:
174
+ tqcenter.tq: 通达信数据访问类
175
+
176
+ Raises:
177
+ RuntimeError: 未找到安装目录 / 加载 TQ 模块失败 / 初始化失败(客户端未启动等)
178
+ """
179
+ global _tq_client
180
+ if _tq_client is not None:
181
+ return _tq_client
182
+
183
+ with _tq_lock:
184
+ if _tq_client is not None:
185
+ return _tq_client
186
+
187
+ root = resolve_tdx_root()
188
+ plugin_dir = root.joinpath('PYPlugins', 'user')
189
+ if str(plugin_dir) not in sys.path:
190
+ sys.path.insert(0, str(plugin_dir))
191
+
192
+ try:
193
+ from tqcenter import tq
194
+ except Exception as e: # noqa: BLE001 - 原样上报给调用方
195
+ raise RuntimeError(f"加载通达信 TQ 模块失败({plugin_dir}): {e}") from e
196
+
197
+ # 初始化参数只是一个标识用的「策略文件路径」,不要求文件真实存在
198
+ try:
199
+ tq.initialize(str(Path(__file__).resolve()))
200
+ except Exception as e: # noqa: BLE001
201
+ raise RuntimeError(
202
+ "通达信 TQ 初始化失败:请确认客户端(TdxW.exe)已启动。"
203
+ f"\n原始错误: {e}"
204
+ ) from e
205
+
206
+ _tq_client = tq
207
+ return _tq_client
@@ -23,10 +23,10 @@ data = Data(
23
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
- | `source` | `str` | `'tickflow'` | 数据源。`'tickflow'`(默认)行情覆盖全,**含 ETF / 可转债全历史**;`'baostock'` 提供估值等扩展字段(见 `extra_fields`) |
26
+ | `source` | `str` | `'tickflow'` | 数据源。`'tickflow'`(默认)行情覆盖全,**含 ETF / 可转债全历史**;`'baostock'` 提供估值等扩展字段(见 `extra_fields`);`'tdx'` 走本机「TQ 版」通达信客户端(仅 Windows,需客户端在运行),仅行情、无扩展字段,安装目录见下方「通达信数据源」 |
27
27
  | `benchmark` | `str` | `None` | 基准指数代码,如 `'000300.SH'`。下载后在 `get()` 结果中追加 `benchmark|returns` 列 |
28
28
  | `indicators` | `dict` | `None` | 预计算指标,见下方 |
29
- | `extra_fields` | `list[str]` | `None` | 扩展字段(选股/估值用)。**仅 `source='baostock'` 提供**;`tickflow` 源无这些列,传入会被静默忽略,见下方 |
29
+ | `extra_fields` | `list[str]` | `None` | 扩展字段(选股/估值用)。**仅 `source='baostock'` 提供**;`tickflow` / `tdx` 源无这些列,传入会被静默忽略,见下方 |
30
30
  | `warmup` | `int` | `0` | 指标预热天数。回测/取数时自动多读 `warmup` 个交易日历史用于计算指标,使第 1 个交易日即拿到有效指标值(无需手写跳过前 N 根 bar)。仅用于计算,不增加回测 bar 数 |
31
31
 
32
32
  > **warmup(指标预热)**:若指标/因子需要较长历史窗口(如动量排名需看前 200 天),设 `warmup=200`。
@@ -47,6 +47,30 @@ data = Data(
47
47
  |------|------|------|
48
48
  | `returns` | `close.diff() / close.shift(1)` | 日收益率,始终存在,无需在 indicators 中声明 |
49
49
 
50
+ ### 通达信数据源
51
+
52
+ `source='tdx'` 走本机安装的「支持 TQ 策略」通达信客户端(在进程内 `import tqcenter`,由客户端与本机 `TdxW.exe` 通信)。前提与边界:
53
+
54
+ - **仅 Windows**,且用它的机器需装着 TQ 版通达信、**客户端在运行**、并已登录;
55
+ - **只接行情**(日线 OHLCV + 成交额),**没有** `extra_fields` 扩展列;
56
+ - 复权由通达信自己按 `adjust` 算好(`qfq`→前复权 / `hfq`→后复权 / `bfq`→不复权),qka 不另算。
57
+
58
+ 安装目录的定位与查询(只有该源需要):
59
+
60
+ ```python
61
+ import qka
62
+
63
+ qka.get_tdx_root_info() # {'root': 'D:\\new_tdx64', 'source': 'auto'} —— 只做识别,不建连接,可随时查
64
+ qka.set_tdx_root(r'D:\new_tdx64') # 代码里显式指定(优先级最高)
65
+ ```
66
+
67
+ 优先级:`set_tdx_root()` > 环境变量 `QKA_TDX_ROOT` > 自动读注册表识别。三者都没有时,取数会抛出带解决步骤的 `RuntimeError`。
68
+
69
+ ```python
70
+ data = Data(symbols=['600519.SH'], source='tdx', adjust='qfq')
71
+ df = data.get(start_date='2024-01-01', end_date='2026-10-09')
72
+ ```
73
+
50
74
  ### 模拟标的
51
75
 
52
76
  `symbols` 的元素除了真实代码字符串,还可以是 `Simulate` 对象——一只由 qka **现场生成**的行情,不下载、不读缓存。写法与真实标的完全同构:
@@ -98,7 +122,7 @@ r_t = drift + reversion * (logAnchor - logP_{t-1}) + vol * eps_t eps_t ~
98
122
 
99
123
  `extra_fields` 追加扩展列(选股/估值用),列名同样遵循 `{symbol}|{field}` 约定(如 `000001.SZ|peTTM`),数值自动转为 `float64`。**不是指标,不参与 indicators 预计算**,是随行情一起下载的原始字段。
100
124
 
101
- > ⚠️ **仅 `source='baostock'` 提供**:这些扩展列来自 baostock 的 `query_history_k_data_plus` 接口。默认源 `'tickflow'` 的历史日线固定只有 OHLCV + 成交额,**没有**这些扩展列——此时传入的 `extra_fields` 会被静默忽略(不报错、也不生成对应列)。需要估值/换手率等字段时,显式用 `Data(..., source='baostock')`。
125
+ > ⚠️ **仅 `source='baostock'` 提供**:这些扩展列来自 baostock 的 `query_history_k_data_plus` 接口。默认源 `'tickflow'` 与 `'tdx'` 的历史日线固定只有 OHLCV + 成交额,**没有**这些扩展列——此时传入的 `extra_fields` 会被静默忽略(不报错、也不生成对应列)。需要估值/换手率等字段时,显式用 `Data(..., source='baostock')`。
102
126
 
103
127
  **不传 `extra_fields` 时,每只股票默认只有 6 个行情列 + 1 个常驻列:**
104
128
 
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