qka 2.5.5.dev2__tar.gz → 2.5.6__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.5.5.dev2 → qka-2.5.6}/PKG-INFO +1 -1
  2. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/data.py +166 -18
  3. {qka-2.5.5.dev2 → qka-2.5.6}/.github/workflows/release.yml +0 -0
  4. {qka-2.5.5.dev2 → qka-2.5.6}/.gitignore +0 -0
  5. {qka-2.5.5.dev2 → qka-2.5.6}/.vscode/settings.json +0 -0
  6. {qka-2.5.5.dev2 → qka-2.5.6}/CHANGELOG.md +0 -0
  7. {qka-2.5.5.dev2 → qka-2.5.6}/LICENSE +0 -0
  8. {qka-2.5.5.dev2 → qka-2.5.6}/README.md +0 -0
  9. {qka-2.5.5.dev2 → qka-2.5.6}/pyproject.toml +0 -0
  10. {qka-2.5.5.dev2 → qka-2.5.6}/qka/__init__.py +0 -0
  11. {qka-2.5.5.dev2 → qka-2.5.6}/qka/cli.py +0 -0
  12. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/__init__.py +0 -0
  13. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/accessor.py +0 -0
  14. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/analysis.py +0 -0
  15. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/backtest.py +0 -0
  16. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/broker.py +0 -0
  17. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/indicator.py +0 -0
  18. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/report.py +0 -0
  19. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/simulate.py +0 -0
  20. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/sizing.py +0 -0
  21. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/strategy.py +0 -0
  22. {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/tdx.py +0 -0
  23. {qka-2.5.5.dev2 → qka-2.5.6}/qka/utils/__init__.py +0 -0
  24. {qka-2.5.5.dev2 → qka-2.5.6}/qka/utils/logger.py +0 -0
  25. {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/SKILL.md +0 -0
  26. {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/analysis.md +0 -0
  27. {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/backtest.md +0 -0
  28. {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/broker.md +0 -0
  29. {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/data.md +0 -0
  30. {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/sizing.md +0 -0
  31. {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/strategy.md +0 -0
  32. {qka-2.5.5.dev2 → qka-2.5.6}/tools/generate_api_ref.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: qka
3
- Version: 2.5.5.dev2
3
+ Version: 2.5.6
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
@@ -1017,7 +1017,9 @@ class Data():
1017
1017
  parquet_path)
1018
1018
  ddf['returns'] = ddf['close'].diff() / ddf['close'].shift(1)
1019
1019
  ddf = self._drop_head_lazy(ddf)
1020
- ddf = self._apply_factors(ddf)
1020
+ # 🚨 因子不在这里算 —— 要等基准注入之后统一算(见下方 _apply_all_factors)。
1021
+ # 原因:内置因子和自定义因子没有本质区别,都可能是「相对基准」的,
1022
+ # 唯一合理的位置是「拿到基准之后」,而不是分两处、按两种时机各算一遍。
1021
1023
  column_mapping = {col: f'{symbol}|{col}' for col in ddf.columns}
1022
1024
  dfs.append(ddf.rename(columns=column_mapping))
1023
1025
 
@@ -1028,17 +1030,19 @@ class Data():
1028
1030
  )
1029
1031
 
1030
1032
  ddf = dd.concat(dfs, axis=1, join='outer')
1031
- # 切片回用户请求的日期范围(去掉指标预热扩展部分)
1032
- if start_date is not None and max_window > 0:
1033
- cutoff = pd.Timestamp(start_date)
1034
- ddf = ddf.loc[cutoff:]
1035
1033
 
1036
- # 基准数据
1034
+ # 基准数据(必须在算因子之前 —— 因子可能需要它)
1037
1035
  if self.benchmark:
1038
1036
  ddf = self._add_benchmark(ddf, self.target_dir, pq_filters)
1039
1037
 
1040
- # qka 内置指标(需要 benchmark 和 symbol 前缀)
1041
- ddf = self._apply_qka_factors(ddf)
1038
+ # 所有因子,一处计算:内置 / ta / 自定义一视同仁
1039
+ ddf = self._apply_all_factors(ddf)
1040
+
1041
+ # 切片回用户请求的日期范围(去掉指标预热扩展部分)——
1042
+ # 必须放在算因子之后:因子要吃掉预热那段数据,切早了窗口就不够长。
1043
+ if start_date is not None and max_window > 0:
1044
+ cutoff = pd.Timestamp(start_date)
1045
+ ddf = ddf.loc[cutoff:]
1042
1046
 
1043
1047
  return ddf
1044
1048
 
@@ -1079,21 +1083,18 @@ class Data():
1079
1083
  # 补齐会重算 returns,先掐会被洗回来;因子在后面才吃 returns。
1080
1084
  result = self._drop_head_returns(result)
1081
1085
 
1082
- # 因子:逐票逐因子贴上(带缓存 —— 全盖住就不算,只缺尾巴就只补尾巴)
1083
- # 注意在切片之前算:此时还带着预热数据,滚动窗口才够长。
1084
- result = self._factors_with_cache(result)
1086
+ # 基准数据(必须在算因子之前 —— 因子可能需要它)
1087
+ if self.benchmark:
1088
+ result = self._add_benchmark(result, self.target_dir, pq_filters)
1089
+
1090
+ # 所有因子,一处计算:内置 / ta / 自定义一视同仁(带缓存)
1091
+ # 注意仍在切片之前:此时还带着预热数据,滚动窗口才够长。
1092
+ result = self._all_factors_with_cache(result)
1085
1093
 
1086
1094
  # 切片回用户请求的日期范围
1087
1095
  if start_date is not None and max_window > 0:
1088
1096
  result = result[result.index >= pd.Timestamp(start_date)]
1089
1097
 
1090
- # 基准数据
1091
- if self.benchmark:
1092
- result = self._add_benchmark(result, self.target_dir, pq_filters)
1093
-
1094
- # qka 内置因子(需要 benchmark 和 symbol 前缀)
1095
- result = self._qka_factors_with_cache(result)
1096
-
1097
1098
  # 列顺序复原,与重构前一致
1098
1099
  return self._order_columns(result, self._qka_factor_names())
1099
1100
 
@@ -1164,6 +1165,149 @@ class Data():
1164
1165
  lambda part: part.assign(returns=part['returns'].where(part.index > cutoff))
1165
1166
  )
1166
1167
 
1168
+ def _all_factors_with_cache(self, result):
1169
+ """算所有因子 —— 内置 / ta / 自定义,**同一处、同一时机**(基准注入之后)。
1170
+
1171
+ 缓存仍按因子粒度分两笔账(内置走 `_qka_factors_with_cache`,其余走
1172
+ `_factors_with_cache`),但它们是同一个阶段的两笔账,不是两条路径:
1173
+ 两者都在基准注入之后执行,都能看到基准。
1174
+
1175
+ 🚨 为什么必须有基准之后才算:因子可能定义成「相对大盘」的量(β、残差波动、
1176
+ 相关系数…)。内置的和自定义的在这个问题上没有任何区别 —— 所以不存在
1177
+ 「自定义因子先算一遍、内置因子后算一遍」这种分法。
1178
+ """
1179
+ if not self._factors:
1180
+ return result
1181
+ result = self._factors_with_cache(result)
1182
+ result = self._qka_factors_with_cache(result)
1183
+ return result
1184
+
1185
+ def _apply_all_factors(self, df):
1186
+ """所有因子一处算(无缓存路径;懒加载 / dask 走这里)。df 已含基准。"""
1187
+ inds = self._factors
1188
+ if not inds:
1189
+ return df
1190
+ if callable(inds):
1191
+ # 旧版整表函数形式:直接把整表交给它
1192
+ if isinstance(df, dd.DataFrame):
1193
+ return df.map_partitions(lambda p: inds(p.copy()))
1194
+ return inds(df.copy())
1195
+ if isinstance(df, dd.DataFrame):
1196
+ sample = df.head(self._min_rows_for_factors())
1197
+ meta = self._compute_all_factor_cols(sample.copy())
1198
+ return df.map_partitions(
1199
+ lambda p: self._compute_all_factor_cols(p.copy()), meta=meta)
1200
+ return self._compute_all_factor_cols(df.copy())
1201
+
1202
+ def _compute_all_factor_cols(self, df, only=None):
1203
+ """逐票把该票的全部因子算出来,写回 `{symbol}|{因子}` 列。
1204
+
1205
+ 🚨 内置指标和自定义因子在这里没有区别:都是「拿这只票的数据,吐一列」。
1206
+ 差别只有函数从哪来 —— 内置的从 qka.core.indicator 取,自定义的是传进来的
1207
+ 函数对象,ta 的是从 `ta.xxx.yyy` 这个路径 import 出来的。
1208
+ """
1209
+ if not isinstance(self._factors, dict):
1210
+ return df
1211
+ col_index = self._columns_by_symbol(df.columns)
1212
+ has_bench = 'benchmark|returns' in df.columns
1213
+
1214
+ for symbol in self.symbols:
1215
+ cols = col_index.get(symbol)
1216
+ if not cols:
1217
+ continue
1218
+ plain = self._symbol_slice(df, symbol, cols)
1219
+ if plain.empty:
1220
+ continue
1221
+ # 基准给到自定义因子时叫 bench —— 与内置看到的 benchmark|returns 同源同值
1222
+ if has_bench:
1223
+ plain['bench'] = df['benchmark|returns'].reindex(plain.index)
1224
+
1225
+ for name, spec in self._factors.items():
1226
+ if only is not None and name not in only:
1227
+ continue
1228
+ series = self._one_factor(plain, symbol, name, spec)
1229
+ if series is not None:
1230
+ df[f'{symbol}|{name}'] = series.reindex(df.index)
1231
+ return df
1232
+
1233
+ def _one_factor(self, plain, symbol, name, spec):
1234
+ """算单个因子。返回一列 Series,认不出/算失败返回 None。
1235
+
1236
+ plain: 这只票的表,列名**已去前缀**(open/high/low/close/volume/amount/returns),
1237
+ 外加一列 bench(基准收益,有基准时才有)。
1238
+ """
1239
+ # ① 自定义函数:直接调,它自己决定用不用 bench
1240
+ if callable(spec):
1241
+ try:
1242
+ res = spec(plain)
1243
+ except Exception as e: # noqa: BLE001
1244
+ logger.warning(f'计算 {symbol}|{name} 失败: {type(e).__name__}: {e}')
1245
+ return None
1246
+ if isinstance(res, pd.DataFrame):
1247
+ return res[name] if name in res.columns else None
1248
+ return res
1249
+
1250
+ if not isinstance(spec, (list, tuple)) or not spec:
1251
+ logger.warning(f'因子 {name} 的定义必须是 tuple 或 callable,跳过')
1252
+ return None
1253
+
1254
+ ind_type, args = spec[0], list(spec[1:])
1255
+
1256
+ # ② qka 内置指标:从 indicator 模块取函数,喂「带前缀的小表」
1257
+ if _is_qka_factor(ind_type):
1258
+ opts = args.pop() if args and isinstance(args[-1], dict) else {}
1259
+ lim = opts.get('max_abs_return')
1260
+ fn_name = ind_type.split('.', 1)[1] if ind_type.startswith('qka.') else ind_type
1261
+ fn = getattr(indicator, fn_name, None)
1262
+ if fn is None:
1263
+ logger.warning(f'qka 内置指标不存在: {fn_name},跳过')
1264
+ return None
1265
+ work = pd.DataFrame({f'{symbol}|returns': plain['returns']})
1266
+ if 'bench' in plain.columns:
1267
+ work['benchmark|returns'] = plain['bench']
1268
+ if lim:
1269
+ for c in work.columns:
1270
+ work[c] = work[c].where(work[c].abs() <= lim)
1271
+ rf_kwargs = {}
1272
+ if (self.risk_free is not None and _accepts_rf(fn)
1273
+ and not _rf_given_positionally(fn, args)):
1274
+ rf_kwargs['rf'] = self.risk_free
1275
+ try:
1276
+ return fn(work, symbol, *args, **rf_kwargs)
1277
+ except ValueError as e:
1278
+ logger.warning(f'计算 {symbol}|{name} 失败: {e}')
1279
+ return None
1280
+
1281
+ # ③ ta 指标:'ta.trend.sma_indicator' + 列名 + 参数
1282
+ cols, rest_start = [], 0
1283
+ for i, arg in enumerate(args):
1284
+ if isinstance(arg, str):
1285
+ cols.append(arg)
1286
+ rest_start = i + 1
1287
+ else:
1288
+ break
1289
+ if not cols:
1290
+ logger.warning(f'因子 {name} 缺少计算列名,跳过')
1291
+ return None
1292
+ rest = args[rest_start:]
1293
+ if not isinstance(ind_type, str) or '.' not in ind_type:
1294
+ logger.warning(f'因子 {name} 的类型必须为 "ta.xxx.yyy" 格式,跳过: {ind_type}')
1295
+ return None
1296
+ import importlib
1297
+ module_name, func_name = ind_type.rsplit('.', 1)
1298
+ try:
1299
+ fn = getattr(importlib.import_module(module_name), func_name)
1300
+ except (ImportError, AttributeError) as e:
1301
+ logger.warning(f'无法加载指标函数 {ind_type}: {e}')
1302
+ return None
1303
+ try:
1304
+ if len(cols) == 1:
1305
+ return fn(plain[cols[0]], *rest)
1306
+ return fn(*[plain[f] for f in cols], *rest)
1307
+ except Exception as e: # noqa: BLE001
1308
+ logger.warning(f'计算 {symbol}|{name} 失败: {type(e).__name__}: {e}')
1309
+ return None
1310
+
1167
1311
  def _apply_factors(self, df):
1168
1312
  """
1169
1313
  对单只股票的数据应用预定义的指标/因子。
@@ -1903,6 +2047,10 @@ class Data():
1903
2047
  sub = self._symbol_slice(result, symbol, cols)
1904
2048
  if sub.empty:
1905
2049
  continue
2050
+ # 基准也交给自定义因子(叫 bench)—— 此时 benchmark 已注入,
2051
+ # 自定义因子和内置因子看到的是同一份基准收益
2052
+ if 'benchmark|returns' in result.columns:
2053
+ sub['bench'] = result['benchmark|returns'].reindex(sub.index)
1906
2054
 
1907
2055
  # 模拟标的没有缓存文件;没开缓存 / 配方认不出也一样 —— 一律现算不落盘
1908
2056
  if plan is None or symbol in self._sim_specs:
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